Scene Marker grid view (#5443)

* add bulk delete mutation
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
dogwithakeyboard 2024-11-29 06:02:20 +00:00 committed by GitHub
parent 6ad0951878
commit 7f8349469a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 482 additions and 31 deletions

View file

@ -300,6 +300,7 @@ type Mutation {
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean! sceneMarkerDestroy(id: ID!): Boolean!
sceneMarkersDestroy(ids: [ID!]!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean! sceneAssignFile(input: AssignSceneFileInput!): Boolean!

View file

@ -814,11 +814,16 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
} }
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
markerID, err := strconv.Atoi(id) return r.SceneMarkersDestroy(ctx, []string{id})
}
func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(markerIDs)
if err != nil { if err != nil {
return false, fmt.Errorf("converting id: %w", err) return false, fmt.Errorf("converting ids: %w", err)
} }
var markers []*models.SceneMarker
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
fileDeleter := &scene.FileDeleter{ fileDeleter := &scene.FileDeleter{
@ -831,35 +836,45 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
qb := r.repository.SceneMarker qb := r.repository.SceneMarker
sqb := r.repository.Scene sqb := r.repository.Scene
marker, err := qb.Find(ctx, markerID) for _, markerID := range ids {
marker, err := qb.Find(ctx, markerID)
if err != nil { if err != nil {
return err return err
}
if marker == nil {
return fmt.Errorf("scene marker with id %d not found", markerID)
}
s, err := sqb.Find(ctx, marker.SceneID)
if err != nil {
return err
}
if s == nil {
return fmt.Errorf("scene with id %d not found", marker.SceneID)
}
markers = append(markers, marker)
if err := scene.DestroyMarker(ctx, s, marker, qb, fileDeleter); err != nil {
return err
}
} }
if marker == nil { return nil
return fmt.Errorf("scene marker with id %d not found", markerID)
}
s, err := sqb.Find(ctx, marker.SceneID)
if err != nil {
return err
}
if s == nil {
return fmt.Errorf("scene with id %d not found", marker.SceneID)
}
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
}); err != nil { }); err != nil {
fileDeleter.Rollback() fileDeleter.Rollback()
return false, err return false, err
} }
// perform the post-commit actions
fileDeleter.Commit() fileDeleter.Commit()
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil) for _, marker := range markers {
r.hookExecutor.ExecutePostHooks(ctx, marker.ID, hook.SceneMarkerDestroyPost, markerIDs, nil)
}
return true, nil return true, nil
} }

View file

@ -8,7 +8,7 @@ fragment SceneMarkerData on SceneMarker {
screenshot screenshot
scene { scene {
id ...SceneMarkerSceneData
} }
primary_tag { primary_tag {
@ -21,3 +21,18 @@ fragment SceneMarkerData on SceneMarker {
name name
} }
} }
fragment SceneMarkerSceneData on Scene {
id
title
files {
width
height
path
}
performers {
id
name
image_path
}
}

View file

@ -47,3 +47,7 @@ mutation SceneMarkerUpdate(
mutation SceneMarkerDestroy($id: ID!) { mutation SceneMarkerDestroy($id: ID!) {
sceneMarkerDestroy(id: $id) sceneMarkerDestroy(id: $id)
} }
mutation SceneMarkersDestroy($ids: [ID!]!) {
sceneMarkersDestroy(ids: $ids)
}

View file

@ -0,0 +1,83 @@
import React, { useState } from "react";
import { useSceneMarkersDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "src/components/Shared/Modal";
import { useToast } from "src/hooks/Toast";
import { useIntl } from "react-intl";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
interface IDeleteSceneMarkersDialogProps {
selected: GQL.SceneMarkerDataFragment[];
onClose: (confirmed: boolean) => void;
}
export const DeleteSceneMarkersDialog: React.FC<
IDeleteSceneMarkersDialogProps
> = (props: IDeleteSceneMarkersDialogProps) => {
const intl = useIntl();
const singularEntity = intl.formatMessage({ id: "marker" });
const pluralEntity = intl.formatMessage({ id: "markers" });
const header = intl.formatMessage(
{ id: "dialogs.delete_object_title" },
{ count: props.selected.length, singularEntity, pluralEntity }
);
const toastMessage = intl.formatMessage(
{ id: "toast.delete_past_tense" },
{ count: props.selected.length, singularEntity, pluralEntity }
);
const message = intl.formatMessage(
{ id: "dialogs.delete_object_desc" },
{ count: props.selected.length, singularEntity, pluralEntity }
);
const Toast = useToast();
const [deleteSceneMarkers] = useSceneMarkersDestroy(
getSceneMarkersDeleteInput()
);
// Network state
const [isDeleting, setIsDeleting] = useState(false);
function getSceneMarkersDeleteInput(): GQL.SceneMarkersDestroyMutationVariables {
return {
ids: props.selected.map((marker) => marker.id),
};
}
async function onDelete() {
setIsDeleting(true);
try {
await deleteSceneMarkers();
Toast.success(toastMessage);
props.onClose(true);
} catch (e) {
Toast.error(e);
props.onClose(false);
}
setIsDeleting(false);
}
return (
<ModalComponent
show
icon={faTrashAlt}
header={header}
accept={{
variant: "danger",
onClick: onDelete,
text: intl.formatMessage({ id: "actions.delete" }),
}}
cancel={{
onClick: () => props.onClose(false),
text: intl.formatMessage({ id: "actions.cancel" }),
variant: "secondary",
}}
isRunning={isDeleting}
>
<p>{message}</p>
</ModalComponent>
);
};
export default DeleteSceneMarkersDialog;

View file

@ -94,7 +94,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
onClick(s.start); onClick(s.start);
} }
if (spriteInfo === null) return null; if (spriteInfo === null || !vttPath) return null;
return ( return (
<div className="preview-scrubber"> <div className="preview-scrubber">

View file

@ -0,0 +1,214 @@
import React, { useEffect, useMemo, useState } from "react";
import { Button, ButtonGroup } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { Icon } from "../Shared/Icon";
import { TagLink } from "../Shared/TagLink";
import { HoverPopover } from "../Shared/HoverPopover";
import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text";
import { ConfigurationContext } from "src/hooks/Config";
import { GridCard, calculateCardWidth } from "../Shared/GridCard/GridCard";
import { faTag } from "@fortawesome/free-solid-svg-icons";
import ScreenUtils from "src/utils/screen";
import { markerTitle } from "src/core/markers";
import { Link } from "react-router-dom";
import { objectTitle } from "src/core/files";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { ScenePreview } from "./SceneCard";
import { TruncatedText } from "../Shared/TruncatedText";
interface ISceneMarkerCardProps {
marker: GQL.SceneMarkerDataFragment;
containerWidth?: number;
previewHeight?: number;
index?: number;
compact?: boolean;
selecting?: boolean;
selected?: boolean | undefined;
zoomIndex?: number;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}
const SceneMarkerCardPopovers = (props: ISceneMarkerCardProps) => {
function maybeRenderPerformerPopoverButton() {
if (props.marker.scene.performers.length <= 0) return;
return (
<PerformerPopoverButton
performers={props.marker.scene.performers}
linkType="scene_marker"
/>
);
}
function renderTagPopoverButton() {
const popoverContent = [
<TagLink
key={props.marker.primary_tag.id}
tag={props.marker.primary_tag}
linkType="scene_marker"
/>,
];
props.marker.tags.map((tag) =>
popoverContent.push(
<TagLink key={tag.id} tag={tag} linkType="scene_marker" />
)
);
return (
<HoverPopover
className="tag-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<Icon icon={faTag} />
<span>{popoverContent.length}</span>
</Button>
</HoverPopover>
);
}
function renderPopoverButtonGroup() {
if (!props.compact) {
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderPerformerPopoverButton()}
{renderTagPopoverButton()}
</ButtonGroup>
</>
);
}
}
return <>{renderPopoverButtonGroup()}</>;
};
const SceneMarkerCardDetails = (props: ISceneMarkerCardProps) => {
return (
<div className="scene-marker-card__details">
<span className="scene-marker-card__time">
{TextUtils.formatTimestampRange(
props.marker.seconds,
props.marker.end_seconds ?? undefined
)}
</span>
<TruncatedText
className="scene-marker-card__scene"
lineCount={3}
text={
<Link to={NavUtils.makeSceneMarkersSceneUrl(props.marker.scene)}>
{objectTitle(props.marker.scene)}
</Link>
}
/>
</div>
);
};
const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => {
const { configuration } = React.useContext(ConfigurationContext);
const file = useMemo(
() =>
props.marker.scene.files.length > 0
? props.marker.scene.files[0]
: undefined,
[props.marker.scene]
);
function isPortrait() {
const width = file?.width ? file.width : 0;
const height = file?.height ? file.height : 0;
return height > width;
}
function maybeRenderSceneSpecsOverlay() {
return (
<div className="scene-specs-overlay">
{props.marker.end_seconds && (
<span className="overlay-duration">
{TextUtils.secondsToTimestamp(
props.marker.end_seconds - props.marker.seconds
)}
</span>
)}
</div>
);
}
return (
<>
<ScenePreview
image={props.marker.screenshot ?? undefined}
video={props.marker.stream ?? undefined}
soundActive={configuration?.interface?.soundOnPreview ?? false}
isPortrait={isPortrait()}
/>
{maybeRenderSceneSpecsOverlay()}
</>
);
};
export const SceneMarkerCard = (props: ISceneMarkerCardProps) => {
const [cardWidth, setCardWidth] = useState<number>();
function zoomIndex() {
if (!props.compact && props.zoomIndex !== undefined) {
return `zoom-${props.zoomIndex}`;
}
return "";
}
useEffect(() => {
if (
!props.containerWidth ||
props.zoomIndex === undefined ||
ScreenUtils.isMobile()
)
return;
let zoomValue = props.zoomIndex;
let preferredCardWidth: number;
switch (zoomValue) {
case 0:
preferredCardWidth = 240;
break;
case 1:
preferredCardWidth = 340; // this value is intentionally higher than 320
break;
case 2:
preferredCardWidth = 480;
break;
case 3:
preferredCardWidth = 640;
}
let fittedCardWidth = calculateCardWidth(
props.containerWidth,
preferredCardWidth!
);
setCardWidth(fittedCardWidth);
}, [props, props.containerWidth, props.zoomIndex]);
return (
<GridCard
className={`scene-marker-card ${zoomIndex()}`}
url={NavUtils.makeSceneMarkerUrl(props.marker)}
title={markerTitle(props.marker)}
width={cardWidth}
linkClassName="scene-marker-card-link"
thumbnailSectionClassName="video-section"
resumeTime={props.marker.seconds}
image={<SceneMarkerCardImage {...props} />}
details={<SceneMarkerCardDetails {...props} />}
popovers={<SceneMarkerCardPopovers {...props} />}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
/>
);
};

View file

@ -0,0 +1,38 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { SceneMarkerCard } from "./SceneMarkerCard";
import { useContainerDimensions } from "../Shared/GridCard/GridCard";
interface ISceneMarkerCardsGrid {
markers: GQL.SceneMarkerDataFragment[];
selectedIds: Set<string>;
zoomIndex: number;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const SceneMarkerCardsGrid: React.FC<ISceneMarkerCardsGrid> = ({
markers,
selectedIds,
zoomIndex,
onSelectChange,
}) => {
const [componentRef, { width }] = useContainerDimensions();
return (
<div className="row justify-content-center" ref={componentRef}>
{markers.map((marker, index) => (
<SceneMarkerCard
key={marker.id}
containerWidth={width}
marker={marker}
index={index}
zoomIndex={zoomIndex}
selecting={selectedIds.size > 0}
selected={selectedIds.has(marker.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(marker.id, selected, shiftKey)
}
/>
))}
</div>
);
};

View file

@ -14,6 +14,8 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
import { MarkerWallPanel } from "../Wall/WallPanel"; import { MarkerWallPanel } from "../Wall/WallPanel";
import { View } from "../List/views"; import { View } from "../List/views";
import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid";
import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog";
function getItems(result: GQL.FindSceneMarkersQueryResult) { function getItems(result: GQL.FindSceneMarkersQueryResult) {
return result?.data?.findSceneMarkers?.scene_markers ?? []; return result?.data?.findSceneMarkers?.scene_markers ?? [];
@ -27,6 +29,7 @@ interface ISceneMarkerList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View; view?: View;
alterQuery?: boolean; alterQuery?: boolean;
defaultSort?: string;
} }
export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
@ -84,7 +87,9 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
function renderContent( function renderContent(
result: GQL.FindSceneMarkersQueryResult, result: GQL.FindSceneMarkersQueryResult,
filter: ListFilterModel filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) { ) {
if (!result.data?.findSceneMarkers) return; if (!result.data?.findSceneMarkers) return;
@ -93,6 +98,29 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
<MarkerWallPanel markers={result.data.findSceneMarkers.scene_markers} /> <MarkerWallPanel markers={result.data.findSceneMarkers.scene_markers} />
); );
} }
if (filter.displayMode === DisplayMode.Grid) {
return (
<SceneMarkerCardsGrid
markers={result.data.findSceneMarkers.scene_markers}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
}
function renderDeleteDialog(
selectedSceneMarkers: GQL.SceneMarkerDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteSceneMarkersDialog
selected={selectedSceneMarkers}
onClose={onClose}
/>
);
} }
return ( return (
@ -104,12 +132,15 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
alterQuery={alterQuery} alterQuery={alterQuery}
filterHook={filterHook} filterHook={filterHook}
view={view} view={view}
selectable
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}
renderContent={renderContent} renderContent={renderContent}
renderDeleteDialog={renderDeleteDialog}
/> />
</ItemListContext> </ItemListContext>
); );

View file

@ -215,6 +215,7 @@ textarea.scene-description {
} }
.scene-card, .scene-card,
.scene-marker-card,
.gallery-card { .gallery-card {
.scene-specs-overlay { .scene-specs-overlay {
transition: opacity 0.5s; transition: opacity 0.5s;
@ -272,7 +273,8 @@ textarea.scene-description {
} }
} }
.scene-card.card { .scene-card.card,
.scene-marker-card.card {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;

View file

@ -110,9 +110,7 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
} }
/> />
<BooleanSetting <BooleanSetting
advanced
id="marker-screenshot-task" id="marker-screenshot-task"
className="sub-setting"
checked={options.markerScreenshots ?? false} checked={options.markerScreenshots ?? false}
disabled={!options.markers} disabled={!options.markers}
headingID="dialogs.scene_gen.marker_screenshots" headingID="dialogs.scene_gen.marker_screenshots"

View file

@ -38,7 +38,7 @@ const CommonLinkComponent: React.FC<ICommonLinkProps> = ({
interface IPerformerLinkProps { interface IPerformerLinkProps {
performer: INamedObject & { disambiguation?: string | null }; performer: INamedObject & { disambiguation?: string | null };
linkType?: "scene" | "gallery" | "image"; linkType?: "scene" | "gallery" | "image" | "scene_marker";
className?: string; className?: string;
} }
@ -55,6 +55,8 @@ export const PerformerLink: React.FC<IPerformerLinkProps> = ({
return NavUtils.makePerformerGalleriesUrl(performer); return NavUtils.makePerformerGalleriesUrl(performer);
case "image": case "image":
return NavUtils.makePerformerImagesUrl(performer); return NavUtils.makePerformerImagesUrl(performer);
case "scene_marker":
return NavUtils.makePerformerSceneMarkersUrl(performer);
case "scene": case "scene":
default: default:
return NavUtils.makePerformerScenesUrl(performer); return NavUtils.makePerformerScenesUrl(performer);
@ -209,7 +211,8 @@ interface ITagLinkProps {
| "details" | "details"
| "performer" | "performer"
| "group" | "group"
| "studio"; | "studio"
| "scene_marker";
className?: string; className?: string;
hoverPlacement?: Placement; hoverPlacement?: Placement;
showHierarchyIcon?: boolean; showHierarchyIcon?: boolean;
@ -238,6 +241,8 @@ export const TagLink: React.FC<ITagLinkProps> = ({
return NavUtils.makeTagImagesUrl(tag); return NavUtils.makeTagImagesUrl(tag);
case "group": case "group":
return NavUtils.makeTagGroupsUrl(tag); return NavUtils.makeTagGroupsUrl(tag);
case "scene_marker":
return NavUtils.makeTagSceneMarkersUrl(tag);
case "details": case "details":
return NavUtils.makeTagUrl(tag.id ?? ""); return NavUtils.makeTagUrl(tag.id ?? "");
} }

View file

@ -1499,6 +1499,24 @@ export const useSceneMarkerDestroy = () =>
}, },
}); });
export const useSceneMarkersDestroy = (
input: GQL.SceneMarkersDestroyMutationVariables
) =>
GQL.useSceneMarkersDestroyMutation({
variables: input,
update(cache, result) {
if (!result.data?.sceneMarkersDestroy) return;
for (const id of input.ids) {
const obj = { __typename: "SceneMarker", id };
cache.evict({ id: cache.identify(obj) });
}
evictTypeFields(cache, sceneMarkerMutationImpactedTypeFields);
evictQueries(cache, sceneMarkerMutationImpactedQueries);
},
});
const galleryMutationImpactedTypeFields = { const galleryMutationImpactedTypeFields = {
Scene: ["galleries"], Scene: ["galleries"],
Performer: ["gallery_count", "performer_count"], Performer: ["gallery_count", "performer_count"],

View file

@ -939,7 +939,7 @@
"marker_image_previews": "Marker Animated Image Previews", "marker_image_previews": "Marker Animated Image Previews",
"marker_image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.", "marker_image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.",
"marker_screenshots": "Marker Screenshots", "marker_screenshots": "Marker Screenshots",
"marker_screenshots_tooltip": "Marker static JPG images, only required if Preview Type is set to Static Image.", "marker_screenshots_tooltip": "Marker static JPG images",
"markers": "Marker Previews", "markers": "Marker Previews",
"markers_tooltip": "20 second videos which begin at the given timecode.", "markers_tooltip": "20 second videos which begin at the given timecode.",
"override_preview_generation_options": "Override Preview Generation Options", "override_preview_generation_options": "Override Preview Generation Options",

View file

@ -18,7 +18,7 @@ const sortByOptions = [
"random", "random",
"scenes_updated_at", "scenes_updated_at",
].map(ListFilterOptions.createSortBy); ].map(ListFilterOptions.createSortBy);
const displayModeOptions = [DisplayMode.Wall]; const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
const criterionOptions = [ const criterionOptions = [
TagsCriterionOption, TagsCriterionOption,
MarkersScenesCriterionOption, MarkersScenesCriterionOption,

View file

@ -30,6 +30,8 @@ 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"; import { galleryTitle } from "src/core/galleries";
import { MarkersScenesCriterion } from "src/models/list-filter/criteria/scenes";
import { objectTitle } from "src/core/files";
function addExtraCriteria( function addExtraCriteria(
dest: Criterion<CriterionValue>[], dest: Criterion<CriterionValue>[],
@ -129,6 +131,20 @@ const makePerformerGroupsUrl = (
return `/groups?${filter.makeQueryParameters()}`; return `/groups?${filter.makeQueryParameters()}`;
}; };
const makePerformerSceneMarkersUrl = (
performer: Partial<GQL.PerformerDataFragment>
) => {
if (!performer.id) return "#";
const filter = new ListFilterModel(GQL.FilterMode.SceneMarkers, undefined);
const criterion = new PerformersCriterion();
criterion.value.items = [
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
];
filter.criteria.push(criterion);
return `/scenes/markers?${filter.makeQueryParameters()}`;
};
const makePerformersCountryUrl = ( const makePerformersCountryUrl = (
performer: Partial<GQL.PerformerDataFragment> performer: Partial<GQL.PerformerDataFragment>
) => { ) => {
@ -429,6 +445,15 @@ const makeSubGroupsUrl = (group: INamedObject) => {
return `/groups?${filter.makeQueryParameters()}`; return `/groups?${filter.makeQueryParameters()}`;
}; };
const makeSceneMarkersSceneUrl = (scene: GQL.SceneMarkerSceneDataFragment) => {
if (!scene.id) return "#";
const filter = new ListFilterModel(GQL.FilterMode.SceneMarkers, undefined);
const criterion = new MarkersScenesCriterion();
criterion.value = [{ id: scene.id, label: objectTitle(scene) }];
filter.criteria.push(criterion);
return `/scenes/markers?${filter.makeQueryParameters()}`;
};
export function handleUnsavedChanges( export function handleUnsavedChanges(
intl: IntlShape, intl: IntlShape,
basepath: string, basepath: string,
@ -449,6 +474,7 @@ const NavUtils = {
makePerformerImagesUrl, makePerformerImagesUrl,
makePerformerGalleriesUrl, makePerformerGalleriesUrl,
makePerformerGroupsUrl, makePerformerGroupsUrl,
makePerformerSceneMarkersUrl,
makePerformersCountryUrl, makePerformersCountryUrl,
makeStudioScenesUrl, makeStudioScenesUrl,
makeStudioImagesUrl, makeStudioImagesUrl,
@ -477,6 +503,7 @@ const NavUtils = {
makeDirectorGroupsUrl, makeDirectorGroupsUrl,
makeContainingGroupsUrl, makeContainingGroupsUrl,
makeSubGroupsUrl, makeSubGroupsUrl,
makeSceneMarkersSceneUrl,
}; };
export default NavUtils; export default NavUtils;