mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Improved list view for scenes, galleries and performers (#4368)
Co-authored-by: InfiniteStash <117855276+InfiniteStash@users.noreply.github.com> Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
e7311a60d2
commit
dd8da7f339
15 changed files with 1553 additions and 423 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import cloneDeep from "lodash-es/cloneDeep";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
import { Table } from "react-bootstrap";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Link, useHistory } from "react-router-dom";
|
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
|
|
@ -18,7 +17,7 @@ import GalleryWallCard from "./GalleryWallCard";
|
||||||
import { EditGalleriesDialog } from "./EditGalleriesDialog";
|
import { EditGalleriesDialog } from "./EditGalleriesDialog";
|
||||||
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
|
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
import { ExportDialog } from "../Shared/ExportDialog";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { GalleryListTable } from "./GalleryListTable";
|
||||||
|
|
||||||
const GalleryItemList = makeItemList({
|
const GalleryItemList = makeItemList({
|
||||||
filterMode: GQL.FilterMode.Galleries,
|
filterMode: GQL.FilterMode.Galleries,
|
||||||
|
|
@ -152,40 +151,11 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.List) {
|
if (filter.displayMode === DisplayMode.List) {
|
||||||
return (
|
return (
|
||||||
<Table className="col col-sm-6 mx-auto">
|
<GalleryListTable
|
||||||
<thead>
|
galleries={result.data.findGalleries.galleries}
|
||||||
<tr>
|
selectedIds={selectedIds}
|
||||||
<th>{intl.formatMessage({ id: "actions.preview" })}</th>
|
onSelectChange={onSelectChange}
|
||||||
<th className="d-none d-sm-none">
|
|
||||||
{intl.formatMessage({ id: "title" })}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{result.data.findGalleries.galleries.map((gallery) => (
|
|
||||||
<tr key={gallery.id}>
|
|
||||||
<td>
|
|
||||||
<Link to={`/galleries/${gallery.id}`}>
|
|
||||||
{gallery.cover ? (
|
|
||||||
<img
|
|
||||||
loading="lazy"
|
|
||||||
alt={gallery.title ?? ""}
|
|
||||||
className="w-100 w-sm-auto"
|
|
||||||
src={`${gallery.cover.paths.thumbnail}`}
|
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td className="d-none d-sm-block">
|
|
||||||
<Link to={`/galleries/${gallery.id}`}>
|
|
||||||
{galleryTitle(gallery)} ({gallery.image_count}{" "}
|
|
||||||
{gallery.image_count === 1 ? "image" : "images"})
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
if (filter.displayMode === DisplayMode.Wall) {
|
||||||
|
|
|
||||||
256
ui/v2.5/src/components/Galleries/GalleryListTable.tsx
Normal file
256
ui/v2.5/src/components/Galleries/GalleryListTable.tsx
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import NavUtils from "src/utils/navigation";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { objectTitle } from "src/core/files";
|
||||||
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
|
import { useGalleryUpdate } from "src/core/StashService";
|
||||||
|
import { IColumn, ListTable } from "../List/ListTable";
|
||||||
|
import { useTableColumns } from "src/hooks/useTableColumns";
|
||||||
|
|
||||||
|
interface IGalleryListTableProps {
|
||||||
|
galleries: GQL.SlimGalleryDataFragment[];
|
||||||
|
selectedIds: Set<string>;
|
||||||
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABLE_NAME = "galleries";
|
||||||
|
|
||||||
|
export const GalleryListTable: React.FC<IGalleryListTableProps> = (
|
||||||
|
props: IGalleryListTableProps
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const [updateGallery] = useGalleryUpdate();
|
||||||
|
|
||||||
|
function setRating(v: number | null, galleryId: string) {
|
||||||
|
if (galleryId) {
|
||||||
|
updateGallery({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: galleryId,
|
||||||
|
rating100: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CoverImageCell = (gallery: GQL.SlimGalleryDataFragment) => {
|
||||||
|
const title = galleryTitle(gallery);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/galleries/${gallery.id}`}>
|
||||||
|
{gallery.cover ? (
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
alt={title}
|
||||||
|
className="image-thumbnail"
|
||||||
|
src={`${gallery.cover.paths.thumbnail}`}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TitleCell = (gallery: GQL.SlimGalleryDataFragment) => {
|
||||||
|
const title = galleryTitle(gallery);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/galleries/${gallery.id}`}>
|
||||||
|
<span className="ellips-data">{title}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DateCell = (gallery: GQL.SlimGalleryDataFragment) => (
|
||||||
|
<>{gallery.date}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RatingCell = (gallery: GQL.SlimGalleryDataFragment) => (
|
||||||
|
<RatingSystem
|
||||||
|
value={gallery.rating100}
|
||||||
|
onSetRating={(value) => setRating(value, gallery.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ImagesCell = (gallery: GQL.SlimGalleryDataFragment) => {
|
||||||
|
return (
|
||||||
|
<Link to={NavUtils.makeGalleryImagesUrl(gallery)}>
|
||||||
|
<span>{gallery.image_count}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TagCell = (gallery: GQL.SlimGalleryDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{gallery.tags.map((tag) => (
|
||||||
|
<li key={tag.id}>
|
||||||
|
<Link to={NavUtils.makeTagGalleriesUrl(tag)}>
|
||||||
|
<span>{tag.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PerformersCell = (gallery: GQL.SlimGalleryDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{gallery.performers.map((performer) => (
|
||||||
|
<li key={performer.id}>
|
||||||
|
<Link to={NavUtils.makePerformerGalleriesUrl(performer)}>
|
||||||
|
<span>{performer.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StudioCell = (gallery: GQL.SlimGalleryDataFragment) => {
|
||||||
|
if (gallery.studio) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={NavUtils.makeStudioGalleriesUrl(gallery.studio)}
|
||||||
|
title={gallery.studio.name}
|
||||||
|
>
|
||||||
|
<span className="ellips-data">{gallery.studio.name}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SceneCell = (gallery: GQL.SlimGalleryDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{gallery.scenes.map((galleryScene) => (
|
||||||
|
<li key={galleryScene.id}>
|
||||||
|
<Link to={`/scenes/${galleryScene.id}`}>
|
||||||
|
<span className="ellips-data">{objectTitle(galleryScene)}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IColumnSpec {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
defaultShow?: boolean;
|
||||||
|
mandatory?: boolean;
|
||||||
|
render?: (
|
||||||
|
gallery: GQL.SlimGalleryDataFragment,
|
||||||
|
index: number
|
||||||
|
) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allColumns: IColumnSpec[] = [
|
||||||
|
{
|
||||||
|
value: "cover_image",
|
||||||
|
label: intl.formatMessage({ id: "cover_image" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: CoverImageCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "title",
|
||||||
|
label: intl.formatMessage({ id: "title" }),
|
||||||
|
defaultShow: true,
|
||||||
|
mandatory: true,
|
||||||
|
render: TitleCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "date",
|
||||||
|
label: intl.formatMessage({ id: "date" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: DateCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "rating",
|
||||||
|
label: intl.formatMessage({ id: "rating" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: RatingCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "code",
|
||||||
|
label: intl.formatMessage({ id: "scene_code" }),
|
||||||
|
render: (s) => <>{s.code}</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "images",
|
||||||
|
label: intl.formatMessage({ id: "images" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: ImagesCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "tags",
|
||||||
|
label: intl.formatMessage({ id: "tags" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: TagCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "performers",
|
||||||
|
label: intl.formatMessage({ id: "performers" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: PerformersCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "studio",
|
||||||
|
label: intl.formatMessage({ id: "studio" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: StudioCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "scenes",
|
||||||
|
label: intl.formatMessage({ id: "scenes" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: SceneCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "photographer",
|
||||||
|
label: intl.formatMessage({ id: "photographer" }),
|
||||||
|
render: (s) => <>{s.photographer}</>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultColumns = allColumns
|
||||||
|
.filter((col) => col.defaultShow)
|
||||||
|
.map((col) => col.value);
|
||||||
|
|
||||||
|
const { selectedColumns, saveColumns } = useTableColumns(
|
||||||
|
TABLE_NAME,
|
||||||
|
defaultColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnRenderFuncs: Record<
|
||||||
|
string,
|
||||||
|
(gallery: GQL.SlimGalleryDataFragment, index: number) => React.ReactNode
|
||||||
|
> = {};
|
||||||
|
allColumns.forEach((col) => {
|
||||||
|
if (col.render) {
|
||||||
|
columnRenderFuncs[col.value] = col.render;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderCell(
|
||||||
|
column: IColumn,
|
||||||
|
gallery: GQL.SlimGalleryDataFragment,
|
||||||
|
index: number
|
||||||
|
) {
|
||||||
|
const render = columnRenderFuncs[column.value];
|
||||||
|
|
||||||
|
if (render) return render(gallery, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListTable
|
||||||
|
className="gallery-table"
|
||||||
|
items={props.galleries}
|
||||||
|
allColumns={allColumns}
|
||||||
|
columns={selectedColumns}
|
||||||
|
setColumns={(c) => saveColumns(c)}
|
||||||
|
selectedIds={props.selectedIds}
|
||||||
|
onSelectChange={props.onSelectChange}
|
||||||
|
renderCell={renderCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
141
ui/v2.5/src/components/List/ListTable.tsx
Normal file
141
ui/v2.5/src/components/List/ListTable.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Table, Form } from "react-bootstrap";
|
||||||
|
import { CheckBoxSelect } from "../Shared/Select";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
|
export interface IColumn {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
mandatory?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColumnSelector: React.FC<{
|
||||||
|
selected: string[];
|
||||||
|
allColumns: IColumn[];
|
||||||
|
setSelected: (selected: string[]) => void;
|
||||||
|
}> = ({ selected, allColumns, setSelected }) => {
|
||||||
|
const disableOptions = useMemo(() => {
|
||||||
|
return allColumns.map((col) => {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
disabled: col.mandatory,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [allColumns]);
|
||||||
|
|
||||||
|
const selectedColumns = useMemo(() => {
|
||||||
|
return disableOptions.filter((col) => selected.includes(col.value));
|
||||||
|
}, [selected, disableOptions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckBoxSelect
|
||||||
|
options={disableOptions}
|
||||||
|
selectedOptions={selectedColumns}
|
||||||
|
onChange={(v) => {
|
||||||
|
setSelected(v.map((col) => col.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IListTableProps<T> {
|
||||||
|
className?: string;
|
||||||
|
items: T[];
|
||||||
|
columns: string[];
|
||||||
|
setColumns: (columns: string[]) => void;
|
||||||
|
allColumns: IColumn[];
|
||||||
|
selectedIds: Set<string>;
|
||||||
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
|
renderCell: (column: IColumn, item: T, index: number) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListTable = <T extends { id: string }>(
|
||||||
|
props: IListTableProps<T>
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
setColumns,
|
||||||
|
allColumns,
|
||||||
|
selectedIds,
|
||||||
|
onSelectChange,
|
||||||
|
renderCell,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const visibleColumns = useMemo(() => {
|
||||||
|
return allColumns.filter(
|
||||||
|
(col) => col.mandatory || columns.includes(col.value)
|
||||||
|
);
|
||||||
|
}, [columns, allColumns]);
|
||||||
|
|
||||||
|
const renderObjectRow = (item: T, index: number) => {
|
||||||
|
let shiftKey = false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td className="select-col">
|
||||||
|
<label>
|
||||||
|
<Form.Control
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedIds.has(item.id)}
|
||||||
|
onChange={() =>
|
||||||
|
onSelectChange(item.id, !selectedIds.has(item.id), shiftKey)
|
||||||
|
}
|
||||||
|
onClick={(
|
||||||
|
event: React.MouseEvent<HTMLInputElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
shiftKey = event.shiftKey;
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{visibleColumns.map((column) => (
|
||||||
|
<td key={column.value} className={`${column.value}-data`}>
|
||||||
|
{renderCell(column, item, index)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnHeaders = useMemo(() => {
|
||||||
|
return visibleColumns.map((column) => (
|
||||||
|
<th key={column.value} className={`${column.value}-head`}>
|
||||||
|
{column.label}
|
||||||
|
</th>
|
||||||
|
));
|
||||||
|
}, [visibleColumns]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx("table-list", className)}>
|
||||||
|
<Table striped bordered>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="select-col">
|
||||||
|
<div
|
||||||
|
className="d-inline-block"
|
||||||
|
data-toggle="popover"
|
||||||
|
data-trigger="focus"
|
||||||
|
>
|
||||||
|
<ColumnSelector
|
||||||
|
allColumns={allColumns}
|
||||||
|
selected={columns}
|
||||||
|
setSelected={setColumns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{columnHeaders}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th className="border-row" colSpan={100}></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{items.map(renderObjectRow)}</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -370,3 +370,138 @@ input[type="range"].zoom-slider {
|
||||||
.tilted {
|
.tilted {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
max-height: 78dvh;
|
||||||
|
min-width: min-content;
|
||||||
|
overflow-x: auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background-color: #202b33;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-select {
|
||||||
|
margin: 0;
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comma-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4px 2px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 190px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
li::after {
|
||||||
|
content: ", ";
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child::after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comma-list:hover {
|
||||||
|
background: #28343c;
|
||||||
|
border: 1px solid #414c53;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.28);
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
margin-left: -0.4rem;
|
||||||
|
margin-top: -0.9rem;
|
||||||
|
max-width: 40rem;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
white-space: normal;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comma-list li .ellips-data:hover {
|
||||||
|
max-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.ellips-data {
|
||||||
|
display: block;
|
||||||
|
max-width: 190px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating-number {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td.select-col {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
border: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-head {
|
||||||
|
width: 97px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-list tbody tr:hover {
|
||||||
|
background-color: #2d3942;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-list a {
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-list .table-striped td,
|
||||||
|
.table-list .table-striped th {
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,16 @@ import { useIntl } from "react-intl";
|
||||||
import { TagLink } from "src/components/Shared/TagLink";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
|
|
||||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||||
import { StashIDPill } from "src/components/Shared/StashID";
|
import { StashIDPill } from "src/components/Shared/StashID";
|
||||||
|
import {
|
||||||
|
FormatAge,
|
||||||
|
FormatCircumcised,
|
||||||
|
FormatHeight,
|
||||||
|
FormatPenisLength,
|
||||||
|
FormatWeight,
|
||||||
|
} from "../PerformerList";
|
||||||
|
|
||||||
interface IPerformerDetails {
|
interface IPerformerDetails {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
|
|
@ -51,123 +57,6 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatHeight = (height?: number | null) => {
|
|
||||||
if (!height) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [feet, inches] = cmToImperial(height);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="performer-height">
|
|
||||||
<span className="height-metric">
|
|
||||||
{intl.formatNumber(height, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "centimeter",
|
|
||||||
unitDisplay: "short",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="height-imperial">
|
|
||||||
{intl.formatNumber(feet, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "foot",
|
|
||||||
unitDisplay: "narrow",
|
|
||||||
})}
|
|
||||||
{intl.formatNumber(inches, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "inch",
|
|
||||||
unitDisplay: "narrow",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAge = (birthdate?: string | null, deathdate?: string | null) => {
|
|
||||||
if (!birthdate) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const age = TextUtils.age(birthdate, deathdate);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="performer-age">
|
|
||||||
<span className="age">{age}</span>
|
|
||||||
<span className="birthdate"> ({birthdate})</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatWeight = (weight?: number | null) => {
|
|
||||||
if (!weight) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const lbs = kgToLbs(weight);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="performer-weight">
|
|
||||||
<span className="weight-metric">
|
|
||||||
{intl.formatNumber(weight, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "kilogram",
|
|
||||||
unitDisplay: "short",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="weight-imperial">
|
|
||||||
{intl.formatNumber(lbs, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "pound",
|
|
||||||
unitDisplay: "short",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPenisLength = (penis_length?: number | null) => {
|
|
||||||
if (!penis_length) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const inches = cmToInches(penis_length);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="performer-penis-length">
|
|
||||||
<span className="penis-length-metric">
|
|
||||||
{intl.formatNumber(penis_length, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "centimeter",
|
|
||||||
unitDisplay: "short",
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="penis-length-imperial">
|
|
||||||
{intl.formatNumber(inches, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "inch",
|
|
||||||
unitDisplay: "narrow",
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCircumcised = (circumcised?: GQL.CircumisedEnum | null) => {
|
|
||||||
if (!circumcised) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="penis-circumcised">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "circumcised_types." + performer.circumcised,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function maybeRenderExtraDetails() {
|
function maybeRenderExtraDetails() {
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
/* Remove extra urls provided in details since they will be present by perfomr name */
|
/* Remove extra urls provided in details since they will be present by perfomr name */
|
||||||
|
|
@ -224,7 +113,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||||
value={
|
value={
|
||||||
!fullWidth
|
!fullWidth
|
||||||
? TextUtils.age(performer.birthdate, performer.death_date)
|
? TextUtils.age(performer.birthdate, performer.death_date)
|
||||||
: formatAge(performer.birthdate, performer.death_date)
|
: FormatAge(performer.birthdate, performer.death_date)
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
!fullWidth
|
!fullWidth
|
||||||
|
|
@ -266,22 +155,22 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
id="height"
|
id="height"
|
||||||
value={formatHeight(performer.height_cm)}
|
value={FormatHeight(performer.height_cm)}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
id="weight"
|
id="weight"
|
||||||
value={formatWeight(performer.weight)}
|
value={FormatWeight(performer.weight)}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
id="penis_length"
|
id="penis_length"
|
||||||
value={formatPenisLength(performer.penis_length)}
|
value={FormatPenisLength(performer.penis_length)}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
id="circumcised"
|
id="circumcised"
|
||||||
value={formatCircumcised(performer.circumcised)}
|
value={FormatCircumcised(performer.circumcised)}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
|
||||||
import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard";
|
import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard";
|
||||||
import { PerformerListTable } from "./PerformerListTable";
|
import { PerformerListTable } from "./PerformerListTable";
|
||||||
import { EditPerformersDialog } from "./EditPerformersDialog";
|
import { EditPerformersDialog } from "./EditPerformersDialog";
|
||||||
|
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
|
||||||
|
import TextUtils from "src/utils/text";
|
||||||
|
|
||||||
const PerformerItemList = makeItemList({
|
const PerformerItemList = makeItemList({
|
||||||
filterMode: GQL.FilterMode.Performers,
|
filterMode: GQL.FilterMode.Performers,
|
||||||
|
|
@ -34,6 +36,129 @@ const PerformerItemList = makeItemList({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const FormatHeight = (height?: number | null) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
if (!height) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [feet, inches] = cmToImperial(height);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="performer-height">
|
||||||
|
<span className="height-metric">
|
||||||
|
{intl.formatNumber(height, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "centimeter",
|
||||||
|
unitDisplay: "short",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="height-imperial">
|
||||||
|
{intl.formatNumber(feet, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "foot",
|
||||||
|
unitDisplay: "narrow",
|
||||||
|
})}
|
||||||
|
{intl.formatNumber(inches, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "inch",
|
||||||
|
unitDisplay: "narrow",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormatAge = (
|
||||||
|
birthdate?: string | null,
|
||||||
|
deathdate?: string | null
|
||||||
|
) => {
|
||||||
|
if (!birthdate) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const age = TextUtils.age(birthdate, deathdate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="performer-age">
|
||||||
|
<span className="age">{age}</span>
|
||||||
|
<span className="birthdate"> ({birthdate})</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormatWeight = (weight?: number | null) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
if (!weight) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const lbs = kgToLbs(weight);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="performer-weight">
|
||||||
|
<span className="weight-metric">
|
||||||
|
{intl.formatNumber(weight, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "kilogram",
|
||||||
|
unitDisplay: "short",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="weight-imperial">
|
||||||
|
{intl.formatNumber(lbs, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "pound",
|
||||||
|
unitDisplay: "short",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormatCircumcised = (circumcised?: GQL.CircumisedEnum | null) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
if (!circumcised) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="penis-circumcised">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "circumcised_types." + circumcised,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormatPenisLength = (penis_length?: number | null) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
if (!penis_length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const inches = cmToInches(penis_length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="performer-penis-length">
|
||||||
|
<span className="penis-length-metric">
|
||||||
|
{intl.formatNumber(penis_length, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "centimeter",
|
||||||
|
unitDisplay: "short",
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="penis-length-imperial">
|
||||||
|
{intl.formatNumber(inches, {
|
||||||
|
style: "unit",
|
||||||
|
unit: "inch",
|
||||||
|
unitDisplay: "narrow",
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IPerformerList {
|
interface IPerformerList {
|
||||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
persistState?: PersistanceLevel;
|
persistState?: PersistanceLevel;
|
||||||
|
|
@ -158,6 +283,8 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
||||||
return (
|
return (
|
||||||
<PerformerListTable
|
<PerformerListTable
|
||||||
performers={result.data.findPerformers.performers}
|
performers={result.data.findPerformers.performers}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectChange={onSelectChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,58 +2,68 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Button, Table } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import NavUtils from "src/utils/navigation";
|
import NavUtils from "src/utils/navigation";
|
||||||
import { faHeart } from "@fortawesome/free-solid-svg-icons";
|
import { faHeart } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { cmToImperial } from "src/utils/units";
|
import { usePerformerUpdate } from "src/core/StashService";
|
||||||
|
import { useTableColumns } from "src/hooks/useTableColumns";
|
||||||
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
|
import cx from "classnames";
|
||||||
|
import {
|
||||||
|
FormatCircumcised,
|
||||||
|
FormatHeight,
|
||||||
|
FormatPenisLength,
|
||||||
|
FormatWeight,
|
||||||
|
} from "./PerformerList";
|
||||||
|
import TextUtils from "src/utils/text";
|
||||||
|
import { getCountryByISO } from "src/utils/country";
|
||||||
|
import { IColumn, ListTable } from "../List/ListTable";
|
||||||
|
|
||||||
interface IPerformerListTableProps {
|
interface IPerformerListTableProps {
|
||||||
performers: GQL.PerformerDataFragment[];
|
performers: GQL.PerformerDataFragment[];
|
||||||
|
selectedIds: Set<string>;
|
||||||
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TABLE_NAME = "performers";
|
||||||
|
|
||||||
export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
||||||
props: IPerformerListTableProps
|
props: IPerformerListTableProps
|
||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const formatHeight = (height?: number | null) => {
|
const [updatePerformer] = usePerformerUpdate();
|
||||||
if (!height) {
|
|
||||||
return "";
|
function setRating(v: number | null, performerId: string) {
|
||||||
|
if (performerId) {
|
||||||
|
updatePerformer({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: performerId,
|
||||||
|
rating100: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [feet, inches] = cmToImperial(height);
|
function setFavorite(v: boolean, performerId: string) {
|
||||||
|
if (performerId) {
|
||||||
|
updatePerformer({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: performerId,
|
||||||
|
favorite: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const ImageCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
<span className="performer-height">
|
|
||||||
<span className="height-metric">
|
|
||||||
{intl.formatNumber(height, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "centimeter",
|
|
||||||
unitDisplay: "short",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="height-imperial">
|
|
||||||
{intl.formatNumber(feet, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "foot",
|
|
||||||
unitDisplay: "narrow",
|
|
||||||
})}
|
|
||||||
{intl.formatNumber(inches, {
|
|
||||||
style: "unit",
|
|
||||||
unit: "inch",
|
|
||||||
unitDisplay: "narrow",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPerformerRow = (performer: GQL.PerformerDataFragment) => (
|
|
||||||
<tr key={performer.id}>
|
|
||||||
<td>
|
|
||||||
<Link to={`/performers/${performer.id}`}>
|
<Link to={`/performers/${performer.id}`}>
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
@ -62,69 +72,329 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
||||||
src={performer.image_path ?? ""}
|
src={performer.image_path ?? ""}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
);
|
||||||
<td className="text-left">
|
|
||||||
|
const NameCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
<Link to={`/performers/${performer.id}`}>
|
<Link to={`/performers/${performer.id}`}>
|
||||||
<h5>
|
<div className="ellips-data" title={performer.name}>
|
||||||
{performer.name}
|
{performer.name}
|
||||||
{performer.disambiguation && (
|
{performer.disambiguation && (
|
||||||
<span className="performer-disambiguation">
|
<span className="performer-disambiguation">
|
||||||
{` (${performer.disambiguation})`}
|
{` (${performer.disambiguation})`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
|
||||||
<td>{performer.alias_list ? performer.alias_list.join(", ") : ""}</td>
|
|
||||||
<td>
|
|
||||||
{performer.favorite && (
|
|
||||||
<Button disabled className="favorite">
|
|
||||||
<Icon icon={faHeart} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
|
||||||
<h6>{performer.scene_count}</h6>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Link to={NavUtils.makePerformerImagesUrl(performer)}>
|
|
||||||
<h6>{performer.image_count}</h6>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Link to={NavUtils.makePerformerGalleriesUrl(performer)}>
|
|
||||||
<h6>{performer.gallery_count}</h6>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<h6>{performer.o_counter}</h6>
|
|
||||||
</td>
|
|
||||||
<td>{performer.birthdate}</td>
|
|
||||||
<td>{!!performer.height_cm && formatHeight(performer.height_cm)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const AliasesCell = (performer: GQL.PerformerDataFragment) => {
|
||||||
|
let aliases = performer.alias_list ? performer.alias_list.join(", ") : "";
|
||||||
return (
|
return (
|
||||||
<div className="row justify-content-center table-list">
|
<span className="ellips-data" title={aliases}>
|
||||||
<Table bordered striped>
|
{aliases}
|
||||||
<thead>
|
</span>
|
||||||
<tr>
|
);
|
||||||
<th />
|
};
|
||||||
<th>{intl.formatMessage({ id: "name" })}</th>
|
|
||||||
<th>{intl.formatMessage({ id: "aliases" })}</th>
|
const GenderCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
<th>{intl.formatMessage({ id: "favourite" })}</th>
|
<>
|
||||||
<th>{intl.formatMessage({ id: "scene_count" })}</th>
|
{performer.gender
|
||||||
<th>{intl.formatMessage({ id: "image_count" })}</th>
|
? intl.formatMessage({ id: "gender_types." + performer.gender })
|
||||||
<th>{intl.formatMessage({ id: "gallery_count" })}</th>
|
: ""}
|
||||||
<th>{intl.formatMessage({ id: "o_counter" })}</th>
|
</>
|
||||||
<th>{intl.formatMessage({ id: "birthdate" })}</th>
|
);
|
||||||
<th>{intl.formatMessage({ id: "height" })}</th>
|
|
||||||
</tr>
|
const RatingCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
</thead>
|
<RatingSystem
|
||||||
<tbody>{props.performers.map(renderPerformerRow)}</tbody>
|
value={performer.rating100}
|
||||||
</Table>
|
onSetRating={(value) => setRating(value, performer.id)}
|
||||||
</div>
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AgeCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<span
|
||||||
|
title={
|
||||||
|
performer.birthdate
|
||||||
|
? TextUtils.formatDate(intl, performer.birthdate ?? undefined)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{performer.birthdate
|
||||||
|
? TextUtils.age(performer.birthdate, performer.death_date)
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeathdateCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{performer.death_date}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FavoriteCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<Button
|
||||||
|
className={cx(
|
||||||
|
"minimal",
|
||||||
|
performer.favorite ? "favorite" : "not-favorite"
|
||||||
|
)}
|
||||||
|
onClick={() => setFavorite(!performer.favorite, performer.id)}
|
||||||
|
>
|
||||||
|
<Icon icon={faHeart} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CountryCell = (performer: GQL.PerformerDataFragment) => {
|
||||||
|
const { locale } = useIntl();
|
||||||
|
return (
|
||||||
|
<span className="ellips-data">
|
||||||
|
{getCountryByISO(performer.country, locale)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EthnicityCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{performer.ethnicity}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MeasurementsCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<span className="ellips-data">{performer.measurements}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FakeTitsCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{performer.fake_tits}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PenisLengthCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{FormatPenisLength(performer.penis_length)}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CircumcisedCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{FormatCircumcised(performer.circumcised)}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const HairColorCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<span className="ellips-data">{performer.hair_color}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EyeColorCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{performer.eye_color}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const HeightCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{FormatHeight(performer.height_cm)}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WeightCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{FormatWeight(performer.weight)}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CareerLengthCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<span className="ellips-data">{performer.career_length}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SceneCountCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||||
|
<span>{performer.scene_count}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GalleryCountCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<Link to={NavUtils.makePerformerGalleriesUrl(performer)}>
|
||||||
|
<span>{performer.gallery_count}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ImageCountCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<Link to={NavUtils.makePerformerImagesUrl(performer)}>
|
||||||
|
<span>{performer.image_count}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const OCounterCell = (performer: GQL.PerformerDataFragment) => (
|
||||||
|
<>{performer.o_counter}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IColumnSpec {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
defaultShow?: boolean;
|
||||||
|
mandatory?: boolean;
|
||||||
|
render?: (
|
||||||
|
scene: GQL.PerformerDataFragment,
|
||||||
|
index: number
|
||||||
|
) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allColumns: IColumnSpec[] = [
|
||||||
|
{
|
||||||
|
value: "image",
|
||||||
|
label: intl.formatMessage({ id: "image" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: ImageCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "name",
|
||||||
|
label: intl.formatMessage({ id: "name" }),
|
||||||
|
mandatory: true,
|
||||||
|
defaultShow: true,
|
||||||
|
render: NameCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "aliases",
|
||||||
|
label: intl.formatMessage({ id: "aliases" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: AliasesCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "gender",
|
||||||
|
label: intl.formatMessage({ id: "gender" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: GenderCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "rating",
|
||||||
|
label: intl.formatMessage({ id: "rating" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: RatingCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "age",
|
||||||
|
label: intl.formatMessage({ id: "age" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: AgeCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "death_date",
|
||||||
|
label: intl.formatMessage({ id: "death_date" }),
|
||||||
|
render: DeathdateCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "favourite",
|
||||||
|
label: intl.formatMessage({ id: "favourite" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: FavoriteCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "country",
|
||||||
|
label: intl.formatMessage({ id: "country" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: CountryCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ethnicity",
|
||||||
|
label: intl.formatMessage({ id: "ethnicity" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: EthnicityCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "hair_color",
|
||||||
|
label: intl.formatMessage({ id: "hair_color" }),
|
||||||
|
render: HairColorCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "eye_color",
|
||||||
|
label: intl.formatMessage({ id: "eye_color" }),
|
||||||
|
render: EyeColorCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "height_cm",
|
||||||
|
label: intl.formatMessage({ id: "height_cm" }),
|
||||||
|
render: HeightCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "weight_kg",
|
||||||
|
label: intl.formatMessage({ id: "weight_kg" }),
|
||||||
|
render: WeightCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "penis_length_cm",
|
||||||
|
label: intl.formatMessage({ id: "penis_length_cm" }),
|
||||||
|
render: PenisLengthCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "circumcised",
|
||||||
|
label: intl.formatMessage({ id: "circumcised" }),
|
||||||
|
render: CircumcisedCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "measurements",
|
||||||
|
label: intl.formatMessage({ id: "measurements" }),
|
||||||
|
render: MeasurementsCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "fake_tits",
|
||||||
|
label: intl.formatMessage({ id: "fake_tits" }),
|
||||||
|
render: FakeTitsCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "career_length",
|
||||||
|
label: intl.formatMessage({ id: "career_length" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: CareerLengthCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "scene_count",
|
||||||
|
label: intl.formatMessage({ id: "scene_count" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: SceneCountCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "gallery_count",
|
||||||
|
label: intl.formatMessage({ id: "gallery_count" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: GalleryCountCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "image_count",
|
||||||
|
label: intl.formatMessage({ id: "image_count" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: ImageCountCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "o_counter",
|
||||||
|
label: intl.formatMessage({ id: "o_counter" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: OCounterCell,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultColumns = allColumns
|
||||||
|
.filter((col) => col.defaultShow)
|
||||||
|
.map((col) => col.value);
|
||||||
|
|
||||||
|
const { selectedColumns, saveColumns } = useTableColumns(
|
||||||
|
TABLE_NAME,
|
||||||
|
defaultColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnRenderFuncs: Record<
|
||||||
|
string,
|
||||||
|
(scene: GQL.PerformerDataFragment, index: number) => React.ReactNode
|
||||||
|
> = {};
|
||||||
|
allColumns.forEach((col) => {
|
||||||
|
if (col.render) {
|
||||||
|
columnRenderFuncs[col.value] = col.render;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderCell(
|
||||||
|
column: IColumn,
|
||||||
|
performer: GQL.PerformerDataFragment,
|
||||||
|
index: number
|
||||||
|
) {
|
||||||
|
const render = columnRenderFuncs[column.value];
|
||||||
|
|
||||||
|
if (render) return render(performer, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListTable
|
||||||
|
className="performer-table"
|
||||||
|
items={props.performers}
|
||||||
|
allColumns={allColumns}
|
||||||
|
columns={selectedColumns}
|
||||||
|
setColumns={(c) => saveColumns(c)}
|
||||||
|
selectedIds={props.selectedIds}
|
||||||
|
onSelectChange={props.onSelectChange}
|
||||||
|
renderCell={renderCell}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -206,11 +206,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favourite-data .favorite {
|
||||||
|
color: #ff7373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performer-table .height-imperial,
|
||||||
|
.performer-table .weight-imperial,
|
||||||
|
.performer-table .penis-length-imperial,
|
||||||
.performer-disambiguation {
|
.performer-disambiguation {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.performer-table .age-data span {
|
||||||
|
border-bottom: 1px dotted #f5f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
.performer-result .performer-details > span {
|
.performer-result .performer-details > span {
|
||||||
&::after {
|
&::after {
|
||||||
content: " • ";
|
content: " • ";
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Table, Form } from "react-bootstrap";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import NavUtils from "src/utils/navigation";
|
import NavUtils from "src/utils/navigation";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import SceneQueue from "src/models/sceneQueue";
|
import SceneQueue from "src/models/sceneQueue";
|
||||||
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
|
import { useSceneUpdate } from "src/core/StashService";
|
||||||
|
import { IColumn, ListTable } from "../List/ListTable";
|
||||||
|
import { useTableColumns } from "src/hooks/useTableColumns";
|
||||||
|
|
||||||
interface ISceneListTableProps {
|
interface ISceneListTableProps {
|
||||||
scenes: GQL.SlimSceneDataFragment[];
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
|
|
@ -16,74 +19,35 @@ interface ISceneListTableProps {
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TABLE_NAME = "scenes";
|
||||||
|
|
||||||
export const SceneListTable: React.FC<ISceneListTableProps> = (
|
export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||||
props: ISceneListTableProps
|
props: ISceneListTableProps
|
||||||
) => {
|
) => {
|
||||||
const renderTags = (tags: Partial<GQL.TagDataFragment>[]) =>
|
const intl = useIntl();
|
||||||
tags.map((tag) => (
|
|
||||||
<Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}>
|
|
||||||
<h6>{tag.name}</h6>
|
|
||||||
</Link>
|
|
||||||
));
|
|
||||||
|
|
||||||
const renderPerformers = (performers: Partial<GQL.PerformerDataFragment>[]) =>
|
const [updateScene] = useSceneUpdate();
|
||||||
performers.map((performer) => (
|
|
||||||
<Link key={performer.id} to={NavUtils.makePerformerScenesUrl(performer)}>
|
|
||||||
<h6>{performer.name}</h6>
|
|
||||||
</Link>
|
|
||||||
));
|
|
||||||
|
|
||||||
const renderMovies = (scene: GQL.SlimSceneDataFragment) =>
|
function setRating(v: number | null, sceneId: string) {
|
||||||
scene.movies.map((sceneMovie) => (
|
if (sceneId) {
|
||||||
<Link
|
updateScene({
|
||||||
key={sceneMovie.movie.id}
|
variables: {
|
||||||
to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}
|
input: {
|
||||||
>
|
id: sceneId,
|
||||||
<h6>{sceneMovie.movie.name}</h6>
|
rating100: v,
|
||||||
</Link>
|
},
|
||||||
));
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const renderGalleries = (scene: GQL.SlimSceneDataFragment) =>
|
const CoverImageCell = (scene: GQL.SlimSceneDataFragment, index: number) => {
|
||||||
scene.galleries.map((gallery) => (
|
const title = objectTitle(scene);
|
||||||
<Link key={gallery.id} to={`/galleries/${gallery.id}`}>
|
|
||||||
<h6>{galleryTitle(gallery)}</h6>
|
|
||||||
</Link>
|
|
||||||
));
|
|
||||||
|
|
||||||
const renderSceneRow = (scene: GQL.SlimSceneDataFragment, index: number) => {
|
|
||||||
const sceneLink = props.queue
|
const sceneLink = props.queue
|
||||||
? props.queue.makeLink(scene.id, { sceneIndex: index })
|
? props.queue.makeLink(scene.id, { sceneIndex: index })
|
||||||
: `/scenes/${scene.id}`;
|
: `/scenes/${scene.id}`;
|
||||||
|
|
||||||
let shiftKey = false;
|
|
||||||
|
|
||||||
const file = scene.files.length > 0 ? scene.files[0] : undefined;
|
|
||||||
|
|
||||||
const title = objectTitle(scene);
|
|
||||||
return (
|
return (
|
||||||
<tr key={scene.id}>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<Form.Control
|
|
||||||
type="checkbox"
|
|
||||||
checked={props.selectedIds.has(scene.id)}
|
|
||||||
onChange={() =>
|
|
||||||
props.onSelectChange(
|
|
||||||
scene.id,
|
|
||||||
!props.selectedIds.has(scene.id),
|
|
||||||
shiftKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={(
|
|
||||||
event: React.MouseEvent<HTMLInputElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
shiftKey = event.shiftKey;
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Link to={sceneLink}>
|
<Link to={sceneLink}>
|
||||||
<img
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
@ -92,64 +56,331 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||||
src={scene.paths.screenshot ?? ""}
|
src={scene.paths.screenshot ?? ""}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
|
||||||
<td className="text-left">
|
|
||||||
<Link to={sceneLink}>
|
|
||||||
<h5>{title}</h5>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td>{scene.rating100 ? scene.rating100 : ""}</td>
|
|
||||||
<td>{file?.duration && TextUtils.secondsToTimestamp(file.duration)}</td>
|
|
||||||
<td>{renderTags(scene.tags)}</td>
|
|
||||||
<td>{renderPerformers(scene.performers)}</td>
|
|
||||||
<td>
|
|
||||||
{scene.studio && (
|
|
||||||
<Link to={NavUtils.makeStudioScenesUrl(scene.studio)}>
|
|
||||||
<h6>{scene.studio.name}</h6>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>{renderMovies(scene)}</td>
|
|
||||||
<td>{renderGalleries(scene)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TitleCell = (scene: GQL.SlimSceneDataFragment, index: number) => {
|
||||||
|
const title = objectTitle(scene);
|
||||||
|
const sceneLink = props.queue
|
||||||
|
? props.queue.makeLink(scene.id, { sceneIndex: index })
|
||||||
|
: `/scenes/${scene.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row scene-table table-list justify-content-center">
|
<Link to={sceneLink} title={title}>
|
||||||
<Table striped bordered>
|
<span className="ellips-data">{title}</span>
|
||||||
<thead>
|
</Link>
|
||||||
<tr>
|
);
|
||||||
<th />
|
};
|
||||||
<th />
|
|
||||||
<th className="text-left">
|
const DateCell = (scene: GQL.SlimSceneDataFragment) => <>{scene.date}</>;
|
||||||
<FormattedMessage id="title" />
|
|
||||||
</th>
|
const RatingCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
<th>
|
<RatingSystem
|
||||||
<FormattedMessage id="rating" />
|
value={scene.rating100}
|
||||||
</th>
|
onSetRating={(value) => setRating(value, scene.id)}
|
||||||
<th>
|
/>
|
||||||
<FormattedMessage id="duration" />
|
);
|
||||||
</th>
|
|
||||||
<th>
|
const DurationCell = (scene: GQL.SlimSceneDataFragment) => {
|
||||||
<FormattedMessage id="tags" />
|
const file = scene.files.length > 0 ? scene.files[0] : undefined;
|
||||||
</th>
|
return file?.duration && TextUtils.secondsToTimestamp(file.duration);
|
||||||
<th>
|
};
|
||||||
<FormattedMessage id="performers" />
|
|
||||||
</th>
|
const TagCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
<th>
|
<ul className="comma-list">
|
||||||
<FormattedMessage id="studio" />
|
{scene.tags.map((tag) => (
|
||||||
</th>
|
<li key={tag.id}>
|
||||||
<th>
|
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
||||||
<FormattedMessage id="movies" />
|
<span>{tag.name}</span>
|
||||||
</th>
|
</Link>
|
||||||
<th>
|
</li>
|
||||||
<FormattedMessage id="galleries" />
|
))}
|
||||||
</th>
|
</ul>
|
||||||
</tr>
|
);
|
||||||
</thead>
|
|
||||||
<tbody>{props.scenes.map(renderSceneRow)}</tbody>
|
const PerformersCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
</Table>
|
<ul className="comma-list">
|
||||||
</div>
|
{scene.performers.map((performer) => (
|
||||||
|
<li key={performer.id}>
|
||||||
|
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||||
|
<span>{performer.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StudioCell = (scene: GQL.SlimSceneDataFragment) => {
|
||||||
|
if (scene.studio) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={NavUtils.makeStudioScenesUrl(scene.studio)}
|
||||||
|
title={scene.studio.name}
|
||||||
|
>
|
||||||
|
<span className="ellips-data">{scene.studio.name}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MovieCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.movies.map((sceneMovie) => (
|
||||||
|
<li key={sceneMovie.movie.id}>
|
||||||
|
<Link to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}>
|
||||||
|
<span className="ellips-data">{sceneMovie.movie.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GalleriesCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.galleries.map((gallery) => (
|
||||||
|
<li key={gallery.id}>
|
||||||
|
<Link to={`/galleries/${gallery.id}`}>
|
||||||
|
<span>{galleryTitle(gallery)}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlayCountCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id="plays"
|
||||||
|
values={{ value: intl.formatNumber(scene.play_count ?? 0) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlayDurationCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<>{TextUtils.secondsToTimestamp(scene.play_duration ?? 0)}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ResolutionCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.files.map((file) => (
|
||||||
|
<li key={file.id}>
|
||||||
|
<span> {TextUtils.resolution(file?.width, file?.height)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FrameRateCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.files.map((file) => (
|
||||||
|
<li key={file.id}>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="frames_per_second"
|
||||||
|
values={{ value: intl.formatNumber(file.frame_rate ?? 0) }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BitRateCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.files.map((file) => (
|
||||||
|
<li key={file.id}>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="megabits_per_second"
|
||||||
|
values={{
|
||||||
|
value: intl.formatNumber((file.bit_rate ?? 0) / 1000000, {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AudioCodecCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.files.map((file) => (
|
||||||
|
<li key={file.id}>
|
||||||
|
<span>{file.audio_codec}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
const VideoCodecCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
|
<ul className="comma-list">
|
||||||
|
{scene.files.map((file) => (
|
||||||
|
<li key={file.id}>
|
||||||
|
<span>{file.video_codec}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IColumnSpec {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
defaultShow?: boolean;
|
||||||
|
mandatory?: boolean;
|
||||||
|
render?: (
|
||||||
|
scene: GQL.SlimSceneDataFragment,
|
||||||
|
index: number
|
||||||
|
) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allColumns: IColumnSpec[] = [
|
||||||
|
{
|
||||||
|
value: "cover_image",
|
||||||
|
label: intl.formatMessage({ id: "cover_image" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: CoverImageCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "title",
|
||||||
|
label: intl.formatMessage({ id: "title" }),
|
||||||
|
defaultShow: true,
|
||||||
|
mandatory: true,
|
||||||
|
render: TitleCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "date",
|
||||||
|
label: intl.formatMessage({ id: "date" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: DateCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "rating",
|
||||||
|
label: intl.formatMessage({ id: "rating" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: RatingCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "scene_code",
|
||||||
|
label: intl.formatMessage({ id: "scene_code" }),
|
||||||
|
render: (s) => <>{s.code}</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "duration",
|
||||||
|
label: intl.formatMessage({ id: "duration" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: DurationCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "tags",
|
||||||
|
label: intl.formatMessage({ id: "tags" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: TagCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "performers",
|
||||||
|
label: intl.formatMessage({ id: "performers" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: PerformersCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "studio",
|
||||||
|
label: intl.formatMessage({ id: "studio" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: StudioCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "movies",
|
||||||
|
label: intl.formatMessage({ id: "movies" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: MovieCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "galleries",
|
||||||
|
label: intl.formatMessage({ id: "galleries" }),
|
||||||
|
defaultShow: true,
|
||||||
|
render: GalleriesCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "play_count",
|
||||||
|
label: intl.formatMessage({ id: "play_count" }),
|
||||||
|
render: PlayCountCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "play_duration",
|
||||||
|
label: intl.formatMessage({ id: "play_duration" }),
|
||||||
|
render: PlayDurationCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "o_counter",
|
||||||
|
label: intl.formatMessage({ id: "o_counter" }),
|
||||||
|
render: (s) => <>{s.o_counter}</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "resolution",
|
||||||
|
label: intl.formatMessage({ id: "resolution" }),
|
||||||
|
render: ResolutionCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "framerate",
|
||||||
|
label: intl.formatMessage({ id: "framerate" }),
|
||||||
|
render: FrameRateCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "bitrate",
|
||||||
|
label: intl.formatMessage({ id: "bitrate" }),
|
||||||
|
render: BitRateCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "video_codec",
|
||||||
|
label: intl.formatMessage({ id: "video_codec" }),
|
||||||
|
render: VideoCodecCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "audio_codec",
|
||||||
|
label: intl.formatMessage({ id: "audio_codec" }),
|
||||||
|
render: AudioCodecCell,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultColumns = allColumns
|
||||||
|
.filter((col) => col.defaultShow)
|
||||||
|
.map((col) => col.value);
|
||||||
|
|
||||||
|
const { selectedColumns, saveColumns } = useTableColumns(
|
||||||
|
TABLE_NAME,
|
||||||
|
defaultColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnRenderFuncs: Record<
|
||||||
|
string,
|
||||||
|
(scene: GQL.SlimSceneDataFragment, index: number) => React.ReactNode
|
||||||
|
> = {};
|
||||||
|
allColumns.forEach((col) => {
|
||||||
|
if (col.render) {
|
||||||
|
columnRenderFuncs[col.value] = col.render;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderCell(
|
||||||
|
column: IColumn,
|
||||||
|
scene: GQL.SlimSceneDataFragment,
|
||||||
|
index: number
|
||||||
|
) {
|
||||||
|
const render = columnRenderFuncs[column.value];
|
||||||
|
|
||||||
|
if (render) return render(scene, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListTable
|
||||||
|
className="scene-table"
|
||||||
|
items={props.scenes}
|
||||||
|
allColumns={allColumns}
|
||||||
|
columns={selectedColumns}
|
||||||
|
setColumns={(c) => saveColumns(c)}
|
||||||
|
selectedIds={props.selectedIds}
|
||||||
|
onSelectChange={props.onSelectChange}
|
||||||
|
renderCell={renderCell}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,12 @@ textarea.scene-description {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable selector-class-pattern */
|
||||||
|
.table .cover_image-head,
|
||||||
|
.table .cover_image-data {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="range"].filter-slider {
|
input[type="range"].filter-slider {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -684,25 +690,20 @@ input[type="range"].blue-slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-table {
|
.scene-table {
|
||||||
table,
|
|
||||||
tr,
|
|
||||||
td,
|
|
||||||
label,
|
|
||||||
input {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:first-child {
|
td:first-child {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-col {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.scrape-dialog .rating-number.disabled {
|
.scrape-dialog .rating-number.disabled {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import Select, {
|
||||||
MenuListProps,
|
MenuListProps,
|
||||||
GroupBase,
|
GroupBase,
|
||||||
OptionsOrGroups,
|
OptionsOrGroups,
|
||||||
|
DropdownIndicatorProps,
|
||||||
} from "react-select";
|
} from "react-select";
|
||||||
import CreatableSelect from "react-select/creatable";
|
import CreatableSelect from "react-select/creatable";
|
||||||
|
|
||||||
|
|
@ -32,6 +33,8 @@ import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
|
||||||
import { useDebounce } from "src/hooks/debounce";
|
import { useDebounce } from "src/hooks/debounce";
|
||||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||||
import { PerformerIDSelect } from "../Performers/PerformerSelect";
|
import { PerformerIDSelect } from "../Performers/PerformerSelect";
|
||||||
|
import { Icon } from "./Icon";
|
||||||
|
import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export type SelectObject = {
|
export type SelectObject = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -979,3 +982,91 @@ export const ListSelect = <T extends {}>(props: IListSelect<T>) => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DisableOption = Option & {
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ICheckBoxSelectProps {
|
||||||
|
options: DisableOption[];
|
||||||
|
selectedOptions?: DisableOption[];
|
||||||
|
onChange: (item: OnChangeValue<DisableOption, true>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckBoxSelect: React.FC<ICheckBoxSelectProps> = ({
|
||||||
|
options,
|
||||||
|
selectedOptions,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const Option = (props: OptionProps<DisableOption, true>) => (
|
||||||
|
<reactSelectComponents.Option {...props}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
disabled={props.data.disabled}
|
||||||
|
checked={props.isSelected}
|
||||||
|
onChange={() => null}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
<label>{props.label}</label>
|
||||||
|
</reactSelectComponents.Option>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DropdownIndicator = (
|
||||||
|
props: DropdownIndicatorProps<DisableOption, true>
|
||||||
|
) => (
|
||||||
|
<reactSelectComponents.DropdownIndicator {...props}>
|
||||||
|
<Icon icon={faTableColumns} className="column-select" />
|
||||||
|
</reactSelectComponents.DropdownIndicator>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="CheckBoxSelect"
|
||||||
|
options={options}
|
||||||
|
value={selectedOptions}
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
hideSelectedOptions={false}
|
||||||
|
isSearchable={false}
|
||||||
|
isClearable={false}
|
||||||
|
components={{
|
||||||
|
DropdownIndicator,
|
||||||
|
Option,
|
||||||
|
ValueContainer: () => null,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
|
}}
|
||||||
|
onChange={onChange}
|
||||||
|
styles={{
|
||||||
|
control: (base) => ({
|
||||||
|
...base,
|
||||||
|
height: "25px",
|
||||||
|
width: "25px",
|
||||||
|
backgroundColor: "none",
|
||||||
|
border: "none",
|
||||||
|
transition: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
}),
|
||||||
|
dropdownIndicator: (base) => ({
|
||||||
|
...base,
|
||||||
|
color: "rgb(255, 255, 255)",
|
||||||
|
padding: "0",
|
||||||
|
}),
|
||||||
|
menu: (base) => ({
|
||||||
|
...base,
|
||||||
|
backgroundColor: "rgb(57, 75, 89)",
|
||||||
|
}),
|
||||||
|
option: (base, fprops) => ({
|
||||||
|
...base,
|
||||||
|
backgroundColor: fprops.isFocused
|
||||||
|
? "rgb(37, 49, 58)"
|
||||||
|
: "rgb(57, 75, 89)",
|
||||||
|
padding: "0px 12px",
|
||||||
|
}),
|
||||||
|
menuList: (base) => ({
|
||||||
|
...base,
|
||||||
|
position: "fixed",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
37
ui/v2.5/src/hooks/useTableColumns.ts
Normal file
37
ui/v2.5/src/hooks/useTableColumns.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { useConfigureUI } from "src/core/StashService";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { useToast } from "./Toast";
|
||||||
|
|
||||||
|
export const useTableColumns = (
|
||||||
|
tableName: string,
|
||||||
|
defaultColumns: string[]
|
||||||
|
) => {
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const { configuration } = useContext(ConfigurationContext);
|
||||||
|
const [saveUI] = useConfigureUI();
|
||||||
|
|
||||||
|
const selectedColumns: string[] =
|
||||||
|
configuration?.ui?.tableColumns?.[tableName] ?? defaultColumns;
|
||||||
|
|
||||||
|
async function saveColumns(updatedColumns: readonly string[]) {
|
||||||
|
try {
|
||||||
|
await saveUI({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
...configuration?.ui,
|
||||||
|
tableColumns: {
|
||||||
|
...configuration?.ui?.tableColumns,
|
||||||
|
[tableName]: updatedColumns,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { selectedColumns, saveColumns };
|
||||||
|
};
|
||||||
|
|
@ -462,35 +462,6 @@ textarea.text-input {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-list {
|
|
||||||
min-width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-list a {
|
|
||||||
color: $text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-list table {
|
|
||||||
width: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-list td,
|
|
||||||
.table-list th {
|
|
||||||
border-left: 1px solid #414c53;
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stylelint-disable declaration-no-important */
|
/* stylelint-disable declaration-no-important */
|
||||||
.border-row {
|
.border-row {
|
||||||
background-color: #414c53;
|
background-color: #414c53;
|
||||||
|
|
|
||||||
|
|
@ -997,7 +997,7 @@
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"framerate": "Frame Rate",
|
"framerate": "Frame Rate",
|
||||||
"frames_per_second": "{value} frames per second",
|
"frames_per_second": "{value} fps",
|
||||||
"front_page": {
|
"front_page": {
|
||||||
"types": {
|
"types": {
|
||||||
"premade_filter": "Premade Filter",
|
"premade_filter": "Premade Filter",
|
||||||
|
|
@ -1068,7 +1068,7 @@
|
||||||
"stream": "Stream",
|
"stream": "Stream",
|
||||||
"video_codec": "Video Codec"
|
"video_codec": "Video Codec"
|
||||||
},
|
},
|
||||||
"megabits_per_second": "{value} megabits per second",
|
"megabits_per_second": "{value} mbps",
|
||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"movie": "Movie",
|
"movie": "Movie",
|
||||||
"movie_scene_number": "Movie Scene Number",
|
"movie_scene_number": "Movie Scene Number",
|
||||||
|
|
@ -1171,6 +1171,7 @@
|
||||||
"performers": "Performers",
|
"performers": "Performers",
|
||||||
"photographer": "Photographer",
|
"photographer": "Photographer",
|
||||||
"piercings": "Piercings",
|
"piercings": "Piercings",
|
||||||
|
"plays": "{value} plays",
|
||||||
"play_count": "Play Count",
|
"play_count": "Play Count",
|
||||||
"play_duration": "Play Duration",
|
"play_duration": "Play Duration",
|
||||||
"primary_file": "Primary file",
|
"primary_file": "Primary file",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
||||||
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
|
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
|
||||||
import { ILabeledId } from "src/models/list-filter/types";
|
import { ILabeledId } from "src/models/list-filter/types";
|
||||||
import { IntlShape } from "react-intl";
|
import { IntlShape } from "react-intl";
|
||||||
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
|
||||||
function addExtraCriteria(
|
function addExtraCriteria(
|
||||||
dest: Criterion<CriterionValue>[],
|
dest: Criterion<CriterionValue>[],
|
||||||
|
|
@ -348,9 +349,7 @@ const makeGalleryImagesUrl = (
|
||||||
if (!gallery.id) return "#";
|
if (!gallery.id) return "#";
|
||||||
const filter = new ListFilterModel(GQL.FilterMode.Images, undefined);
|
const filter = new ListFilterModel(GQL.FilterMode.Images, undefined);
|
||||||
const criterion = new GalleriesCriterion();
|
const criterion = new GalleriesCriterion();
|
||||||
criterion.value = [
|
criterion.value = [{ id: gallery.id, label: galleryTitle(gallery) }];
|
||||||
{ id: gallery.id, label: gallery.title || `Gallery ${gallery.id}` },
|
|
||||||
];
|
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
addExtraCriteria(filter.criteria, extraCriteria);
|
addExtraCriteria(filter.criteria, extraCriteria);
|
||||||
return `/images?${filter.makeQueryParameters()}`;
|
return `/images?${filter.makeQueryParameters()}`;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue