Organised flag (#988)

* Add organized boolean to scene model (#729)
* Add organized button to scene page
* Add flag to galleries and images
* Import/export changes
* Make organized flag not null
* Ignore organized scenes for autotag

Co-authored-by: com1234 <com1234@notarealemail.com>
This commit is contained in:
WithoutPants
2020-12-18 08:06:49 +11:00
committed by GitHub
parent 99bd7bc750
commit aadbcaeec2
58 changed files with 543 additions and 81 deletions

View File

@@ -1,4 +1,5 @@
### ✨ New Features
* Add organized flag for scenes, galleries and images.
* Allow configuration of visible navbar items.
### 🎨 Improvements

View File

@@ -28,12 +28,15 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateGalleries] = useBulkGalleryUpdate();
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function makeBulkUpdateIds(
ids: string[],
mode: GQL.BulkUpdateIdMode
@@ -119,6 +122,10 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
galleryInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
if (organized !== undefined) {
galleryInput.organized = organized;
}
return galleryInput;
}
@@ -223,6 +230,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((gallery: GQL.GallerySlimDataFragment) => {
@@ -238,6 +246,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
updateStudioID = GalleriestudioID;
updatePerformerIds = galleryPerformerIDs;
updateTagIds = galleryTagIDs;
updateOrganized = gallery.organized;
first = false;
} else {
if (galleryRating !== updateRating) {
@@ -252,6 +261,9 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
if (!_.isEqual(galleryTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (gallery.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
@@ -264,8 +276,16 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@@ -311,6 +331,16 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<Modal
@@ -359,10 +389,20 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="performers">
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label="Organized"
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</Modal>
);

View File

@@ -104,11 +104,24 @@ export const GalleryCard: React.FC<IProps> = (props) => {
);
}
function maybeRenderOrganized() {
if (props.gallery.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.gallery.scene ||
props.gallery.performers.length > 0 ||
props.gallery.tags.length > 0
props.gallery.tags.length > 0 ||
props.gallery.organized
) {
return (
<>
@@ -117,6 +130,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderScenePopoverButton()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);

View File

@@ -1,10 +1,12 @@
import { Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { useParams, useHistory, Link } from "react-router-dom";
import { useFindGallery } from "src/core/StashService";
import { useFindGallery, useGalleryUpdate } from "src/core/StashService";
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
import { TextUtils } from "src/utils";
import * as Mousetrap from "mousetrap";
import { useToast } from "src/hooks";
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
import { GalleryEditPanel } from "./GalleryEditPanel";
import { GalleryDetailPanel } from "./GalleryDetailPanel";
import { DeleteGalleriesDialog } from "../DeleteGalleriesDialog";
@@ -20,6 +22,7 @@ interface IGalleryParams {
export const Gallery: React.FC = () => {
const { tab = "images", id = "new" } = useParams<IGalleryParams>();
const history = useHistory();
const Toast = useToast();
const isNew = id === "new";
const { data, error, loading } = useFindGallery(id);
@@ -34,6 +37,28 @@ export const Gallery: React.FC = () => {
}
};
const [updateGallery] = useGalleryUpdate();
const [organizedLoading, setOrganizedLoading] = useState(false);
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
await updateGallery({
variables: {
input: {
id: gallery?.id ?? "",
organized: !gallery?.organized,
},
},
});
} catch (e) {
Toast.error(e);
} finally {
setOrganizedLoading(false);
}
};
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
function onDeleteDialogClosed(deleted: boolean) {
@@ -103,7 +128,14 @@ export const Gallery: React.FC = () => {
<Nav.Item>
<Nav.Link eventKey="gallery-edit-panel">Edit</Nav.Link>
</Nav.Item>
<Nav.Item className="ml-auto">{renderOperations()}</Nav.Item>
<Nav.Item className="ml-auto">
<OrganizedButton
loading={organizedLoading}
organized={gallery.organized}
onClick={onOrganizedClick}
/>
</Nav.Item>
<Nav.Item>{renderOperations()}</Nav.Item>
</Nav>
</div>

View File

@@ -28,12 +28,15 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateImages] = useBulkImageUpdate();
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function makeBulkUpdateIds(
ids: string[],
mode: GQL.BulkUpdateIdMode
@@ -119,6 +122,10 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
imageInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
if (organized !== undefined) {
imageInput.organized = organized;
}
return imageInput;
}
@@ -221,6 +228,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((image: GQL.SlimImageDataFragment) => {
@@ -236,6 +244,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
updateStudioID = imageStudioID;
updatePerformerIds = imagePerformerIDs;
updateTagIds = imageTagIDs;
updateOrganized = image.organized;
first = false;
} else {
if (imageRating !== updateRating) {
@@ -250,6 +259,9 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
if (!_.isEqual(imageTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (image.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
@@ -262,8 +274,15 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@@ -309,6 +328,16 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<Modal
@@ -357,10 +386,20 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="performers">
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label="Organized"
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</Modal>
);

View File

@@ -93,11 +93,24 @@ export const ImageCard: React.FC<IImageCardProps> = (
}
}
function maybeRenderOrganized() {
if (props.image.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.image.tags.length > 0 ||
props.image.performers.length > 0 ||
props.image?.o_counter
props.image.o_counter ||
props.image.organized
) {
return (
<>
@@ -106,6 +119,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);

View File

@@ -6,12 +6,14 @@ import {
useImageIncrementO,
useImageDecrementO,
useImageResetO,
useImageUpdate,
} from "src/core/StashService";
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
import * as Mousetrap from "mousetrap";
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
import { ImageFileInfoPanel } from "./ImageFileInfoPanel";
import { ImageEditPanel } from "./ImageEditPanel";
import { ImageDetailPanel } from "./ImageDetailPanel";
@@ -33,10 +35,32 @@ export const Image: React.FC = () => {
const [decrementO] = useImageDecrementO(image?.id ?? "0");
const [resetO] = useImageResetO(image?.id ?? "0");
const [updateImage] = useImageUpdate();
const [organizedLoading, setOrganizedLoading] = useState(false);
const [activeTabKey, setActiveTabKey] = useState("image-details-panel");
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
await updateImage({
variables: {
input: {
id: image?.id ?? "",
organized: !image?.organized,
},
},
});
} catch (e) {
Toast.error(e);
} finally {
setOrganizedLoading(false);
}
};
const onIncrementClick = async () => {
try {
setOLoading(true);
@@ -139,6 +163,13 @@ export const Image: React.FC = () => {
onReset={onResetClick}
/>
</Nav.Item>
<Nav.Item>
<OrganizedButton
loading={organizedLoading}
organized={image.organized}
onClick={onOrganizedClick}
/>
</Nav.Item>
<Nav.Item>{renderOperations()}</Nav.Item>
</Nav>
</div>

View File

@@ -28,12 +28,15 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function makeBulkUpdateIds(
ids: string[],
mode: GQL.BulkUpdateIdMode
@@ -119,6 +122,10 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
if (organized !== undefined) {
sceneInput.organized = organized;
}
return sceneInput;
}
@@ -217,6 +224,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((scene: GQL.SlimSceneDataFragment) => {
@@ -233,6 +241,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
updatePerformerIds = scenePerformerIDs;
updateTagIds = sceneTagIDs;
first = false;
updateOrganized = scene.organized;
} else {
if (sceneRating !== updateRating) {
updateRating = undefined;
@@ -246,6 +255,9 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
if (!_.isEqual(sceneTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (scene.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
@@ -258,8 +270,15 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@@ -305,6 +324,16 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<Modal
@@ -353,10 +382,20 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="performers">
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label="Organized"
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</Modal>
);

View File

@@ -267,6 +267,18 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}
}
function maybeRenderOrganized() {
if (props.scene.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.scene.tags.length > 0 ||
@@ -274,7 +286,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 ||
props.scene?.o_counter ||
props.scene.gallery
props.scene.gallery ||
props.scene.organized
) {
return (
<>
@@ -286,6 +299,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);

View File

@@ -0,0 +1,40 @@
import React from "react";
import cx from "classnames";
import { Button, Spinner } from "react-bootstrap";
import { Icon } from "src/components/Shared";
import { defineMessages, useIntl } from "react-intl";
export interface IOrganizedButtonProps {
loading: boolean;
organized: boolean;
onClick: () => void;
}
export const OrganizedButton: React.FC<IOrganizedButtonProps> = (
props: IOrganizedButtonProps
) => {
const intl = useIntl();
const messages = defineMessages({
organized: {
id: "organized",
defaultMessage: "Organized",
},
});
if (props.loading) return <Spinner animation="border" role="status" />;
return (
<Button
variant="secondary"
title={intl.formatMessage(messages.organized)}
className={cx(
"minimal",
"organized-button",
props.organized ? "organized" : "not-organized"
)}
onClick={props.onClick}
>
<Icon icon="box" />
</Button>
);
};

View File

@@ -10,6 +10,7 @@ import {
useSceneResetO,
useSceneStreams,
useSceneGenerateScreenshot,
useSceneUpdate,
} from "src/core/StashService";
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
@@ -26,6 +27,7 @@ import { SceneMoviePanel } from "./SceneMoviePanel";
import { DeleteScenesDialog } from "../DeleteScenesDialog";
import { SceneGenerateDialog } from "../SceneGenerateDialog";
import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel";
import { OrganizedButton } from "./OrganizedButton";
interface ISceneParams {
id?: string;
@@ -36,6 +38,7 @@ export const Scene: React.FC = () => {
const location = useLocation();
const history = useHistory();
const Toast = useToast();
const [updateScene] = useSceneUpdate();
const [generateScreenshot] = useSceneGenerateScreenshot();
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
const [collapsed, setCollapsed] = useState(false);
@@ -52,6 +55,8 @@ export const Scene: React.FC = () => {
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
const [resetO] = useSceneResetO(scene?.id ?? "0");
const [organizedLoading, setOrganizedLoading] = useState(false);
const [activeTabKey, setActiveTabKey] = useState("scene-details-panel");
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
@@ -69,6 +74,24 @@ export const Scene: React.FC = () => {
);
}
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
await updateScene({
variables: {
input: {
id: scene?.id ?? "",
organized: !scene?.organized,
},
},
});
} catch (e) {
Toast.error(e);
} finally {
setOrganizedLoading(false);
}
};
const onIncrementClick = async () => {
try {
setOLoading(true);
@@ -246,6 +269,13 @@ export const Scene: React.FC = () => {
onReset={onResetClick}
/>
</Nav.Item>
<Nav.Item>
<OrganizedButton
loading={organizedLoading}
organized={scene.organized}
onClick={onOrganizedClick}
/>
</Nav.Item>
<Nav.Item>{renderOperations()}</Nav.Item>
</ButtonGroup>
</Nav>

View File

@@ -70,7 +70,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
// Network state
const [isLoading, setIsLoading] = useState(true);
const [updateScene] = useSceneUpdate(getSceneInput());
const [updateScene] = useSceneUpdate();
useEffect(() => {
if (props.isVisible) {
@@ -230,7 +230,11 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
async function onSave() {
setIsLoading(true);
try {
const result = await updateScene();
const result = await updateScene({
variables: {
input: getSceneInput(),
},
});
if (result.data?.sceneUpdate) {
Toast.success({ content: "Updated scene" });
}

View File

@@ -535,3 +535,13 @@ input[type="range"].blue-slider {
}
}
}
.organized-button {
&.not-organized {
color: rgba(191, 204, 214, 0.5);
}
&.organized {
color: #664c3f;
}
}