+
{photos.length ? (
= ({
if (filter.displayMode === DisplayMode.Grid) {
return (
- = ({
pageCount={pageCount}
handleImageOpen={handleImageOpen}
zoomIndex={filter.zoomIndex}
+ selectedIds={selectedIds}
+ onSelectChange={onSelectChange}
+ selecting={!!selectedIds && selectedIds.size > 0}
/>
);
}
@@ -252,9 +302,17 @@ function getCount(result: GQL.FindImagesQueryResult) {
return result?.data?.findImages?.count ?? 0;
}
-function renderMetadataByline(result: GQL.FindImagesQueryResult) {
- const megapixels = result?.data?.findImages?.megapixels;
- const size = result?.data?.findImages?.filesize;
+function renderMetadataByline(
+ result: GQL.FindImagesQueryResult,
+ metadataInfo?: GQL.FindImagesMetadataQueryResult
+) {
+ const megapixels = metadataInfo?.data?.findImages?.megapixels;
+ const size = metadataInfo?.data?.findImages?.filesize;
+
+ if (metadataInfo?.loading) {
+ // return ellipsis
+ return (...);
+ }
if (!megapixels && !size) {
return;
@@ -289,167 +347,185 @@ interface IImageList {
chapters?: GQL.GalleryChapterDataFragment[];
}
-export const ImageList: React.FC = ({
- filterHook,
- view,
- alterQuery,
- extraOperations,
- chapters = [],
-}) => {
- const intl = useIntl();
- const history = useHistory();
- const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
- const [isExportAll, setIsExportAll] = useState(false);
- const [slideshowRunning, setSlideshowRunning] = useState(false);
+export const ImageList: React.FC = PatchComponent(
+ "ImageList",
+ ({ filterHook, view, alterQuery, extraOperations = [], chapters = [] }) => {
+ const intl = useIntl();
+ const history = useHistory();
+ const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
+ const [isExportAll, setIsExportAll] = useState(false);
+ const [slideshowRunning, setSlideshowRunning] = useState(false);
- const filterMode = GQL.FilterMode.Images;
+ const filterMode = GQL.FilterMode.Images;
- const otherOperations = [
- ...(extraOperations ?? []),
- {
- text: intl.formatMessage({ id: "actions.view_random" }),
- onClick: viewRandom,
- },
- {
- text: intl.formatMessage({ id: "actions.export" }),
- onClick: onExport,
- isDisplayed: showWhenSelected,
- },
- {
- text: intl.formatMessage({ id: "actions.export_all" }),
- onClick: onExportAll,
- },
- ];
+ const { modal, showModal, closeModal } = useModal();
- function addKeybinds(
- result: GQL.FindImagesQueryResult,
- filter: ListFilterModel
- ) {
- Mousetrap.bind("p r", () => {
- viewRandom(result, filter);
- });
+ const otherOperations: IItemListOperation[] = [
+ ...extraOperations,
+ {
+ text: intl.formatMessage({ id: "actions.view_random" }),
+ onClick: viewRandom,
+ },
+ {
+ text: `${intl.formatMessage({ id: "actions.generate" })}…`,
+ onClick: (result, filter, selectedIds) => {
+ showModal(
+ closeModal()}
+ />
+ );
+ return Promise.resolve();
+ },
+ isDisplayed: showWhenSelected,
+ },
+ {
+ text: intl.formatMessage({ id: "actions.export" }),
+ onClick: onExport,
+ isDisplayed: showWhenSelected,
+ },
+ {
+ text: intl.formatMessage({ id: "actions.export_all" }),
+ onClick: onExportAll,
+ },
+ ];
- return () => {
- Mousetrap.unbind("p r");
- };
- }
+ function addKeybinds(
+ result: GQL.FindImagesQueryResult,
+ filter: ListFilterModel
+ ) {
+ Mousetrap.bind("p r", () => {
+ viewRandom(result, filter);
+ });
- async function viewRandom(
- result: GQL.FindImagesQueryResult,
- filter: ListFilterModel
- ) {
- // query for a random image
- if (result.data?.findImages) {
- const { count } = result.data.findImages;
+ return () => {
+ Mousetrap.unbind("p r");
+ };
+ }
- const index = Math.floor(Math.random() * count);
- const filterCopy = cloneDeep(filter);
- filterCopy.itemsPerPage = 1;
- filterCopy.currentPage = index + 1;
- const singleResult = await queryFindImages(filterCopy);
- if (singleResult.data.findImages.images.length === 1) {
- const { id } = singleResult.data.findImages.images[0];
- // navigate to the image player page
- history.push(`/images/${id}`);
+ async function viewRandom(
+ result: GQL.FindImagesQueryResult,
+ filter: ListFilterModel
+ ) {
+ // query for a random image
+ if (result.data?.findImages) {
+ const { count } = result.data.findImages;
+
+ const index = Math.floor(Math.random() * count);
+ const filterCopy = cloneDeep(filter);
+ filterCopy.itemsPerPage = 1;
+ filterCopy.currentPage = index + 1;
+ const singleResult = await queryFindImages(filterCopy);
+ if (singleResult.data.findImages.images.length === 1) {
+ const { id } = singleResult.data.findImages.images[0];
+ // navigate to the image player page
+ history.push(`/images/${id}`);
+ }
}
}
- }
- async function onExport() {
- setIsExportAll(false);
- setIsExportDialogOpen(true);
- }
+ async function onExport() {
+ setIsExportAll(false);
+ setIsExportDialogOpen(true);
+ }
- async function onExportAll() {
- setIsExportAll(true);
- setIsExportDialogOpen(true);
- }
+ async function onExportAll() {
+ setIsExportAll(true);
+ setIsExportDialogOpen(true);
+ }
+
+ function renderContent(
+ result: GQL.FindImagesQueryResult,
+ filter: ListFilterModel,
+ selectedIds: Set,
+ onSelectChange: (
+ id: string,
+ selected: boolean,
+ shiftKey: boolean
+ ) => void,
+ onChangePage: (page: number) => void,
+ pageCount: number
+ ) {
+ function maybeRenderImageExportDialog() {
+ if (isExportDialogOpen) {
+ return (
+ setIsExportDialogOpen(false)}
+ />
+ );
+ }
+ }
+
+ function renderImages() {
+ if (!result.data?.findImages) return;
- function renderContent(
- result: GQL.FindImagesQueryResult,
- filter: ListFilterModel,
- selectedIds: Set,
- onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void,
- onChangePage: (page: number) => void,
- pageCount: number
- ) {
- function maybeRenderImageExportDialog() {
- if (isExportDialogOpen) {
return (
- setIsExportDialogOpen(false)}
+
);
}
- }
-
- function renderImages() {
- if (!result.data?.findImages) return;
return (
-
+ <>
+ {maybeRenderImageExportDialog()}
+ {renderImages()}
+ >
);
}
+ function renderEditDialog(
+ selectedImages: GQL.SlimImageDataFragment[],
+ onClose: (applied: boolean) => void
+ ) {
+ return ;
+ }
+
+ function renderDeleteDialog(
+ selectedImages: GQL.SlimImageDataFragment[],
+ onClose: (confirmed: boolean) => void
+ ) {
+ return ;
+ }
+
return (
- <>
- {maybeRenderImageExportDialog()}
- {renderImages()}
- >
+
+ {modal}
+
+
);
}
-
- function renderEditDialog(
- selectedImages: GQL.SlimImageDataFragment[],
- onClose: (applied: boolean) => void
- ) {
- return ;
- }
-
- function renderDeleteDialog(
- selectedImages: GQL.SlimImageDataFragment[],
- onClose: (confirmed: boolean) => void
- ) {
- return ;
- }
-
- return (
-
-
-
- );
-};
+);
diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx
index f0fc84493..6499be894 100644
--- a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx
+++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx
@@ -7,6 +7,7 @@ import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { ImageCard } from "./ImageCard";
+import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@@ -14,38 +15,44 @@ interface IProps {
header: string;
}
-export const ImageRecommendationRow: React.FC = (props: IProps) => {
- const result = useFindImages(props.filter);
- const cardCount = result.data?.findImages.count;
+export const ImageRecommendationRow: React.FC = PatchComponent(
+ "ImageRecommendationRow",
+ (props: IProps) => {
+ const result = useFindImages(props.filter);
+ const cardCount = result.data?.findImages.count;
- if (!result.loading && !cardCount) {
- return null;
- }
+ if (!result.loading && !cardCount) {
+ return null;
+ }
- return (
-
-
-
- }
- >
-
+
+
+ }
>
- {result.loading
- ? [...Array(props.filter.itemsPerPage)].map((i) => (
-
- ))
- : result.data?.findImages.images.map((i) => (
-
- ))}
-
-
- );
-};
+
+ {result.loading
+ ? [...Array(props.filter.itemsPerPage)].map((i) => (
+
+ ))
+ : result.data?.findImages.images.map((i) => (
+
+ ))}
+
+
+ );
+ }
+);
diff --git a/ui/v2.5/src/components/Images/ImageWallItem.tsx b/ui/v2.5/src/components/Images/ImageWallItem.tsx
index 8403b3a98..a9f681474 100644
--- a/ui/v2.5/src/components/Images/ImageWallItem.tsx
+++ b/ui/v2.5/src/components/Images/ImageWallItem.tsx
@@ -1,38 +1,50 @@
import React from "react";
-import type {
- RenderImageProps,
- renderImageClickHandler,
- PhotoProps,
-} from "react-photo-gallery";
+import { Form } from "react-bootstrap";
+import type { RenderImageProps } from "react-photo-gallery";
+import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect";
-interface IImageWallProps {
- margin?: string;
- index: number;
- photo: PhotoProps;
- onClick: renderImageClickHandler | null;
- direction: "row" | "column";
- top?: number;
- left?: number;
+interface IExtraProps {
+ maxHeight: number;
+ selected?: boolean;
+ onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
+ selecting?: boolean;
}
-export const ImageWallItem: React.FC = (
- props: IImageWallProps
+export const ImageWallItem: React.FC = (
+ 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;
- 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
) {
+ 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 });
}
@@ -41,18 +53,39 @@ export const ImageWallItem: React.FC = (
const video = props.photo.src.includes("preview");
const ImagePreview = video ? "video" : "img";
+ let shiftKey = false;
+
return (
-
+ {...dragProps}
+ >
+ {props.onSelectedChanged && (
+ props.onSelectedChanged!(!props.selected, shiftKey)}
+ onClick={(event: React.MouseEvent) => {
+ shiftKey = event.shiftKey;
+ event.stopPropagation();
+ }}
+ />
+ )}
+
+
);
};
diff --git a/ui/v2.5/src/components/Images/styles.scss b/ui/v2.5/src/components/Images/styles.scss
index 936947bc3..0050a9434 100644
--- a/ui/v2.5/src/components/Images/styles.scss
+++ b/ui/v2.5/src/components/Images/styles.scss
@@ -86,6 +86,7 @@
}
&-preview {
+ align-items: center;
display: flex;
justify-content: center;
margin-bottom: 5px;
@@ -94,7 +95,6 @@
&-image {
height: 100%;
object-fit: contain;
- object-position: top;
width: 100%;
}
@@ -175,6 +175,10 @@ $imageTabWidth: 450px;
font-size: 1.3em;
height: calc(1.5em + 0.75rem + 2px);
}
+
+ .form-group[data-field="urls"] .string-list-input input.form-control {
+ font-size: 0.85em;
+ }
}
.image-file-card.card {
diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx
index 4b31ac31a..3f0f486b8 100644
--- a/ui/v2.5/src/components/List/EditFilterDialog.tsx
+++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx
@@ -1,7 +1,6 @@
import cloneDeep from "lodash-es/cloneDeep";
import React, {
useCallback,
- useContext,
useEffect,
useMemo,
useRef,
@@ -14,7 +13,7 @@ import {
CriterionOption,
} from "src/models/list-filter/criteria/criterion";
import { FormattedMessage, useIntl } from "react-intl";
-import { ConfigurationContext } from "src/hooks/Config";
+import { useConfigurationContext } from "src/hooks/Config";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getFilterOptions } from "src/models/list-filter/factory";
import { FilterTags } from "./FilterTags";
@@ -29,11 +28,16 @@ import {
import { useCompare, usePrevious } from "src/hooks/state";
import { CriterionType } from "src/models/list-filter/types";
import { useToast } from "src/hooks/Toast";
-import { useConfigureUI } from "src/core/StashService";
-import { FilterMode } from "src/core/generated-graphql";
+import { useConfigureUI, useSaveFilter } from "src/core/StashService";
+import {
+ FilterMode,
+ SavedFilterDataFragment,
+} from "src/core/generated-graphql";
import { useFocusOnce } from "src/utils/focus";
import Mousetrap from "mousetrap";
import ScreenUtils from "src/utils/screen";
+import { LoadFilterDialog, SaveFilterDialog } from "./SavedFilterList";
+import { SearchTermInput } from "./ListFilter";
interface ICriterionList {
criteria: string[];
@@ -45,6 +49,7 @@ interface ICriterionList {
optionSelected: (o?: CriterionOption) => void;
onRemoveCriterion: (c: string) => void;
onTogglePin: (c: CriterionOption) => void;
+ externallySelected?: boolean;
}
const CriterionOptionList: React.FC
= ({
@@ -57,7 +62,11 @@ const CriterionOptionList: React.FC = ({
optionSelected,
onRemoveCriterion,
onTogglePin,
+ externallySelected = false,
}) => {
+ const { configuration } = useConfigurationContext();
+ const { sfwContentMode } = configuration.interface;
+
const prevCriterion = usePrevious(currentCriterion);
const scrolled = useRef(false);
@@ -96,14 +105,19 @@ const CriterionOptionList: React.FC = ({
// scrolling to the current criterion doesn't work well when the
// dialog is already open, so limit to when we click on the
// criterion from the external tags
- if (!scrolled.current && type && criteriaRefs[type]?.current) {
+ if (
+ externallySelected &&
+ !scrolled.current &&
+ type &&
+ criteriaRefs[type]?.current
+ ) {
criteriaRefs[type].current!.scrollIntoView({
behavior: "smooth",
block: "start",
});
scrolled.current = true;
}
- }, [currentCriterion, criteriaRefs, type]);
+ }, [externallySelected, currentCriterion, criteriaRefs, type]);
function getReleventCriterion(t: CriterionType) {
if (currentCriterion?.criterionOption.type === t) {
@@ -136,7 +150,9 @@ const CriterionOptionList: React.FC = ({
className="collapse-icon fa-fw"
icon={type === c.type ? faChevronDown : faChevronRight}
/>
-
+
{criteria.some((cc) => c.type === cc) && (