mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 00:43:12 +01:00
Support deleting multiple scenes (#630)
* Improve layout and add buttons * Move functionality into ListFilter * Make modal style dark * Convert scene options into edit scenes dialog * Add delete scenes dialog * Clear selected ids on delete * Refetch after update/delete * Use DeleteScenesDialog in Scene page * Show scene check boxes in small screens * Change default multi-set mode to set
This commit is contained in:
parent
83f8bc0832
commit
455e16ece9
20 changed files with 613 additions and 366 deletions
|
|
@ -80,6 +80,10 @@ mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boole
|
||||||
sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
|
sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation ScenesDestroy($ids: [ID!]!, $delete_file: Boolean, $delete_generated : Boolean) {
|
||||||
|
scenesDestroy(input: {ids: $ids, delete_file: $delete_file, delete_generated: $delete_generated})
|
||||||
|
}
|
||||||
|
|
||||||
mutation SceneGenerateScreenshot($id: ID!, $at: Float) {
|
mutation SceneGenerateScreenshot($id: ID!, $at: Float) {
|
||||||
sceneGenerateScreenshot(id: $id, at: $at)
|
sceneGenerateScreenshot(id: $id, at: $at)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ type Mutation {
|
||||||
sceneUpdate(input: SceneUpdateInput!): Scene
|
sceneUpdate(input: SceneUpdateInput!): Scene
|
||||||
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
|
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
|
||||||
sceneDestroy(input: SceneDestroyInput!): Boolean!
|
sceneDestroy(input: SceneDestroyInput!): Boolean!
|
||||||
|
scenesDestroy(input: ScenesDestroyInput!): Boolean!
|
||||||
scenesUpdate(input: [SceneUpdateInput!]!): [Scene]
|
scenesUpdate(input: [SceneUpdateInput!]!): [Scene]
|
||||||
|
|
||||||
"""Increments the o-counter for a scene. Returns the new value"""
|
"""Increments the o-counter for a scene. Returns the new value"""
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,12 @@ input SceneDestroyInput {
|
||||||
delete_generated: Boolean
|
delete_generated: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ScenesDestroyInput {
|
||||||
|
ids: [ID!]!
|
||||||
|
delete_file: Boolean
|
||||||
|
delete_generated: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
type FindScenesResultType {
|
type FindScenesResultType {
|
||||||
count: Int!
|
count: Int!
|
||||||
scenes: [Scene!]!
|
scenes: [Scene!]!
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,47 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.ScenesDestroyInput) (bool, error) {
|
||||||
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
|
||||||
|
var scenes []*models.Scene
|
||||||
|
for _, id := range input.Ids {
|
||||||
|
sceneID, _ := strconv.Atoi(id)
|
||||||
|
|
||||||
|
scene, err := qb.Find(sceneID)
|
||||||
|
if scene != nil {
|
||||||
|
scenes = append(scenes, scene)
|
||||||
|
}
|
||||||
|
err = manager.DestroyScene(sceneID, tx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scene := range scenes {
|
||||||
|
// if delete generated is true, then delete the generated files
|
||||||
|
// for the scene
|
||||||
|
if input.DeleteGenerated != nil && *input.DeleteGenerated {
|
||||||
|
manager.DeleteGeneratedSceneFiles(scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if delete file is true, then delete the file as well
|
||||||
|
// if it fails, just log a message
|
||||||
|
if input.DeleteFile != nil && *input.DeleteFile {
|
||||||
|
manager.DeleteSceneFile(scene)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) {
|
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) {
|
||||||
primaryTagID, _ := strconv.Atoi(input.PrimaryTagID)
|
primaryTagID, _ := strconv.Atoi(input.PrimaryTagID)
|
||||||
sceneID, _ := strconv.Atoi(input.SceneID)
|
sceneID, _ := strconv.Atoi(input.SceneID)
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
const markup = `
|
const markup = `
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Support deleting multiple scenes.
|
||||||
* Add in-app help manual.
|
* Add in-app help manual.
|
||||||
* Add support for custom served folders.
|
* Add support for custom served folders.
|
||||||
* Add support for parent/child studios.
|
* Add support for parent/child studios.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Added multi-scene edit dialog.
|
||||||
* Moved images to separate tables, increasing performance.
|
* Moved images to separate tables, increasing performance.
|
||||||
* Add gallery grid view.
|
* Add gallery grid view.
|
||||||
* Add is-missing scene filter for gallery query.
|
* Add is-missing scene filter for gallery query.
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||||
as="select"
|
as="select"
|
||||||
onChange={onChangedModifierSelect}
|
onChange={onChangedModifierSelect}
|
||||||
value={criterion.modifier}
|
value={criterion.modifier}
|
||||||
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
{criterion.modifierOptions.map((c) => (
|
{criterion.modifierOptions.map((c) => (
|
||||||
<option key={c.value} value={c.value}>
|
<option key={c.value} value={c.value}>
|
||||||
|
|
@ -170,6 +171,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||||
as="select"
|
as="select"
|
||||||
onChange={onChangedSingleSelect}
|
onChange={onChangedSingleSelect}
|
||||||
value={criterion.value.toString()}
|
value={criterion.value.toString()}
|
||||||
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
{criterion.options.map((c) => (
|
{criterion.options.map((c) => (
|
||||||
<option key={c.toString()} value={c.toString()}>
|
<option key={c.toString()} value={c.toString()}>
|
||||||
|
|
@ -190,6 +192,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
type={criterion.inputType}
|
type={criterion.inputType}
|
||||||
onChange={onChangedInput}
|
onChange={onChangedInput}
|
||||||
onBlur={onBlurInput}
|
onBlur={onBlurInput}
|
||||||
|
|
@ -216,6 +219,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||||
as="select"
|
as="select"
|
||||||
onChange={onChangedCriteriaType}
|
onChange={onChangedCriteriaType}
|
||||||
value={criterion.type}
|
value={criterion.type}
|
||||||
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
{props.filter.criterionOptions.map((c) => (
|
{props.filter.criterionOptions.map((c) => (
|
||||||
<option key={c.value} value={c.value}>
|
<option key={c.value} value={c.value}>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { debounce } from "lodash";
|
import _, { debounce } from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { SortDirectionEnum } from "src/core/generated-graphql";
|
import { SortDirectionEnum } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
|
|
@ -10,6 +10,10 @@ import {
|
||||||
OverlayTrigger,
|
OverlayTrigger,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
SafeAnchorProps,
|
SafeAnchorProps,
|
||||||
|
InputGroup,
|
||||||
|
FormControl,
|
||||||
|
Col,
|
||||||
|
Row,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
|
|
@ -24,20 +28,16 @@ interface IListFilterOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IListFilterProps {
|
interface IListFilterProps {
|
||||||
onChangePageSize: (pageSize: number) => void;
|
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||||
onChangeQuery: (query: string) => void;
|
|
||||||
onChangeSortDirection: (sortDirection: SortDirectionEnum) => void;
|
|
||||||
onChangeSortBy: (sortBy: string) => void;
|
|
||||||
onSortReshuffle: () => void;
|
|
||||||
onChangeDisplayMode: (displayMode: DisplayMode) => void;
|
|
||||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
|
||||||
onRemoveCriterion: (criterion: Criterion) => void;
|
|
||||||
zoomIndex?: number;
|
zoomIndex?: number;
|
||||||
onChangeZoom?: (zoomIndex: number) => void;
|
onChangeZoom?: (zoomIndex: number) => void;
|
||||||
onSelectAll?: () => void;
|
onSelectAll?: () => void;
|
||||||
onSelectNone?: () => void;
|
onSelectNone?: () => void;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
otherOperations?: IListFilterOperation[];
|
otherOperations?: IListFilterOperation[];
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
|
itemsSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||||
|
|
@ -46,7 +46,10 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
props: IListFilterProps
|
props: IListFilterProps
|
||||||
) => {
|
) => {
|
||||||
const searchCallback = debounce((value: string) => {
|
const searchCallback = debounce((value: string) => {
|
||||||
props.onChangeQuery(value);
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
newFilter.searchTerm = value;
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const [editingCriterion, setEditingCriterion] = useState<
|
const [editingCriterion, setEditingCriterion] = useState<
|
||||||
|
|
@ -55,7 +58,11 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
|
|
||||||
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const val = event.currentTarget.value;
|
const val = event.currentTarget.value;
|
||||||
props.onChangePageSize(parseInt(val, 10));
|
|
||||||
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
newFilter.itemsPerPage = parseInt(val, 10);
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
|
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
|
||||||
|
|
@ -63,34 +70,75 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSortDirection() {
|
function onChangeSortDirection() {
|
||||||
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
if (props.filter.sortDirection === SortDirectionEnum.Asc) {
|
if (props.filter.sortDirection === SortDirectionEnum.Asc) {
|
||||||
props.onChangeSortDirection(SortDirectionEnum.Desc);
|
newFilter.sortDirection = SortDirectionEnum.Desc;
|
||||||
} else {
|
} else {
|
||||||
props.onChangeSortDirection(SortDirectionEnum.Asc);
|
newFilter.sortDirection = SortDirectionEnum.Asc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSortBy(event: React.MouseEvent<SafeAnchorProps>) {
|
function onChangeSortBy(event: React.MouseEvent<SafeAnchorProps>) {
|
||||||
const target = event.currentTarget as HTMLAnchorElement;
|
const target = event.currentTarget as HTMLAnchorElement;
|
||||||
props.onChangeSortBy(target.text);
|
|
||||||
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
newFilter.sortBy = target.text;
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReshuffleRandomSort() {
|
function onReshuffleRandomSort() {
|
||||||
props.onSortReshuffle();
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
newFilter.randomSeed = -1;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeDisplayMode(displayMode: DisplayMode) {
|
function onChangeDisplayMode(displayMode: DisplayMode) {
|
||||||
props.onChangeDisplayMode(displayMode);
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
newFilter.displayMode = displayMode;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAddCriterion(criterion: Criterion, oldId?: string) {
|
function onAddCriterion(criterion: Criterion, oldId?: string) {
|
||||||
props.onAddCriterion(criterion, oldId);
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
|
||||||
|
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
||||||
|
const existingIndex = newFilter.criteria.findIndex((c) => {
|
||||||
|
// If we modified an existing criterion, then look for the old id.
|
||||||
|
const id = oldId || criterion.getId();
|
||||||
|
return c.getId() === id;
|
||||||
|
});
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
newFilter.criteria.push(criterion);
|
||||||
|
} else {
|
||||||
|
newFilter.criteria[existingIndex] = criterion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate modifiers
|
||||||
|
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
||||||
|
return arr.map((mapObj) => mapObj.getId()).indexOf(obj.getId()) === pos;
|
||||||
|
});
|
||||||
|
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCancelAddCriterion() {
|
function onCancelAddCriterion() {
|
||||||
setEditingCriterion(undefined);
|
setEditingCriterion(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRemoveCriterion(removedCriterion: Criterion) {
|
||||||
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
|
newFilter.criteria = newFilter.criteria.filter(
|
||||||
|
(criterion) => criterion.getId() !== removedCriterion.getId()
|
||||||
|
);
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
props.onFilterUpdate(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
let removedCriterionId = "";
|
let removedCriterionId = "";
|
||||||
function onRemoveCriterionTag(criterion?: Criterion) {
|
function onRemoveCriterionTag(criterion?: Criterion) {
|
||||||
if (!criterion) {
|
if (!criterion) {
|
||||||
|
|
@ -98,8 +146,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
}
|
}
|
||||||
setEditingCriterion(undefined);
|
setEditingCriterion(undefined);
|
||||||
removedCriterionId = criterion.getId();
|
removedCriterionId = criterion.getId();
|
||||||
props.onRemoveCriterion(criterion);
|
onRemoveCriterion(criterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickCriterionTag(criterion?: Criterion) {
|
function onClickCriterionTag(criterion?: Criterion) {
|
||||||
if (!criterion || removedCriterionId !== "") {
|
if (!criterion || removedCriterionId !== "") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -140,6 +189,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
return "Wall";
|
return "Wall";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.filter.displayModeOptions.map((option) => (
|
return props.filter.displayModeOptions.map((option) => (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
key={option}
|
key={option}
|
||||||
|
|
@ -189,6 +239,18 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEdit() {
|
||||||
|
if (props.onEdit) {
|
||||||
|
props.onEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
if (props.onDelete) {
|
||||||
|
props.onDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSelectAll() {
|
function renderSelectAll() {
|
||||||
if (props.onSelectAll) {
|
if (props.onSelectAll) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -258,7 +320,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
if (props.onChangeZoom) {
|
if (props.onChangeZoom) {
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="zoom-slider col-1 d-none d-sm-block"
|
className="zoom-slider d-none d-sm-inline-flex"
|
||||||
type="range"
|
type="range"
|
||||||
min={0}
|
min={0}
|
||||||
max={3}
|
max={3}
|
||||||
|
|
@ -271,85 +333,141 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderSelectedButtons() {
|
||||||
|
if (props.itemsSelected) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.onEdit ? (
|
||||||
|
<ButtonGroup className="mr-1">
|
||||||
|
<OverlayTrigger overlay={<Tooltip id="edit">Edit</Tooltip>}>
|
||||||
|
<Button variant="secondary" onClick={onEdit}>
|
||||||
|
<Icon icon="pencil-alt" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</ButtonGroup>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{props.onDelete ? (
|
||||||
|
<ButtonGroup className="mr-1">
|
||||||
|
<OverlayTrigger overlay={<Tooltip id="delete">Delete</Tooltip>}>
|
||||||
|
<Button variant="danger" onClick={onDelete}>
|
||||||
|
<Icon icon="trash" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</ButtonGroup>
|
||||||
|
) : undefined}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="filter-container">
|
<div className="form-row align-items-center justify-content-center">
|
||||||
<Form.Control
|
<Col sm={12} md={6} xl={4} lg={5} className="my-1">
|
||||||
placeholder="Search..."
|
<Row className="justify-content-center">
|
||||||
defaultValue={props.filter.searchTerm}
|
<Col xs={6} className="px-1">
|
||||||
onInput={onChangeQuery}
|
<InputGroup>
|
||||||
className="filter-item col-5 col-sm-2 bg-secondary text-white border-secondary"
|
<FormControl
|
||||||
/>
|
placeholder="Search..."
|
||||||
<Form.Control
|
defaultValue={props.filter.searchTerm}
|
||||||
as="select"
|
onInput={onChangeQuery}
|
||||||
onChange={onChangePageSize}
|
className="bg-secondary text-white border-secondary"
|
||||||
value={props.filter.itemsPerPage.toString()}
|
|
||||||
className="btn-secondary filter-item col-1 d-none d-sm-inline"
|
|
||||||
>
|
|
||||||
{PAGE_SIZE_OPTIONS.map((s) => (
|
|
||||||
<option value={s} key={s}>
|
|
||||||
{s}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
<ButtonGroup className="filter-item">
|
|
||||||
<Dropdown as={ButtonGroup}>
|
|
||||||
<Dropdown.Toggle split variant="secondary" id="more-menu">
|
|
||||||
{props.filter.sortBy}
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
<Dropdown.Menu className="bg-secondary text-white">
|
|
||||||
{renderSortByOptions()}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
<OverlayTrigger
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="sort-direction-tooltip">
|
|
||||||
{props.filter.sortDirection === SortDirectionEnum.Asc
|
|
||||||
? "Ascending"
|
|
||||||
: "Descending"}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" onClick={onChangeSortDirection}>
|
|
||||||
<Icon
|
|
||||||
icon={
|
|
||||||
props.filter.sortDirection === SortDirectionEnum.Asc
|
|
||||||
? "caret-up"
|
|
||||||
: "caret-down"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
<InputGroup.Append>
|
||||||
{props.filter.sortBy === "random" && (
|
<AddFilter
|
||||||
<OverlayTrigger
|
filter={props.filter}
|
||||||
overlay={
|
onAddCriterion={onAddCriterion}
|
||||||
<Tooltip id="sort-reshuffle-tooltip">Reshuffle</Tooltip>
|
onCancel={onCancelAddCriterion}
|
||||||
}
|
editingCriterion={editingCriterion}
|
||||||
|
/>
|
||||||
|
</InputGroup.Append>
|
||||||
|
</InputGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs="auto" className="px-1">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Dropdown as={ButtonGroup}>
|
||||||
|
<Dropdown.Toggle split variant="secondary" id="more-menu">
|
||||||
|
{props.filter.sortBy}
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu className="bg-secondary text-white">
|
||||||
|
{renderSortByOptions()}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="sort-direction-tooltip">
|
||||||
|
{props.filter.sortDirection === SortDirectionEnum.Asc
|
||||||
|
? "Ascending"
|
||||||
|
: "Descending"}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onChangeSortDirection}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
props.filter.sortDirection === SortDirectionEnum.Asc
|
||||||
|
? "caret-up"
|
||||||
|
: "caret-down"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
{props.filter.sortBy === "random" && (
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="sort-reshuffle-tooltip">
|
||||||
|
Reshuffle
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onReshuffleRandomSort}
|
||||||
|
>
|
||||||
|
<Icon icon="random" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs="auto" className="px-1">
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
onChange={onChangePageSize}
|
||||||
|
value={props.filter.itemsPerPage.toString()}
|
||||||
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
<Button variant="secondary" onClick={onReshuffleRandomSort}>
|
{PAGE_SIZE_OPTIONS.map((s) => (
|
||||||
<Icon icon="random" />
|
<option value={s} key={s}>
|
||||||
</Button>
|
{s}
|
||||||
</OverlayTrigger>
|
</option>
|
||||||
)}
|
))}
|
||||||
</Dropdown>
|
</Form.Control>
|
||||||
</ButtonGroup>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<AddFilter
|
<Col sm={12} md="auto" className="my-1">
|
||||||
filter={props.filter}
|
<Row className="align-items-center justify-content-center">
|
||||||
onAddCriterion={onAddCriterion}
|
{maybeRenderSelectedButtons()}
|
||||||
onCancel={onCancelAddCriterion}
|
|
||||||
editingCriterion={editingCriterion}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ButtonGroup className="filter-item d-none d-sm-inline-flex">
|
<ButtonGroup className="mr-3">{renderMore()}</ButtonGroup>
|
||||||
{renderDisplayModeOptions()}
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
{maybeRenderZoom()}
|
<ButtonGroup className="mr-3">
|
||||||
|
{renderDisplayModeOptions()}
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup className="filter-item d-none d-sm-inline-flex">
|
<ButtonGroup>{maybeRenderZoom()}</ButtonGroup>
|
||||||
{renderMore()}
|
</Row>
|
||||||
</ButtonGroup>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex justify-content-center">
|
<div className="d-flex justify-content-center">
|
||||||
{renderFilterTags()}
|
{renderFilterTags()}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-slider {
|
.zoom-slider {
|
||||||
|
max-width: 60px;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx
Normal file
91
ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { useScenesDestroy } from "src/core/StashService";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { Modal } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
interface IDeleteSceneDialogProps {
|
||||||
|
selected: GQL.SlimSceneDataFragment[];
|
||||||
|
onClose: (confirmed: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
||||||
|
props: IDeleteSceneDialogProps
|
||||||
|
) => {
|
||||||
|
const plural = props.selected.length > 1;
|
||||||
|
|
||||||
|
const singleMessageId = "deleteSceneText";
|
||||||
|
const pluralMessageId = "deleteScenesText";
|
||||||
|
|
||||||
|
const singleMessage =
|
||||||
|
"Are you sure you want to delete this scene? Unless the file is also deleted, this scene will be re-added when scan is performed.";
|
||||||
|
const pluralMessage =
|
||||||
|
"Are you sure you want to delete these scenes? Unless the files are also deleted, these scenes will be re-added when scan is performed.";
|
||||||
|
|
||||||
|
const header = plural ? "Delete Scenes" : "Delete Scene";
|
||||||
|
const toastMessage = plural ? "Deleted scenes" : "Deleted scene";
|
||||||
|
const messageId = plural ? pluralMessageId : singleMessageId;
|
||||||
|
const message = plural ? pluralMessage : singleMessage;
|
||||||
|
|
||||||
|
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||||
|
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const Toast = useToast();
|
||||||
|
const [deleteScene] = useScenesDestroy(getScenesDeleteInput());
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
function getScenesDeleteInput(): GQL.ScenesDestroyInput {
|
||||||
|
return {
|
||||||
|
ids: props.selected.map((scene) => scene.id),
|
||||||
|
delete_file: deleteFile,
|
||||||
|
delete_generated: deleteGenerated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDelete() {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteScene();
|
||||||
|
Toast.success({ content: toastMessage });
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
setIsDeleting(false);
|
||||||
|
props.onClose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show
|
||||||
|
icon="trash-alt"
|
||||||
|
header={header}
|
||||||
|
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => props.onClose(false),
|
||||||
|
text: "Cancel",
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
isRunning={isDeleting}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage id={messageId} defaultMessage={message} />
|
||||||
|
</p>
|
||||||
|
<Form>
|
||||||
|
<Form.Check
|
||||||
|
checked={deleteFile}
|
||||||
|
label="Delete file"
|
||||||
|
onChange={() => setDeleteFile(!deleteFile)}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
checked={deleteGenerated}
|
||||||
|
label="Delete generated supporting files"
|
||||||
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,36 +1,38 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useBulkSceneUpdate } from "src/core/StashService";
|
import { useBulkSceneUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StudioSelect, LoadingIndicator } from "src/components/Shared";
|
import { StudioSelect, Modal } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
|
import { RatingStars } from "./SceneDetails/RatingStars";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimSceneDataFragment[];
|
selected: GQL.SlimSceneDataFragment[];
|
||||||
onScenesUpdated: () => void;
|
onClose: (applied: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||||
props: IListOperationProps
|
props: IListOperationProps
|
||||||
) => {
|
) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<string>("");
|
const [rating, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [performerMode, setPerformerMode] = React.useState<
|
const [performerMode, setPerformerMode] = React.useState<
|
||||||
GQL.BulkUpdateIdMode
|
GQL.BulkUpdateIdMode
|
||||||
>(GQL.BulkUpdateIdMode.Add);
|
>(GQL.BulkUpdateIdMode.Set);
|
||||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||||
GQL.BulkUpdateIdMode.Add
|
GQL.BulkUpdateIdMode.Set
|
||||||
);
|
);
|
||||||
const [tagIds, setTagIds] = useState<string[]>();
|
const [tagIds, setTagIds] = useState<string[]>();
|
||||||
|
|
||||||
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
|
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
function makeBulkUpdateIds(
|
function makeBulkUpdateIds(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
|
|
@ -56,7 +58,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
// if rating is undefined
|
||||||
if (rating === "") {
|
if (rating === undefined) {
|
||||||
// and all scenes have the same rating, then we are unsetting the rating.
|
// and all scenes have the same rating, then we are unsetting the rating.
|
||||||
if (aggregateRating) {
|
if (aggregateRating) {
|
||||||
// an undefined rating is ignored in the server, so set it to 0 instead
|
// an undefined rating is ignored in the server, so set it to 0 instead
|
||||||
|
|
@ -65,7 +67,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
// otherwise not setting the rating
|
// otherwise not setting the rating
|
||||||
} else {
|
} else {
|
||||||
// if rating is set, then we are setting the rating for all
|
// if rating is set, then we are setting the rating for all
|
||||||
sceneInput.rating = Number.parseInt(rating, 10);
|
sceneInput.rating = rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if studioId is undefined
|
// if studioId is undefined
|
||||||
|
|
@ -121,15 +123,15 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
setIsLoading(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
await updateScenes();
|
await updateScenes();
|
||||||
Toast.success({ content: "Updated scenes" });
|
Toast.success({ content: "Updated scenes" });
|
||||||
|
props.onClose(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsUpdating(false);
|
||||||
props.onScenesUpdated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRating(state: GQL.SlimSceneDataFragment[]) {
|
function getRating(state: GQL.SlimSceneDataFragment[]) {
|
||||||
|
|
@ -211,14 +213,14 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = props.selected;
|
const state = props.selected;
|
||||||
let updateRating = "";
|
let updateRating: number | undefined;
|
||||||
let updateStudioID: string | undefined;
|
let updateStudioID: string | undefined;
|
||||||
let updatePerformerIds: string[] = [];
|
let updatePerformerIds: string[] = [];
|
||||||
let updateTagIds: string[] = [];
|
let updateTagIds: string[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||||
const sceneRating = scene.rating?.toString() ?? "";
|
const sceneRating = scene.rating;
|
||||||
const sceneStudioID = scene?.studio?.id;
|
const sceneStudioID = scene?.studio?.id;
|
||||||
const scenePerformerIDs = (scene.performers ?? [])
|
const scenePerformerIDs = (scene.performers ?? [])
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
|
|
@ -226,14 +228,14 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
const sceneTagIDs = (scene.tags ?? []).map((p) => p.id).sort();
|
const sceneTagIDs = (scene.tags ?? []).map((p) => p.id).sort();
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
updateRating = sceneRating;
|
updateRating = sceneRating ?? undefined;
|
||||||
updateStudioID = sceneStudioID;
|
updateStudioID = sceneStudioID;
|
||||||
updatePerformerIds = scenePerformerIDs;
|
updatePerformerIds = scenePerformerIDs;
|
||||||
updateTagIds = sceneTagIDs;
|
updateTagIds = sceneTagIDs;
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
if (sceneRating !== updateRating) {
|
if (sceneRating !== updateRating) {
|
||||||
updateRating = "";
|
updateRating = undefined;
|
||||||
}
|
}
|
||||||
if (sceneStudioID !== updateStudioID) {
|
if (sceneStudioID !== updateStudioID) {
|
||||||
updateStudioID = undefined;
|
updateStudioID = undefined;
|
||||||
|
|
@ -256,8 +258,6 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
if (tagMode === GQL.BulkUpdateIdMode.Set) {
|
if (tagMode === GQL.BulkUpdateIdMode.Set) {
|
||||||
setTagIds(updateTagIds);
|
setTagIds(updateTagIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
}, [props.selected, performerMode, tagMode]);
|
}, [props.selected, performerMode, tagMode]);
|
||||||
|
|
||||||
function renderMultiSelect(
|
function renderMultiSelect(
|
||||||
|
|
@ -277,6 +277,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
return (
|
return (
|
||||||
<MultiSet
|
<MultiSet
|
||||||
type={type}
|
type={type}
|
||||||
|
disabled={isUpdating}
|
||||||
onUpdate={(items) => {
|
onUpdate={(items) => {
|
||||||
const itemIDs = items.map((i) => i.id);
|
const itemIDs = items.map((i) => i.id);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -304,54 +305,60 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<div className="operation-container">
|
<Modal
|
||||||
<Form.Group
|
show
|
||||||
controlId="rating"
|
icon="pencil-alt"
|
||||||
className="operation-item rating-operation"
|
header="Edit Scenes"
|
||||||
>
|
accept={{ onClick: onSave, text: "Apply" }}
|
||||||
<Form.Label>Rating</Form.Label>
|
cancel={{
|
||||||
<Form.Control
|
onClick: () => props.onClose(false),
|
||||||
as="select"
|
text: "Cancel",
|
||||||
className="btn-secondary"
|
variant: "secondary",
|
||||||
value={rating}
|
}}
|
||||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
isRunning={isUpdating}
|
||||||
setRating(event.currentTarget.value)
|
>
|
||||||
}
|
<Form>
|
||||||
>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{["", "1", "2", "3", "4", "5"].map((opt) => (
|
{FormUtils.renderLabel({
|
||||||
<option key={opt} value={opt}>
|
title: "Rating",
|
||||||
{opt}
|
})}
|
||||||
</option>
|
<Col xs={9}>
|
||||||
))}
|
<RatingStars
|
||||||
</Form.Control>
|
value={rating}
|
||||||
</Form.Group>
|
onSetRating={(value) => setRating(value)}
|
||||||
|
disabled={isUpdating}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" className="operation-item">
|
<Form.Group controlId="studio" as={Row}>
|
||||||
<Form.Label>Studio</Form.Label>
|
{FormUtils.renderLabel({
|
||||||
<StudioSelect
|
title: "Studio",
|
||||||
onSelect={(items) => setStudioId(items[0]?.id)}
|
})}
|
||||||
ids={studioId ? [studioId] : []}
|
<Col xs={9}>
|
||||||
/>
|
<StudioSelect
|
||||||
</Form.Group>
|
onSelect={(items) =>
|
||||||
|
setStudioId(items.length > 0 ? items[0]?.id : undefined)
|
||||||
|
}
|
||||||
|
ids={studioId ? [studioId] : []}
|
||||||
|
isDisabled={isUpdating}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className="operation-item" controlId="performers">
|
<Form.Group controlId="performers">
|
||||||
<Form.Label>Performers</Form.Label>
|
<Form.Label>Performers</Form.Label>
|
||||||
{renderMultiSelect("performers", performerIds)}
|
{renderMultiSelect("performers", performerIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className="operation-item" controlId="performers">
|
<Form.Group controlId="performers">
|
||||||
<Form.Label>Tags</Form.Label>
|
<Form.Label>Tags</Form.Label>
|
||||||
{renderMultiSelect("tags", tagIds)}
|
{renderMultiSelect("tags", tagIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
</Form>
|
||||||
<Button variant="primary" onClick={onSave} className="apply-operation">
|
</Modal>
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,7 +243,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||||
>
|
>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="scene-card-check d-none d-sm-block"
|
className="scene-card-check"
|
||||||
checked={props.selected}
|
checked={props.selected}
|
||||||
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
|
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
|
||||||
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
|
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ import { Icon } from "src/components/Shared";
|
||||||
export interface IRatingStarsProps {
|
export interface IRatingStarsProps {
|
||||||
value?: number;
|
value?: number;
|
||||||
onSetRating?: (value?: number) => void;
|
onSetRating?: (value?: number) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingStars: React.FC<IRatingStarsProps> = (
|
export const RatingStars: React.FC<IRatingStarsProps> = (
|
||||||
props: IRatingStarsProps
|
props: IRatingStarsProps
|
||||||
) => {
|
) => {
|
||||||
const [hoverRating, setHoverRating] = useState<number | undefined>();
|
const [hoverRating, setHoverRating] = useState<number | undefined>();
|
||||||
const disabled = !props.onSetRating;
|
const disabled = props.disabled || !props.onSetRating;
|
||||||
|
|
||||||
function setRating(rating: number) {
|
function setRating(rating: number) {
|
||||||
if (!props.onSetRating) {
|
if (!props.onSetRating) {
|
||||||
|
|
@ -109,7 +110,7 @@ export const RatingStars: React.FC<IRatingStarsProps> = (
|
||||||
const maxRating = 5;
|
const maxRating = 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rating-stars">
|
<div className="rating-stars align-middle">
|
||||||
{Array.from(Array(maxRating)).map((value, index) =>
|
{Array.from(Array(maxRating)).map((value, index) =>
|
||||||
renderRatingButton(index + 1)
|
renderRatingButton(index + 1)
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Tab, Nav, Dropdown, Form } from "react-bootstrap";
|
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
|
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
|
||||||
|
|
@ -9,10 +9,9 @@ import {
|
||||||
useSceneDecrementO,
|
useSceneDecrementO,
|
||||||
useSceneResetO,
|
useSceneResetO,
|
||||||
useSceneGenerateScreenshot,
|
useSceneGenerateScreenshot,
|
||||||
useSceneDestroy,
|
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
|
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
|
||||||
import { LoadingIndicator, Icon, Modal } from "src/components/Shared";
|
import { LoadingIndicator, Icon } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { ScenePlayer } from "src/components/ScenePlayer";
|
import { ScenePlayer } from "src/components/ScenePlayer";
|
||||||
import { TextUtils, JWUtils } from "src/utils";
|
import { TextUtils, JWUtils } from "src/utils";
|
||||||
|
|
@ -22,6 +21,7 @@ import { SceneEditPanel } from "./SceneEditPanel";
|
||||||
import { SceneDetailPanel } from "./SceneDetailPanel";
|
import { SceneDetailPanel } from "./SceneDetailPanel";
|
||||||
import { OCounterButton } from "./OCounterButton";
|
import { OCounterButton } from "./OCounterButton";
|
||||||
import { SceneMoviePanel } from "./SceneMoviePanel";
|
import { SceneMoviePanel } from "./SceneMoviePanel";
|
||||||
|
import { DeleteScenesDialog } from "../DeleteScenesDialog";
|
||||||
|
|
||||||
export const Scene: React.FC = () => {
|
export const Scene: React.FC = () => {
|
||||||
const { id = "new" } = useParams();
|
const { id = "new" } = useParams();
|
||||||
|
|
@ -39,10 +39,6 @@ export const Scene: React.FC = () => {
|
||||||
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
||||||
|
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
|
||||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
||||||
const [deleteScene] = useSceneDestroy(getSceneDeleteInput());
|
|
||||||
|
|
||||||
const queryParams = queryString.parse(location.search);
|
const queryParams = queryString.parse(location.search);
|
||||||
const autoplay = queryParams?.autoplay === "true";
|
const autoplay = queryParams?.autoplay === "true";
|
||||||
|
|
@ -120,54 +116,19 @@ export const Scene: React.FC = () => {
|
||||||
Toast.success({ content: "Generating screenshot" });
|
Toast.success({ content: "Generating screenshot" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSceneDeleteInput(): GQL.SceneDestroyInput {
|
function onDeleteDialogClosed(deleted: boolean) {
|
||||||
return {
|
|
||||||
id: scene?.id ?? "0",
|
|
||||||
delete_file: deleteFile,
|
|
||||||
delete_generated: deleteGenerated,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onDelete() {
|
|
||||||
setIsDeleteAlertOpen(false);
|
setIsDeleteAlertOpen(false);
|
||||||
setDeleteLoading(true);
|
if (deleted) {
|
||||||
try {
|
history.push("/scenes");
|
||||||
await deleteScene();
|
|
||||||
Toast.success({ content: "Deleted scene" });
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
}
|
}
|
||||||
setDeleteLoading(false);
|
|
||||||
history.push("/scenes");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteAlert() {
|
function maybeRenderDeleteDialog() {
|
||||||
return (
|
if (isDeleteAlertOpen && scene) {
|
||||||
<Modal
|
return (
|
||||||
show={isDeleteAlertOpen}
|
<DeleteScenesDialog selected={[scene]} onClose={onDeleteDialogClosed} />
|
||||||
icon="trash-alt"
|
);
|
||||||
header="Delete Scene?"
|
}
|
||||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
|
||||||
cancel={{ onClick: () => setIsDeleteAlertOpen(false), text: "Cancel" }}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Are you sure you want to delete this scene? Unless the file is also
|
|
||||||
deleted, this scene will be re-added when scan is performed.
|
|
||||||
</p>
|
|
||||||
<Form>
|
|
||||||
<Form.Check
|
|
||||||
checked={deleteFile}
|
|
||||||
label="Delete file"
|
|
||||||
onChange={() => setDeleteFile(!deleteFile)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
checked={deleteGenerated}
|
|
||||||
label="Delete generated supporting files"
|
|
||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOperations() {
|
function renderOperations() {
|
||||||
|
|
@ -294,7 +255,7 @@ export const Scene: React.FC = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteLoading || loading || !scene || !data?.findScene) {
|
if (loading || !scene || !data?.findScene) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,7 +263,7 @@ export const Scene: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{renderDeleteAlert()}
|
{maybeRenderDeleteDialog()}
|
||||||
<div className="scene-tabs order-xl-first order-last">
|
<div className="scene-tabs order-xl-first order-last">
|
||||||
<div className="d-none d-xl-block">
|
<div className="d-none d-xl-block">
|
||||||
{scene.studio && (
|
{scene.studio && (
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { WallPanel } from "../Wall/WallPanel";
|
import { WallPanel } from "../Wall/WallPanel";
|
||||||
import { SceneCard } from "./SceneCard";
|
import { SceneCard } from "./SceneCard";
|
||||||
import { SceneListTable } from "./SceneListTable";
|
import { SceneListTable } from "./SceneListTable";
|
||||||
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
import { EditScenesDialog } from "./EditScenesDialog";
|
||||||
|
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
||||||
|
|
||||||
interface ISceneList {
|
interface ISceneList {
|
||||||
subComponent?: boolean;
|
subComponent?: boolean;
|
||||||
|
|
@ -35,7 +36,8 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||||
zoomable: true,
|
zoomable: true,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
renderSelectedOptions,
|
renderEditDialog: renderEditScenesDialog,
|
||||||
|
renderDeleteDialog: renderDeleteScenesDialog,
|
||||||
subComponent,
|
subComponent,
|
||||||
filterHook,
|
filterHook,
|
||||||
});
|
});
|
||||||
|
|
@ -66,32 +68,24 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectedOptions(
|
function renderEditScenesDialog(
|
||||||
result: FindScenesQueryResult,
|
selectedScenes: SlimSceneDataFragment[],
|
||||||
selectedIds: Set<string>
|
onClose: (applied: boolean) => void
|
||||||
) {
|
) {
|
||||||
// find the selected items from the ids
|
|
||||||
if (!result.data || !result.data.findScenes) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { scenes } = result.data.findScenes;
|
|
||||||
|
|
||||||
const selectedScenes: SlimSceneDataFragment[] = [];
|
|
||||||
selectedIds.forEach((id) => {
|
|
||||||
const scene = scenes.find((s) => s.id === id);
|
|
||||||
|
|
||||||
if (scene) {
|
|
||||||
selectedScenes.push(scene);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SceneSelectedOptions
|
<EditScenesDialog selected={selectedScenes} onClose={onClose} />
|
||||||
selected={selectedScenes}
|
</>
|
||||||
onScenesUpdated={() => {}}
|
);
|
||||||
/>
|
}
|
||||||
|
|
||||||
|
function renderDeleteScenesDialog(
|
||||||
|
selectedScenes: SlimSceneDataFragment[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteScenesDialog selected={selectedScenes} onClose={onClose} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Modal } from "react-bootstrap";
|
import { Button, Modal, Spinner } from "react-bootstrap";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
|
||||||
interface IButton {
|
interface IButton {
|
||||||
text?: string;
|
text?: string;
|
||||||
variant?: "danger" | "primary";
|
variant?: "danger" | "primary" | "secondary";
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface IModal {
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
cancel?: IButton;
|
cancel?: IButton;
|
||||||
accept?: IButton;
|
accept?: IButton;
|
||||||
|
isRunning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalComponent: React.FC<IModal> = ({
|
const ModalComponent: React.FC<IModal> = ({
|
||||||
|
|
@ -26,6 +27,7 @@ const ModalComponent: React.FC<IModal> = ({
|
||||||
cancel,
|
cancel,
|
||||||
accept,
|
accept,
|
||||||
onHide,
|
onHide,
|
||||||
|
isRunning,
|
||||||
}) => (
|
}) => (
|
||||||
<Modal keyboard={false} onHide={onHide} show={show}>
|
<Modal keyboard={false} onHide={onHide} show={show}>
|
||||||
<Modal.Header>
|
<Modal.Header>
|
||||||
|
|
@ -37,6 +39,7 @@ const ModalComponent: React.FC<IModal> = ({
|
||||||
<div>
|
<div>
|
||||||
{cancel ? (
|
{cancel ? (
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isRunning}
|
||||||
variant={cancel.variant ?? "primary"}
|
variant={cancel.variant ?? "primary"}
|
||||||
onClick={cancel.onClick}
|
onClick={cancel.onClick}
|
||||||
>
|
>
|
||||||
|
|
@ -46,10 +49,15 @@ const ModalComponent: React.FC<IModal> = ({
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isRunning}
|
||||||
variant={accept?.variant ?? "primary"}
|
variant={accept?.variant ?? "primary"}
|
||||||
onClick={accept?.onClick}
|
onClick={accept?.onClick}
|
||||||
>
|
>
|
||||||
{accept?.text ?? "Close"}
|
{isRunning ? (
|
||||||
|
<Spinner animation="border" role="status" size="sm" />
|
||||||
|
) : (
|
||||||
|
accept?.text ?? "Close"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ interface IMultiSetProps {
|
||||||
type: "performers" | "studios" | "tags";
|
type: "performers" | "studios" | "tags";
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
mode: GQL.BulkUpdateIdMode;
|
mode: GQL.BulkUpdateIdMode;
|
||||||
|
disabled?: boolean;
|
||||||
onUpdate: (items: ValidTypes[]) => void;
|
onUpdate: (items: ValidTypes[]) => void;
|
||||||
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
|
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +67,7 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => props.onSetMode(nextMode())}
|
onClick={() => props.onSetMode(nextMode())}
|
||||||
title={getModeText()}
|
title={getModeText()}
|
||||||
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
<Icon icon={getModeIcon()} className="fa-fw" />
|
<Icon icon={getModeIcon()} className="fa-fw" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -73,6 +75,7 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
||||||
|
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
type={props.type}
|
type={props.type}
|
||||||
|
isDisabled={props.disabled}
|
||||||
isMulti
|
isMulti
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
onSelect={onUpdate}
|
onSelect={onUpdate}
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,12 @@ export const useSceneDestroy = (input: GQL.SceneDestroyInput) =>
|
||||||
update: () => invalidateQueries(sceneMutationImpactedQueries),
|
update: () => invalidateQueries(sceneMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useScenesDestroy = (input: GQL.ScenesDestroyInput) =>
|
||||||
|
GQL.useScenesDestroyMutation({
|
||||||
|
variables: input,
|
||||||
|
update: () => invalidateQueries(sceneMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const useSceneGenerateScreenshot = () =>
|
export const useSceneGenerateScreenshot = () =>
|
||||||
GQL.useSceneGenerateScreenshotMutation({
|
GQL.useSceneGenerateScreenshotMutation({
|
||||||
update: () => invalidateQueries(["findScenes"]),
|
update: () => invalidateQueries(["findScenes"]),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import React, { useCallback, useState, useEffect } from "react";
|
||||||
import { ApolloError } from "apollo-client";
|
import { ApolloError } from "apollo-client";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
SortDirectionEnum,
|
|
||||||
SlimSceneDataFragment,
|
SlimSceneDataFragment,
|
||||||
SceneMarkerDataFragment,
|
SceneMarkerDataFragment,
|
||||||
GalleryDataFragment,
|
GalleryDataFragment,
|
||||||
|
|
@ -33,9 +32,8 @@ import {
|
||||||
useFindGalleries,
|
useFindGalleries,
|
||||||
useFindPerformers,
|
useFindPerformers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
import { FilterMode } from "src/models/list-filter/types";
|
||||||
|
|
||||||
interface IListHookData {
|
interface IListHookData {
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
|
|
@ -52,7 +50,7 @@ interface IListHookOperation<T> {
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IListHookOptions<T> {
|
interface IListHookOptions<T, E> {
|
||||||
subComponent?: boolean;
|
subComponent?: boolean;
|
||||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
zoomable?: boolean;
|
zoomable?: boolean;
|
||||||
|
|
@ -63,9 +61,13 @@ interface IListHookOptions<T> {
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
zoomIndex: number
|
zoomIndex: number
|
||||||
) => JSX.Element | undefined;
|
) => JSX.Element | undefined;
|
||||||
renderSelectedOptions?: (
|
renderEditDialog?: (
|
||||||
result: T,
|
selected: E[],
|
||||||
selectedIds: Set<string>
|
onClose: (applied: boolean) => void
|
||||||
|
) => JSX.Element | undefined;
|
||||||
|
renderDeleteDialog?: (
|
||||||
|
selected: E[],
|
||||||
|
onClose: (confirmed: boolean) => void
|
||||||
) => JSX.Element | undefined;
|
) => JSX.Element | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,17 +77,20 @@ interface IDataItem {
|
||||||
interface IQueryResult {
|
interface IQueryResult {
|
||||||
error?: ApolloError;
|
error?: ApolloError;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
||||||
filterMode: FilterMode;
|
filterMode: FilterMode;
|
||||||
useData: (filter: ListFilterModel) => T;
|
useData: (filter: ListFilterModel) => T;
|
||||||
getData: (data: T) => T2[];
|
getData: (data: T) => T2[];
|
||||||
|
getSelectedData: (data: T, selectedIds: Set<string>) => T2[];
|
||||||
getCount: (data: T) => number;
|
getCount: (data: T) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
options: IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>
|
options: IListHookOptions<QueryResult, QueryData> &
|
||||||
|
IQuery<QueryResult, QueryData>
|
||||||
): IListHookData => {
|
): IListHookData => {
|
||||||
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
|
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
|
||||||
const [forageInitialised, setForageInitialised] = useState(false);
|
const [forageInitialised, setForageInitialised] = useState(false);
|
||||||
|
|
@ -97,6 +102,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
options.subComponent ? undefined : queryString.parse(location.search)
|
options.subComponent ? undefined : queryString.parse(location.search)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
||||||
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
||||||
|
|
@ -191,79 +198,6 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangePageSize(pageSize: number) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.itemsPerPage = pageSize;
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeQuery(query: string) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.searchTerm = query;
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeSortDirection(sortDirection: SortDirectionEnum) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.sortDirection = sortDirection;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeSortBy(sortBy: string) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.sortBy = sortBy;
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSortReshuffle() {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
newFilter.randomSeed = -1;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeDisplayMode(displayMode: DisplayMode) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.displayMode = displayMode;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAddCriterion(criterion: Criterion, oldId?: string) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
|
|
||||||
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
|
||||||
const existingIndex = newFilter.criteria.findIndex((c) => {
|
|
||||||
// If we modified an existing criterion, then look for the old id.
|
|
||||||
const id = oldId || criterion.getId();
|
|
||||||
return c.getId() === id;
|
|
||||||
});
|
|
||||||
if (existingIndex === -1) {
|
|
||||||
newFilter.criteria.push(criterion);
|
|
||||||
} else {
|
|
||||||
newFilter.criteria[existingIndex] = criterion;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicate modifiers
|
|
||||||
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
|
||||||
return arr.map((mapObj) => mapObj.getId()).indexOf(obj.getId()) === pos;
|
|
||||||
});
|
|
||||||
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemoveCriterion(removedCriterion: Criterion) {
|
|
||||||
const newFilter = _.cloneDeep(filter);
|
|
||||||
newFilter.criteria = newFilter.criteria.filter(
|
|
||||||
(criterion) => criterion.getId() !== removedCriterion.getId()
|
|
||||||
);
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
updateQueryParams(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangePage(page: number) {
|
function onChangePage(page: number) {
|
||||||
const newFilter = _.cloneDeep(filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
newFilter.currentPage = page;
|
newFilter.currentPage = page;
|
||||||
|
|
@ -389,26 +323,59 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEdit() {
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditDialogClosed(applied: boolean) {
|
||||||
|
if (applied) {
|
||||||
|
onSelectNone();
|
||||||
|
}
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
|
||||||
|
// refetch
|
||||||
|
result.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDeleteDialogClosed(deleted: boolean) {
|
||||||
|
if (deleted) {
|
||||||
|
onSelectNone();
|
||||||
|
}
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
|
||||||
|
// refetch
|
||||||
|
result.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
const template = (
|
const template = (
|
||||||
<div>
|
<div>
|
||||||
<ListFilter
|
<ListFilter
|
||||||
onChangePageSize={onChangePageSize}
|
onFilterUpdate={updateQueryParams}
|
||||||
onChangeQuery={onChangeQuery}
|
|
||||||
onChangeSortDirection={onChangeSortDirection}
|
|
||||||
onChangeSortBy={onChangeSortBy}
|
|
||||||
onSortReshuffle={onSortReshuffle}
|
|
||||||
onChangeDisplayMode={onChangeDisplayMode}
|
|
||||||
onAddCriterion={onAddCriterion}
|
|
||||||
onRemoveCriterion={onRemoveCriterion}
|
|
||||||
onSelectAll={onSelectAll}
|
onSelectAll={onSelectAll}
|
||||||
onSelectNone={onSelectNone}
|
onSelectNone={onSelectNone}
|
||||||
zoomIndex={options.zoomable ? zoomIndex : undefined}
|
zoomIndex={options.zoomable ? zoomIndex : undefined}
|
||||||
onChangeZoom={options.zoomable ? onChangeZoom : undefined}
|
onChangeZoom={options.zoomable ? onChangeZoom : undefined}
|
||||||
otherOperations={otherOperations}
|
otherOperations={otherOperations}
|
||||||
|
itemsSelected={selectedIds.size > 0}
|
||||||
|
onEdit={options.renderEditDialog ? onEdit : undefined}
|
||||||
|
onDelete={options.renderDeleteDialog ? onDelete : undefined}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
/>
|
/>
|
||||||
{options.renderSelectedOptions && selectedIds.size > 0
|
{isEditDialogOpen && options.renderEditDialog
|
||||||
? options.renderSelectedOptions(result, selectedIds)
|
? options.renderEditDialog(
|
||||||
|
options.getSelectedData(result, selectedIds),
|
||||||
|
(applied) => onEditDialogClosed(applied)
|
||||||
|
)
|
||||||
|
: undefined}
|
||||||
|
{isDeleteDialogOpen && options.renderDeleteDialog
|
||||||
|
? options.renderDeleteDialog(
|
||||||
|
options.getSelectedData(result, selectedIds),
|
||||||
|
(deleted) => onDeleteDialogClosed(deleted)
|
||||||
|
)
|
||||||
: undefined}
|
: undefined}
|
||||||
{(result.loading || !forageInitialised) && <LoadingIndicator />}
|
{(result.loading || !forageInitialised) && <LoadingIndicator />}
|
||||||
{result.error && <h1>{result.error.message}</h1>}
|
{result.error && <h1>{result.error.message}</h1>}
|
||||||
|
|
@ -422,7 +389,27 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
return { filter, template, onSelectChange };
|
return { filter, template, onSelectChange };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useScenesList = (props: IListHookOptions<FindScenesQueryResult>) =>
|
const getSelectedData = <I extends IDataItem>(
|
||||||
|
result: I[],
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) => {
|
||||||
|
// find the selected items from the ids
|
||||||
|
const selectedResults: I[] = [];
|
||||||
|
|
||||||
|
selectedIds.forEach((id) => {
|
||||||
|
const item = result.find((s) => s.id === id);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
selectedResults.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return selectedResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScenesList = (
|
||||||
|
props: IListHookOptions<FindScenesQueryResult, SlimSceneDataFragment>
|
||||||
|
) =>
|
||||||
useList<FindScenesQueryResult, SlimSceneDataFragment>({
|
useList<FindScenesQueryResult, SlimSceneDataFragment>({
|
||||||
...props,
|
...props,
|
||||||
filterMode: FilterMode.Scenes,
|
filterMode: FilterMode.Scenes,
|
||||||
|
|
@ -431,10 +418,14 @@ export const useScenesList = (props: IListHookOptions<FindScenesQueryResult>) =>
|
||||||
result?.data?.findScenes?.scenes ?? [],
|
result?.data?.findScenes?.scenes ?? [],
|
||||||
getCount: (result: FindScenesQueryResult) =>
|
getCount: (result: FindScenesQueryResult) =>
|
||||||
result?.data?.findScenes?.count ?? 0,
|
result?.data?.findScenes?.count ?? 0,
|
||||||
|
getSelectedData: (
|
||||||
|
result: FindScenesQueryResult,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) => getSelectedData(result?.data?.findScenes?.scenes ?? [], selectedIds),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useSceneMarkersList = (
|
export const useSceneMarkersList = (
|
||||||
props: IListHookOptions<FindSceneMarkersQueryResult>
|
props: IListHookOptions<FindSceneMarkersQueryResult, SceneMarkerDataFragment>
|
||||||
) =>
|
) =>
|
||||||
useList<FindSceneMarkersQueryResult, SceneMarkerDataFragment>({
|
useList<FindSceneMarkersQueryResult, SceneMarkerDataFragment>({
|
||||||
...props,
|
...props,
|
||||||
|
|
@ -444,10 +435,18 @@ export const useSceneMarkersList = (
|
||||||
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
||||||
getCount: (result: FindSceneMarkersQueryResult) =>
|
getCount: (result: FindSceneMarkersQueryResult) =>
|
||||||
result?.data?.findSceneMarkers?.count ?? 0,
|
result?.data?.findSceneMarkers?.count ?? 0,
|
||||||
|
getSelectedData: (
|
||||||
|
result: FindSceneMarkersQueryResult,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) =>
|
||||||
|
getSelectedData(
|
||||||
|
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
||||||
|
selectedIds
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGalleriesList = (
|
export const useGalleriesList = (
|
||||||
props: IListHookOptions<FindGalleriesQueryResult>
|
props: IListHookOptions<FindGalleriesQueryResult, GalleryDataFragment>
|
||||||
) =>
|
) =>
|
||||||
useList<FindGalleriesQueryResult, GalleryDataFragment>({
|
useList<FindGalleriesQueryResult, GalleryDataFragment>({
|
||||||
...props,
|
...props,
|
||||||
|
|
@ -457,10 +456,18 @@ export const useGalleriesList = (
|
||||||
result?.data?.findGalleries?.galleries ?? [],
|
result?.data?.findGalleries?.galleries ?? [],
|
||||||
getCount: (result: FindGalleriesQueryResult) =>
|
getCount: (result: FindGalleriesQueryResult) =>
|
||||||
result?.data?.findGalleries?.count ?? 0,
|
result?.data?.findGalleries?.count ?? 0,
|
||||||
|
getSelectedData: (
|
||||||
|
result: FindGalleriesQueryResult,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) =>
|
||||||
|
getSelectedData(
|
||||||
|
result?.data?.findGalleries?.galleries ?? [],
|
||||||
|
selectedIds
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useStudiosList = (
|
export const useStudiosList = (
|
||||||
props: IListHookOptions<FindStudiosQueryResult>
|
props: IListHookOptions<FindStudiosQueryResult, StudioDataFragment>
|
||||||
) =>
|
) =>
|
||||||
useList<FindStudiosQueryResult, StudioDataFragment>({
|
useList<FindStudiosQueryResult, StudioDataFragment>({
|
||||||
...props,
|
...props,
|
||||||
|
|
@ -470,10 +477,14 @@ export const useStudiosList = (
|
||||||
result?.data?.findStudios?.studios ?? [],
|
result?.data?.findStudios?.studios ?? [],
|
||||||
getCount: (result: FindStudiosQueryResult) =>
|
getCount: (result: FindStudiosQueryResult) =>
|
||||||
result?.data?.findStudios?.count ?? 0,
|
result?.data?.findStudios?.count ?? 0,
|
||||||
|
getSelectedData: (
|
||||||
|
result: FindStudiosQueryResult,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) => getSelectedData(result?.data?.findStudios?.studios ?? [], selectedIds),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usePerformersList = (
|
export const usePerformersList = (
|
||||||
props: IListHookOptions<FindPerformersQueryResult>
|
props: IListHookOptions<FindPerformersQueryResult, PerformerDataFragment>
|
||||||
) =>
|
) =>
|
||||||
useList<FindPerformersQueryResult, PerformerDataFragment>({
|
useList<FindPerformersQueryResult, PerformerDataFragment>({
|
||||||
...props,
|
...props,
|
||||||
|
|
@ -483,9 +494,19 @@ export const usePerformersList = (
|
||||||
result?.data?.findPerformers?.performers ?? [],
|
result?.data?.findPerformers?.performers ?? [],
|
||||||
getCount: (result: FindPerformersQueryResult) =>
|
getCount: (result: FindPerformersQueryResult) =>
|
||||||
result?.data?.findPerformers?.count ?? 0,
|
result?.data?.findPerformers?.count ?? 0,
|
||||||
|
getSelectedData: (
|
||||||
|
result: FindPerformersQueryResult,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) =>
|
||||||
|
getSelectedData(
|
||||||
|
result?.data?.findPerformers?.performers ?? [],
|
||||||
|
selectedIds
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMoviesList = (props: IListHookOptions<FindMoviesQueryResult>) =>
|
export const useMoviesList = (
|
||||||
|
props: IListHookOptions<FindMoviesQueryResult, MovieDataFragment>
|
||||||
|
) =>
|
||||||
useList<FindMoviesQueryResult, MovieDataFragment>({
|
useList<FindMoviesQueryResult, MovieDataFragment>({
|
||||||
...props,
|
...props,
|
||||||
filterMode: FilterMode.Movies,
|
filterMode: FilterMode.Movies,
|
||||||
|
|
@ -494,4 +515,8 @@ export const useMoviesList = (props: IListHookOptions<FindMoviesQueryResult>) =>
|
||||||
result?.data?.findMovies?.movies ?? [],
|
result?.data?.findMovies?.movies ?? [],
|
||||||
getCount: (result: FindMoviesQueryResult) =>
|
getCount: (result: FindMoviesQueryResult) =>
|
||||||
result?.data?.findMovies?.count ?? 0,
|
result?.data?.findMovies?.count ?? 0,
|
||||||
|
getSelectedData: (
|
||||||
|
result: FindMoviesQueryResult,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) => getSelectedData(result?.data?.findMovies?.movies ?? [], selectedIds),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -211,38 +211,6 @@ div.dropdown-menu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* we don't want to override this for dialogs, which are light colored */
|
|
||||||
.modal {
|
|
||||||
div.react-select__control {
|
|
||||||
background-color: #fff;
|
|
||||||
border-color: inherit;
|
|
||||||
color: $dark-text;
|
|
||||||
|
|
||||||
.react-select__single-value,
|
|
||||||
.react-select__input {
|
|
||||||
color: $dark-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-select__multi-value {
|
|
||||||
background-color: #fff;
|
|
||||||
color: $dark-text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div.react-select__menu {
|
|
||||||
background-color: #fff;
|
|
||||||
color: $text-color;
|
|
||||||
|
|
||||||
.react-select__option {
|
|
||||||
color: $dark-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-select__option--is-focused {
|
|
||||||
background-color: rgba(167, 182, 194, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stylelint-enable selector-class-pattern */
|
/* stylelint-enable selector-class-pattern */
|
||||||
|
|
||||||
.image-thumbnail {
|
.image-thumbnail {
|
||||||
|
|
|
||||||
|
|
@ -170,10 +170,16 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
color: $dark-text;
|
color: $text-color;
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
.modal-body,
|
.modal-body,
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
background-color: rgb(235, 241, 245);
|
background-color: #30404d;
|
||||||
|
color: $text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue