mirror of
https://github.com/stashapp/stash.git
synced 2026-02-08 08:21:32 +01:00
Added loop feature for markers + AB prefill (#6510)
* add loop feature for markers + AB prefill * chore(ui): type ab loop plugin access
This commit is contained in:
parent
b76edffc5d
commit
cf5d60f511
5 changed files with 126 additions and 12 deletions
|
|
@ -1,7 +1,27 @@
|
|||
import videojs from "video.js";
|
||||
import videojs, { VideoJsPlayer } from "video.js";
|
||||
|
||||
export const VIDEO_PLAYER_ID = "VideoJsPlayer";
|
||||
|
||||
export const getPlayer = () => videojs.getPlayer(VIDEO_PLAYER_ID);
|
||||
|
||||
export const getPlayerPosition = () => getPlayer()?.currentTime();
|
||||
|
||||
export type AbLoopOptions = {
|
||||
start: number;
|
||||
end: number | false;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type AbLoopPluginApi = {
|
||||
getOptions: () => AbLoopOptions;
|
||||
setOptions: (options: AbLoopOptions) => void;
|
||||
};
|
||||
|
||||
export const getAbLoopPlugin = () => {
|
||||
const player = getPlayer();
|
||||
if (!player) return null;
|
||||
const { abLoopPlugin } = player as VideoJsPlayer & {
|
||||
abLoopPlugin?: AbLoopPluginApi;
|
||||
};
|
||||
return abLoopPlugin ?? null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,18 +4,24 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { Button, Badge, Card } from "react-bootstrap";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { markerTitle } from "src/core/markers";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IPrimaryTags {
|
||||
sceneMarkers: GQL.SceneMarkerDataFragment[];
|
||||
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
onLoopMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
onEdit: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
}
|
||||
|
||||
export const PrimaryTags: React.FC<IPrimaryTags> = ({
|
||||
sceneMarkers,
|
||||
onClickMarker,
|
||||
onLoopMarker,
|
||||
onEdit,
|
||||
}) => {
|
||||
const { configuration } = useConfigurationContext();
|
||||
const showAbLoopControls = configuration?.ui?.showAbLoopControls;
|
||||
|
||||
if (!sceneMarkers?.length) return <div />;
|
||||
|
||||
const primaryTagNames: Record<string, string> = {};
|
||||
|
|
@ -52,10 +58,21 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
|
|||
<FormattedMessage id="actions.edit" />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{TextUtils.formatTimestampRange(
|
||||
marker.seconds,
|
||||
marker.end_seconds ?? undefined
|
||||
<div className="d-flex align-items-center">
|
||||
<div>
|
||||
{TextUtils.formatTimestampRange(
|
||||
marker.seconds,
|
||||
marker.end_seconds ?? undefined
|
||||
)}
|
||||
</div>
|
||||
{showAbLoopControls && marker.end_seconds != null && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="ml-2 p-0"
|
||||
onClick={() => onLoopMarker(marker)}
|
||||
>
|
||||
Loop
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-section centered">{tags}</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||
import Mousetrap from "mousetrap";
|
||||
import { OrganizedButton } from "./OrganizedButton";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { getPlayerPosition } from "src/components/ScenePlayer/util";
|
||||
import {
|
||||
getAbLoopPlugin,
|
||||
getPlayerPosition,
|
||||
} from "src/components/ScenePlayer/util";
|
||||
import {
|
||||
faEllipsisV,
|
||||
faChevronRight,
|
||||
|
|
@ -311,9 +314,53 @@ const ScenePage: React.FC<IProps> = PatchComponent("ScenePage", (props) => {
|
|||
};
|
||||
|
||||
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
||||
const abLoopPlugin = getAbLoopPlugin();
|
||||
const opts = abLoopPlugin?.getOptions();
|
||||
const start = opts?.start;
|
||||
const end = opts?.end;
|
||||
|
||||
const hasLoopRange =
|
||||
opts?.enabled &&
|
||||
typeof start === "number" &&
|
||||
typeof end === "number" &&
|
||||
Number.isFinite(start) &&
|
||||
Number.isFinite(end);
|
||||
|
||||
if (
|
||||
abLoopPlugin &&
|
||||
opts &&
|
||||
hasLoopRange &&
|
||||
(marker.seconds < Math.min(start as number, end as number) ||
|
||||
marker.seconds > Math.max(start as number, end as number))
|
||||
) {
|
||||
abLoopPlugin.setOptions({
|
||||
...opts,
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
setTimestamp(marker.seconds);
|
||||
}
|
||||
|
||||
function onLoopMarker(marker: GQL.SceneMarkerDataFragment) {
|
||||
if (marker.end_seconds == null) return;
|
||||
|
||||
setTimestamp(marker.seconds);
|
||||
const start = Math.min(marker.seconds, marker.end_seconds);
|
||||
const end = Math.max(marker.seconds, marker.end_seconds);
|
||||
const abLoopPlugin = getAbLoopPlugin();
|
||||
const opts = abLoopPlugin?.getOptions();
|
||||
|
||||
if (opts && abLoopPlugin) {
|
||||
abLoopPlugin.setOptions({
|
||||
...opts,
|
||||
start,
|
||||
end,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function onRescan() {
|
||||
await mutateMetadataScan({
|
||||
paths: [objectPath(scene)],
|
||||
|
|
@ -561,6 +608,7 @@ const ScenePage: React.FC<IProps> = PatchComponent("ScenePage", (props) => {
|
|||
<SceneMarkersPanel
|
||||
sceneId={scene.id}
|
||||
onClickMarker={onClickMarker}
|
||||
onLoopMarker={onLoopMarker}
|
||||
isVisible={activeTabKey === "scene-markers-panel"}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import {
|
|||
} from "src/core/StashService";
|
||||
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||
import { MarkerTitleSuggest } from "src/components/Shared/Select";
|
||||
import { getPlayerPosition } from "src/components/ScenePlayer/util";
|
||||
import {
|
||||
getAbLoopPlugin,
|
||||
getPlayerPosition,
|
||||
} from "src/components/ScenePlayer/util";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { formikUtils } from "src/utils/form";
|
||||
|
|
@ -61,16 +64,39 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||
});
|
||||
|
||||
// useMemo to only run getPlayerPosition when the input marker actually changes
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
const initialValues = useMemo(() => {
|
||||
if (!marker) {
|
||||
const abLoopPlugin = getAbLoopPlugin();
|
||||
const opts = abLoopPlugin?.getOptions();
|
||||
const start = opts?.start;
|
||||
const end = opts?.end;
|
||||
const hasAbLoop = Number.isFinite(start);
|
||||
|
||||
if (hasAbLoop) {
|
||||
const current = Math.round(getPlayerPosition() ?? 0);
|
||||
const rawEnd =
|
||||
Number.isFinite(end) && (end as number) > 0 ? (end as number) : null;
|
||||
const endSeconds =
|
||||
rawEnd !== null ? rawEnd : Math.max(start as number, current);
|
||||
|
||||
return {
|
||||
title: "",
|
||||
seconds: start as number,
|
||||
end_seconds: endSeconds,
|
||||
primary_tag_id: "",
|
||||
tag_ids: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: marker?.title ?? "",
|
||||
seconds: marker?.seconds ?? Math.round(getPlayerPosition() ?? 0),
|
||||
end_seconds: marker?.end_seconds ?? null,
|
||||
primary_tag_id: marker?.primary_tag.id ?? "",
|
||||
tag_ids: marker?.tags.map((tag) => tag.id) ?? [],
|
||||
}),
|
||||
[marker]
|
||||
);
|
||||
};
|
||||
}, [marker]);
|
||||
|
||||
type InputValues = yup.InferType<typeof schema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ interface ISceneMarkersPanelProps {
|
|||
sceneId: string;
|
||||
isVisible: boolean;
|
||||
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
onLoopMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
}
|
||||
|
||||
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = ({
|
||||
sceneId,
|
||||
isVisible,
|
||||
onClickMarker,
|
||||
onLoopMarker,
|
||||
}) => {
|
||||
const { data, loading } = GQL.useFindSceneMarkerTagsQuery({
|
||||
variables: { id: sceneId },
|
||||
|
|
@ -70,6 +72,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = ({
|
|||
<PrimaryTags
|
||||
sceneMarkers={sceneMarkers}
|
||||
onClickMarker={onClickMarker}
|
||||
onLoopMarker={onLoopMarker}
|
||||
onEdit={onOpenEditor}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue