diff --git a/app/components/ConfirmationModal.tsx b/app/components/ConfirmationModal.tsx new file mode 100644 index 0000000000..abcdd1040a --- /dev/null +++ b/app/components/ConfirmationModal.tsx @@ -0,0 +1,89 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Button } from "~/components/Button"; +import { Fragment } from "react"; + +interface Props { + text: string; + title: string; + open: boolean; + onClose: () => void; + onOk?: () => void; + okText?: string; + okColor?: string; + onCancel?: () => void; + cancelText?: string; +} + +export const ConfirmationModal = ({ + text, + title, + open, + onClose, + onOk, + okText = "OK", + okColor = "bg-brand-success", + onCancel, + cancelText = "Cancel", +}: Props) => { + return ( + + + + + + + + + + {title} + + + {text} + + + {!!onCancel && ( + + {cancelText} + + )} + {!!onOk && ( + + {okText} + + )} + + + + + + + ); +}; diff --git a/app/modules/LowerPanel/LowerPanel.tsx b/app/modules/LowerPanel/LowerPanel.tsx index 680a097953..669cf4b086 100644 --- a/app/modules/LowerPanel/LowerPanel.tsx +++ b/app/modules/LowerPanel/LowerPanel.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import React, { useCallback, useContext } from "react"; +import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; interface LowerPanelPropsI { timelineHeight: number; @@ -6,10 +7,22 @@ interface LowerPanelPropsI { } export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => { - const [open, setOpen] = useState(false); - const handleOpen = () => { - setOpen(true); - }; + // const [open, setOpen] = useState(false); + const { setOverTimeline, updateCurrentTime, scale } = + useContext(TrackContext); + + const onTimelineClick = useCallback( + (event: React.PointerEvent) => { + if (event.button === 0) { + updateCurrentTime((Math.round(event.clientX) - 92) / 4 / scale); + } + }, + [updateCurrentTime, scale], + ); + + // const handleOpen = () => { + // setOpen(true); + // }; return ( { "bg-ui-panel", ].join(" ")} style={{ height: timelineHeight }} + onPointerOver={() => { + setOverTimeline(true); + }} + onPointerLeave={() => setOverTimeline(false)} + onPointerDown={onTimelineClick} > {children} diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx index 747d64434d..c8f6e6fba5 100644 --- a/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx +++ b/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx @@ -1,5 +1,5 @@ import { useCallback, useContext, useEffect, useState } from "react"; -import { BaseClip } from "~/models/track"; +import { BaseClip } from "~/pages/PageEnigma/models/track"; import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; interface Props { @@ -8,8 +8,16 @@ interface Props { } export const ClipElement = ({ clip, type }: Props) => { - const { startDrag, dragId, endDrag, scale, canDrop, setCanDrop } = - useContext(TrackContext); + const { + startDrag, + dragId, + endDrag, + scale, + canDrop, + setCanDrop, + overTimeline, + dropId, + } = useContext(TrackContext); const [initPosition, setInitPosition] = useState<{ initX: number; initY: number; @@ -36,6 +44,8 @@ export const ClipElement = ({ clip, type }: Props) => { const onMouseMove = (event: MouseEvent) => { if (dragId) { + event.stopPropagation(); + event.preventDefault(); const deltaX = event.clientX - initX; const deltaY = event.clientY - initY; setCurrPosition({ x: deltaX, y: deltaY }); @@ -55,20 +65,24 @@ export const ClipElement = ({ clip, type }: Props) => { const onPointerDown = useCallback( (event: React.PointerEvent) => { if (event.button === 0) { + const box = document.getElementById(`ani-clip-${clip.id}`); + const position = box!.getBoundingClientRect(); + startDrag(type, clip.id); setCurrPosition({ - x: 0, - y: 0, + x: position.x, + y: position.y + position.height + 1, + }); + setInitPosition({ + initX: position.x, + initY: position.y + position.height + 1, }); - setInitPosition({ initY: event.clientY, initX: event.clientX }); setCanDrop(false); } }, [type, clip.id, startDrag, setCanDrop], ); - // console.log("clip", canDrop); - return ( { diff --git a/app/pages/PageEnigma/comps/Timeline/Audio.tsx b/app/pages/PageEnigma/comps/Timeline/Audio.tsx index 140cd051de..ae652ec4e8 100644 --- a/app/pages/PageEnigma/comps/Timeline/Audio.tsx +++ b/app/pages/PageEnigma/comps/Timeline/Audio.tsx @@ -17,11 +17,12 @@ export const Audio = () => { diff --git a/app/pages/PageEnigma/comps/Timeline/Camera.tsx b/app/pages/PageEnigma/comps/Timeline/Camera.tsx index 164097b445..f09e8a7d71 100644 --- a/app/pages/PageEnigma/comps/Timeline/Camera.tsx +++ b/app/pages/PageEnigma/comps/Timeline/Camera.tsx @@ -14,6 +14,7 @@ export const Camera = () => { Camera { { type="animations" /> { type="positions" /> { {objects.objects.map((object) => ( { - const { characters, objects, scale, length } = useContext(TrackContext); + const { + characters, + objects, + scale, + length, + selectedClip, + currentTime, + deleteCharacterClip, + deleteAudioClip, + deleteCameraClip, + deleteObjectClip, + } = useContext(TrackContext); const { onPointerDown, time } = useMouseEventsAnimation(); + const [dialogOpen, setDialogOpen] = useState(false); const sectionWidth = 60 * 4 * scale; const fullHeight = - characters.length * 200 + objects.objects.length * 80 + 248 + 96; + characters.length * 268 + objects.objects.length * 60 + 300 + 96; + + const onDeleteAsk = useCallback( + (event: KeyboardEvent) => { + if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedClip) { + setDialogOpen(true); + } + }, + [selectedClip], + ); + + const displayTime = time === -1 ? currentTime : time; + + const onDelete = useCallback(() => { + deleteCharacterClip(selectedClip!); + deleteCameraClip(selectedClip!); + deleteAudioClip(selectedClip!); + deleteObjectClip(selectedClip!); + }, [ + selectedClip, + deleteAudioClip, + deleteCharacterClip, + deleteCameraClip, + deleteObjectClip, + ]); + + useEffect(() => { + document.addEventListener("keydown", onDeleteAsk); + + return () => { + document.addEventListener("keydown", onDeleteAsk); + }; + }, [onDeleteAsk]); return ( <> @@ -84,7 +129,7 @@ export const Timeline = ({ timelineHeight }: Props) => { { /> + setDialogOpen(false)} + onOk={() => { + onDelete(); + setDialogOpen(false); + }} + okText="Delete" + okColor="bg-brand-primary" + onCancel={() => setDialogOpen(false)} + /> > ); }; diff --git a/app/pages/PageEnigma/comps/Timeline/Track.tsx b/app/pages/PageEnigma/comps/Timeline/Track.tsx index 22070fd1d4..5290d67c7a 100644 --- a/app/pages/PageEnigma/comps/Timeline/Track.tsx +++ b/app/pages/PageEnigma/comps/Timeline/Track.tsx @@ -1,11 +1,12 @@ import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons"; -import { BaseClip } from "~/models/track"; -import { useContext } from "react"; +import { BaseClip } from "~/pages/PageEnigma/models/track"; +import { PointerEvent, useContext } from "react"; import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; interface Props { + id: string; clips: BaseClip[]; title: string; style: "character" | "audio" | "camera" | "objects"; @@ -16,6 +17,7 @@ interface Props { } export const Track = ({ + id, clips, toggleMute, updateClip, @@ -24,7 +26,8 @@ export const Track = ({ style, type, }: Props) => { - const { length, setCanDrop, dragType } = useContext(TrackContext); + const { length, scale, setCanDrop, dragType, setDropId, setDropOffset } = + useContext(TrackContext); const trackType = type ?? style; function onPointerOver() { @@ -32,6 +35,19 @@ export const Track = ({ return; } setCanDrop(true); + setDropId(id); + } + + function onPointerMove(event: PointerEvent) { + if (dragType !== trackType) { + return; + } + const track = document.getElementById(`track-${trackType}-${id}`); + if (!track) { + return; + } + const position = track.getBoundingClientRect(); + setDropOffset((event.clientX - position.x) / 4 / scale); } function onPointerLeave() { if (dragType !== trackType) { @@ -43,9 +59,11 @@ export const Track = ({ return ( {clips.map((clip, index) => ( { "rounded-l-lg", "block h-full", clip.id === selectedClip - ? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white" + ? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0" : "", ].join(" ")} style={{ width: 15, left: offset * 4 * scale, cursor: "w-resize" }} @@ -63,7 +63,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => { ...classes, "block h-full", clip.id === selectedClip - ? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white" + ? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0" : "", ].join(" ")} style={{ @@ -80,7 +80,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => { "rounded-r-lg", "block h-full", clip.id === selectedClip - ? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white" + ? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0" : "", ].join(" ")} style={{ diff --git a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsAnimation.ts b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsAnimation.ts index 131cd4740a..f525bd2a0a 100644 --- a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsAnimation.ts +++ b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsAnimation.ts @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useState } from "react"; import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; export const useMouseEventsAnimation = () => { @@ -7,7 +7,7 @@ export const useMouseEventsAnimation = () => { const [isActive, setIsActive] = useState(""); const [clientX, setClientX] = useState(0); - const [time, setTime] = useState(currentTime); + const [time, setTime] = useState(-1); useEffect(() => { const max = length * 60 * 4 * scale; @@ -16,12 +16,15 @@ export const useMouseEventsAnimation = () => { if (isActive) { updateCurrentTime(Math.round(time)); setIsActive(""); + setTime(-1); } }; const onMouseMove = (event: MouseEvent) => { const delta = (event.clientX - clientX) / 4 / scale + currentTime; if (isActive === "drag") { + event.stopPropagation(); + event.preventDefault(); if (delta < 0 || delta > max) { return; } @@ -37,15 +40,16 @@ export const useMouseEventsAnimation = () => { window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointermove", onMouseMove); }; - }, [clientX, isActive, length, updateCurrentTime, scale, time]); + }, [clientX, isActive, length, updateCurrentTime, scale, time, currentTime]); return { - onPointerDown: (event: React.PointerEvent) => { + onPointerDown: useCallback((event: React.PointerEvent) => { if (event.button === 0) { setClientX(event.clientX); setIsActive("drag"); + setTime(currentTime); } - }, + }, []), time, }; }; diff --git a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts index cad387f83b..35c82d795d 100644 --- a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts +++ b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts @@ -5,7 +5,7 @@ import React, { useEffect, useRef, } from "react"; -import { BaseClip } from "~/models/track"; +import { BaseClip } from "~/pages/PageEnigma/models/track"; import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; export const useMouseEventsClip = ( @@ -89,6 +89,8 @@ export const useMouseEventsClip = ( type: string, ) => { if (event.button === 0) { + event.stopPropagation(); + event.preventDefault(); clientX.current = event.clientX; isActive.current = type; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx index aa744a0f7b..9fddae7aa2 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx @@ -3,78 +3,155 @@ import { AnimationClip, AudioClip, AudioGroup, + BaseClip, CameraGroup, CharacterGroup, ObjectGroup, -} from "~/models/track"; +} from "~/pages/PageEnigma/models/track"; export const TrackContext = createContext<{ + // timeline characters and it's functions characters: CharacterGroup[]; - camera: CameraGroup | null; - audio: AudioGroup | null; - objects: ObjectGroup; - selectedClip: string | null; updateCharacters: (options: { type: "animations" | "positions" | "lipSync"; id: string; length: number; offset: number; }) => void; + toggleLipSyncMute: (characterId: string) => void; + addCharacterAnimation: (options: { + clipId: string; + characterId: string; + animationClips: BaseClip[]; + offset: number; + }) => void; + addCharacterAudio: (options: { + clipId: string; + characterId: string; + audioClips: BaseClip[]; + offset: number; + }) => void; + selectCharacterClip: (clipId: string) => void; + deleteCharacterClip: (clipId: string) => void; + + // timeline camera group + camera: CameraGroup | null; updateCamera: (options: { id: string; length: number; offset: number; }) => void; + selectCameraClip: (clipId: string) => void; + deleteCameraClip: (clipId: string) => void; + + // timeline global audio group + audio: AudioGroup | null; updateAudio: (options: { id: string; length: number; offset: number; }) => void; + toggleAudioMute: () => void; + addGlobalAudio: ( + clipId: string, + animationClips: BaseClip[], + offset: number, + ) => void; + selectAudioClip: (clipId: string) => void; + deleteAudioClip: (clipId: string) => void; + + // timeline objects group + objects: ObjectGroup; updateObject: (options: { id: string; length: number; offset: number; }) => void; + selectObjectClip: (clipId: string) => void; + deleteObjectClip: (clipId: string) => void; + + // current - only select one item - will be replaced + selectedClip: string | null; selectClip: (clipId: string) => void; - toggleLipSyncMute: (characterId: string) => void; - toggleAudioMute: () => void; + + // sidebar clips animationClips: AnimationClip[]; audioClips: AudioClip[]; + + // drag and drop dragType: "animations" | "lipSync" | null; dragId: string | null; startDrag: (type: "animations" | "lipSync", id: string) => void; endDrag: () => void; - scale: number; - currentTime: number; - length: number; - updateCurrentTime: (time: number) => void; canDrop: boolean; setCanDrop: (can: boolean) => void; + overTimeline: boolean; + setOverTimeline: (over: boolean) => void; + dropId: string; + setDropId: (id: string) => void; + dropOffset: number; + setDropOffset: (offset: number) => void; + + // scale of timeline displa + scale: number; + + // current time position + currentTime: number; + updateCurrentTime: (time: number) => void; + + // total length of the film clip + length: number; + + // computed width length * 60 * 4 * scale fullWidth: number; }>({ characters: [], - camera: null, - audio: null, - objects: { id: "", objects: [] }, - selectedClip: null, updateCharacters: () => {}, - updateCamera: () => {}, - updateAudio: () => {}, - updateObject: () => {}, - selectClip: () => {}, toggleLipSyncMute: () => {}, + addCharacterAnimation: () => {}, + addCharacterAudio: () => {}, + selectCharacterClip: () => {}, + deleteCharacterClip: () => {}, + + camera: null, + updateCamera: () => {}, + selectCameraClip: () => {}, + deleteCameraClip: () => {}, + + audio: null, + updateAudio: () => {}, toggleAudioMute: () => {}, + selectAudioClip: () => {}, + addGlobalAudio: () => {}, + deleteAudioClip: () => {}, + + objects: { id: "", objects: [] }, + updateObject: () => {}, + selectObjectClip: () => {}, + deleteObjectClip: () => {}, + + selectedClip: null, + selectClip: () => {}, + animationClips: [], audioClips: [], + dragType: null, dragId: null, startDrag: () => {}, endDrag: () => {}, - scale: 1, - currentTime: 0, - length: 12, - updateCurrentTime: () => {}, + dropId: "", + setDropId: () => {}, canDrop: false, setCanDrop: () => {}, + overTimeline: false, + setOverTimeline: () => {}, + dropOffset: 0, + setDropOffset: () => {}, + + scale: 1, + currentTime: 0, + updateCurrentTime: () => {}, + length: 12, fullWidth: 0, }); diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx index ecdcd15063..4ef71835e5 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx @@ -4,50 +4,56 @@ import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/ import useUpdateCamera from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera"; import useUpdateAudio from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio"; import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject"; -import { AnimationClip, AudioClip } from "~/models/track"; +import { AnimationClip, AudioClip } from "~/pages/PageEnigma/models/track"; +import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop"; interface Props { children: ReactNode; } export const TrackProvider = ({ children }: Props) => { - const { characters, updateCharacters, toggleLipSyncMute } = - useUpdateCharacters(); - const { camera, updateCamera } = useUpdateCamera(); - const { audio, updateAudio, toggleAudioMute } = useUpdateAudio(); - const { objects, updateObject } = useUpdateObject(); - const [selectedClip, setSelectedClip] = useState(null); + const characters = useUpdateCharacters(); + const camera = useUpdateCamera(); + const audio = useUpdateAudio(); + const objects = useUpdateObject(); + const { endDrag, ...dragDrop } = useUpdateDragDrop(); + + const [selectedClip, setSelectedClip] = useState(null); const selectClip = useCallback((clipId: string | null) => { setSelectedClip(clipId); }, []); - const [state, setState] = useState<{ - dragType: "animations" | "lipSync" | null; - dragId: string | null; - }>({ dragType: null, dragId: null }); const [animationClips, setAnimationClips] = useState([]); const [audioClips, setAudioClips] = useState([]); - const startDrag = useCallback( - (type: "animations" | "lipSync", id: string) => { - setState({ dragId: id, dragType: type }); - }, - [], - ); - - const endDrag = useCallback(() => { - setState({ dragId: null, dragType: null }); - }, []); - + const [scale, setScale] = useState(1); + const [length, setLength] = useState(1); const [time, setTime] = useState(0); const updateCurrentTime = useCallback((newTime: number) => { setTime(newTime); + console.log("message", { + action: "UpdateCurrentTime", + id: "", + data: { currentTime: newTime }, + }); }, []); - const [canDrop, setCanDrop] = useState(false); - const [scale, setScale] = useState(1); - const [length, setLength] = useState(1); + // cross group functions + const dropClip = useCallback(() => { + const { canDrop, dragType, dropId, dragId, dropOffset } = dragDrop; + if (canDrop) { + if (dragType === "animations") { + characters.addCharacterAnimation({ + clipId: dragId!, + characterId: dropId, + animationClips, + offset: dropOffset, + }); + } + } + endDrag(); + }, [dragDrop, animationClips, characters, endDrag]); const fullWidth = useMemo(() => { return length * 60 * 4 * scale; @@ -69,54 +75,42 @@ export const TrackProvider = ({ children }: Props) => { const values = useMemo(() => { return { - characters, - camera, - audio, - objects, - updateCharacters, - updateCamera, - updateAudio, - updateObject, + ...characters, + ...camera, + ...audio, + ...objects, + selectClip, selectedClip, - toggleLipSyncMute, - toggleAudioMute, - dragType: state.dragType, - dragId: state.dragId, + + ...dragDrop, + endDrag: dropClip, + animationClips, audioClips, - startDrag, - endDrag, + scale, currentTime: time, - length, updateCurrentTime, - canDrop, - setCanDrop, + length, fullWidth, }; }, [ characters, - updateCharacters, + camera, + audio, + objects, + selectClip, selectedClip, - camera, - updateCamera, - audio, - updateAudio, - objects, - updateObject, - toggleLipSyncMute, - toggleAudioMute, - state.dragId, - state.dragType, + + dragDrop, + dropClip, + animationClips, audioClips, - startDrag, - endDrag, - canDrop, + updateCurrentTime, - setCanDrop, time, fullWidth, length, diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts index e366ba419a..2052926c16 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { AudioGroup } from "~/models/track"; +import { AudioGroup, BaseClip } from "~/pages/PageEnigma/models/track"; export default function useUpdateAudio() { const [audio, setAudio] = useState({ @@ -45,6 +45,33 @@ export default function useUpdateAudio() { clips: newClips, }; }); + console.log("message", { + action: "UpdateGlobalAudio", + id, + data: { offset, length }, + }); + }, + [], + ); + + const addGlobalAudio = useCallback( + (dragId: string, audioClips: BaseClip[], offset: number) => { + const clip = audioClips.find((row) => row.id === dragId); + if (!clip) { + return; + } + + setAudio((oldAudio) => { + return { + ...oldAudio, + clips: [...oldAudio.clips, { ...clip, offset }], + }; + }); + console.log("message", { + action: "UpdateGlobalAudio", + id: dragId, + data: { offset }, + }); }, [], ); @@ -56,11 +83,47 @@ export default function useUpdateAudio() { muted: !oldAudio.muted, }; }); + console.log("message", { + action: "ToggleGlobalAudioMute", + id: "", + }); + }, []); + + const selectAudioClip = useCallback((clipId: string) => { + setAudio((oldAudio) => { + return { + ...oldAudio, + clips: [ + ...oldAudio.clips.map((clip) => { + return { + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + }; + }), + ], + }; + }); + }, []); + + const deleteAudioClip = useCallback((clipId: string) => { + setAudio((oldAudio) => { + return { + ...oldAudio, + clips: [...oldAudio.clips.filter((clip) => clip.id !== clipId)], + }; + }); + console.log("message", { + action: "UpdateGlobalAudio", + id: clipId, + }); }, []); return { audio, updateAudio, toggleAudioMute, + selectAudioClip, + addGlobalAudio, + deleteAudioClip, }; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts index f0d3f59c9f..b5b7d21da6 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { CameraGroup } from "~/models/track"; +import { CameraGroup } from "~/pages/PageEnigma/models/track"; export default function useUpdateCamera() { const [camera, setCamera] = useState({ @@ -48,8 +48,35 @@ export default function useUpdateCamera() { [], ); + const selectCameraClip = useCallback((clipId: string) => { + setCamera((oldCamera) => { + return { + ...oldCamera, + clips: [ + ...oldCamera.clips.map((clip) => { + return { + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + }; + }), + ], + }; + }); + }, []); + + const deleteCameraClip = useCallback((clipId: string) => { + setCamera((oldCamera) => { + return { + ...oldCamera, + clips: [...oldCamera.clips.filter((clip) => clip.id !== clipId)], + }; + }); + }, []); + return { camera, updateCamera, + selectCameraClip, + deleteCameraClip, }; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts index 60f013df55..546e3664cc 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts @@ -1,39 +1,13 @@ import { useCallback, useRef, useState } from "react"; -import { CharacterGroup } from "~/models/track"; +import { BaseClip, CharacterGroup } from "~/pages/PageEnigma/models/track"; export default function useUpdateCharacters() { const [characters, setCharacters] = useState([ { id: "CH1", muted: false, - animationClips: [ - { - id: "CH1-A1", - length: 200, - offset: 0, - name: "ani 1", - }, - { - id: "CH1-A2", - length: 180, - offset: 300, - name: "ani 2", - }, - ], - positionClips: [ - { - id: "CH1-P1", - length: 200, - offset: 100, - name: "pos 1", - }, - { - id: "CH1-P2", - length: 180, - offset: 500, - name: "pos 2", - }, - ], + animationClips: [], + positionClips: [], lipSyncClips: [ { id: "CH1-L1", @@ -75,6 +49,11 @@ export default function useUpdateCharacters() { }; }); }); + console.log("message", { + action: "UpdateAnimation", + id, + data: { offset, length }, + }); return; } if (type === "positions") { @@ -115,11 +94,90 @@ export default function useUpdateCharacters() { }; }); }); + console.log("message", { + action: "UpdateLipSync", + id, + data: { offset, length }, + }); } }, [], ); + const addCharacterAnimation = useCallback( + ({ + clipId, + characterId, + animationClips, + offset, + }: { + clipId: string; + characterId: string; + animationClips: BaseClip[]; + offset: number; + }) => { + const clip = animationClips.find((row) => row.id === clipId); + if (!clip) { + return; + } + + setCharacters((oldCharacters) => { + return oldCharacters.map((character) => { + if (character.id !== characterId) { + return { ...character }; + } + return { + ...character, + animationClips: [...character.animationClips, { ...clip, offset }], + }; + }); + }); + console.log("message", { + action: "AddAnimation", + id: clipId, + data: { offset }, + }); + }, + [], + ); + + const addCharacterAudio = useCallback( + ({ + clipId, + characterId, + audioClips, + offset, + }: { + clipId: string; + characterId: string; + audioClips: BaseClip[]; + offset: number; + }) => { + const clip = audioClips.find((row) => row.id === clipId); + if (!clip) { + return; + } + + setCharacters((oldCharacters) => { + return oldCharacters.map((character) => { + if (character.id !== characterId) { + return { ...character }; + } + return { + ...character, + lipSyncClips: [...character.lipSyncClips, { ...clip, offset }], + }; + }); + }); + console.log("message", { + action: "AddLipSync", + id: clipId, + data: { offset }, + }); + }, + [], + ); + const toggleLipSyncMute = useCallback((characterId: string) => { setCharacters((oldCharacters) => { return oldCharacters.map((character) => ({ @@ -128,11 +186,64 @@ export default function useUpdateCharacters() { character.id === characterId ? !character.muted : character.muted, })); }); + console.log("message", { + action: "ToggleLipSync", + id: characterId, + }); + }, []); + + const selectCharacterClip = useCallback((clipId: string) => { + setCharacters((oldCharacters) => { + return [ + ...oldCharacters.map((character) => ({ + ...character, + animationClips: character.animationClips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + positionClips: character.positionClips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + lipSyncClips: character.lipSyncClips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + })), + ]; + }); + }, []); + + const deleteCharacterClip = useCallback((clipId: string) => { + setCharacters((oldCharacters) => { + return [ + ...oldCharacters.map((character) => ({ + ...character, + animationClips: character.animationClips.filter( + (clip) => clip.id !== clipId, + ), + positionClips: character.positionClips.filter( + (clip) => clip.id !== clipId, + ), + lipSyncClips: character.lipSyncClips.filter( + (clip) => clip.id !== clipId, + ), + })), + ]; + }); + console.log("message", { + action: "DeleteCharacterClip", + id: clipId, + }); }, []); return { characters, updateCharacters, toggleLipSyncMute, + addCharacterAnimation, + addCharacterAudio, + selectCharacterClip, + deleteCharacterClip, }; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop.ts new file mode 100644 index 0000000000..35bf92ccb8 --- /dev/null +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop.ts @@ -0,0 +1,38 @@ +import { useCallback, useState } from "react"; + +export default function useUpdateDragDrop() { + const [state, setState] = useState<{ + dragType: "animations" | "lipSync" | null; + dragId: string | null; + }>({ dragType: null, dragId: null }); + const startDrag = useCallback( + (type: "animations" | "lipSync", id: string) => { + setState({ dragId: id, dragType: type }); + }, + [], + ); + const [canDrop, setCanDrop] = useState(false); + const [overTimeline, setOverTimeline] = useState(false); + + const endDrag = useCallback(() => { + setState({ dragId: null, dragType: null }); + }, []); + + const [dropId, setDropId] = useState(""); + const [dropOffset, setDropOffset] = useState(0); + + return { + dragId: state.dragId, + dragType: state.dragType, + startDrag, + endDrag, + canDrop, + setCanDrop, + overTimeline, + setOverTimeline, + dropId, + setDropId, + dropOffset, + setDropOffset, + }; +} diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts index 5fbad191e7..b23b738816 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { ObjectGroup } from "~/models/track"; +import { ObjectGroup } from "~/pages/PageEnigma/models/track"; export default function useUpdateObject() { const [objects, setObjects] = useState({ @@ -46,8 +46,37 @@ export default function useUpdateObject() { [], ); + const selectObjectClip = useCallback((clipId: string) => { + setObjects((oldObjectGroup) => { + return { + ...oldObjectGroup, + objects: oldObjectGroup.objects.map((object) => ({ + ...object, + clips: object.clips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + })), + }; + }); + }, []); + + const deleteObjectClip = useCallback((clipId: string) => { + setObjects((oldObjectGroup) => { + return { + ...oldObjectGroup, + objects: oldObjectGroup.objects.map((object) => ({ + ...object, + clips: object.clips.filter((clip) => clip.id !== clipId), + })), + }; + }); + }, []); + return { objects, updateObject, + selectObjectClip, + deleteObjectClip, }; } diff --git a/app/models/track.ts b/app/pages/PageEnigma/models/track.ts similarity index 97% rename from app/models/track.ts rename to app/pages/PageEnigma/models/track.ts index 29d570891d..50c0723e17 100644 --- a/app/models/track.ts +++ b/app/pages/PageEnigma/models/track.ts @@ -3,6 +3,7 @@ export interface BaseClip { name: string; offset: number; length: number; + selected?: boolean; } export interface AnimationClip extends BaseClip {}