mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Scene Marker grid view (#5443)
* add bulk delete mutation --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
6ad0951878
commit
7f8349469a
16 changed files with 482 additions and 31 deletions
|
|
@ -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!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,6 +836,7 @@ 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
|
||||||
|
|
||||||
|
for _, markerID := range ids {
|
||||||
marker, err := qb.Find(ctx, markerID)
|
marker, err := qb.Find(ctx, markerID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -842,6 +848,7 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := sqb.Find(ctx, marker.SceneID)
|
s, err := sqb.Find(ctx, marker.SceneID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -850,16 +857,24 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||||
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
|
markers = append(markers, marker)
|
||||||
|
|
||||||
|
if err := scene.DestroyMarker(ctx, s, marker, qb, fileDeleter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}); 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
83
ui/v2.5/src/components/Scenes/DeleteSceneMarkersDialog.tsx
Normal file
83
ui/v2.5/src/components/Scenes/DeleteSceneMarkersDialog.tsx
Normal 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;
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
214
ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx
Normal file
214
ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
ui/v2.5/src/components/Scenes/SceneMarkerCardsGrid.tsx
Normal file
38
ui/v2.5/src/components/Scenes/SceneMarkerCardsGrid.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue