From fcc79d0eb236a35732248c22a0417c0e26d26e9b Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 13 Apr 2023 16:30:20 +0200 Subject: [PATCH 01/13] Basic zoom functionality for the cutting view Based off of the subtitle view. Implements a very basic zoom functionality, hoping that it's better than nothing. Includes: - A Slider to control the zoom level - A horizontal scrollbar to move the timeline - Drag n' Drop to move the timeline Known issues: - The scrubber/playhead is cut off if placed either at 0 or at the maximum duration. I might have css'd myself into a corner here. --- src/i18n/locales/en-US.json | 3 +- src/main/CuttingActions.tsx | 92 +++++++++++++++++++++++++++++-------- src/main/Timeline.tsx | 44 +++++++++++------- src/redux/videoSlice.ts | 11 ++++- 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 4514a5dd6..50e0fcae4 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -23,7 +23,8 @@ "mergeLeft-tooltip-aria": "Merge Left. Combine the currently active segment with the segment to its left. Hotkey: {{hotkeyName}}.", "mergeRight-button": "Merge Right", "mergeRight-tooltip": "Combine the currently active segment with the segment to its right. Hotkey: {{hotkeyName}}", - "mergeRight-tooltip-aria": "Merge Right. Combine the currently active segment with the segment to its right. Hotkey: {{hotkeyName}}." + "mergeRight-tooltip-aria": "Merge Right. Combine the currently active segment with the segment to its right. Hotkey: {{hotkeyName}}.", + "zoomSlider-aria": "Zoom" }, "video": { diff --git a/src/main/CuttingActions.tsx b/src/main/CuttingActions.tsx index c314392a6..8dd69dd3e 100644 --- a/src/main/CuttingActions.tsx +++ b/src/main/CuttingActions.tsx @@ -16,16 +16,17 @@ import { css } from '@emotion/react' import { useDispatch, useSelector } from 'react-redux'; import { - cut, markAsDeletedOrAlive, selectIsCurrentSegmentAlive, mergeLeft, mergeRight + cut, markAsDeletedOrAlive, selectIsCurrentSegmentAlive, mergeLeft, mergeRight, setTimelineZoom } from '../redux/videoSlice' import { GlobalHotKeys, KeySequence, KeyMapOptions } from "react-hotkeys"; import { cuttingKeyMap } from "../globalKeys"; -import { ActionCreatorWithoutPayload } from "@reduxjs/toolkit"; +import { ActionCreatorWithoutPayload, ActionCreatorWithPayload } from "@reduxjs/toolkit"; import './../i18n/config'; import { useTranslation } from 'react-i18next'; import { selectTheme, Theme } from "../redux/themeSlice"; import { ThemedTooltip } from "./Tooltip"; +import { Slider } from "@mui/material"; /** * Defines the different actions a user can perform while in cutting mode @@ -36,6 +37,7 @@ const CuttingActions: React.FC<{}> = () => { // Init redux variables const dispatch = useDispatch(); + const theme = useSelector(selectTheme); /** * General action callback for cutting actions @@ -43,10 +45,22 @@ const CuttingActions: React.FC<{}> = () => { * @param action redux event to dispatch * @param ref Pass a reference if the clicked element should lose focus */ - const dispatchAction = (event: KeyboardEvent | SyntheticEvent, action: ActionCreatorWithoutPayload, ref: React.RefObject | undefined) => { + const dispatchAction = ( + event: KeyboardEvent | SyntheticEvent | Event, + action: ActionCreatorWithoutPayload | undefined, + actionWithPayload: ActionCreatorWithPayload | undefined, + payload: any, + ref: React.RefObject | undefined + ) => { event.preventDefault() // Prevent page scrolling due to Space bar press event.stopPropagation() // Prevent video playback due to Space bar press - dispatch(action()) + + if (action) { + dispatch(action()) + } + if (actionWithPayload) { + dispatch(actionWithPayload(payload)) + } // Lose focus if clicked by mouse if (ref) { @@ -56,10 +70,15 @@ const CuttingActions: React.FC<{}> = () => { // Maps functions to hotkeys const handlers = { - cut: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, cut, undefined) } }, - delete: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, markAsDeletedOrAlive, undefined) } }, - mergeLeft: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, mergeLeft, undefined) } }, - mergeRight: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, mergeRight, undefined) } }, + cut: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, cut, undefined, undefined, undefined) } }, + delete: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, markAsDeletedOrAlive, undefined, undefined, undefined) } }, + mergeLeft: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, mergeLeft, undefined, undefined, undefined) } }, + mergeRight: (keyEvent?: KeyboardEvent | SyntheticEvent) => { if(keyEvent) { dispatchAction(keyEvent, mergeRight, undefined, undefined, undefined) } }, + } + + // Callback for the zoom slider + const zoomSliderOnChange = (event: Event, newValue: number | number[]) => { + dispatchAction(event, undefined, setTimelineZoom, newValue, undefined) } const cuttingStyle = css({ @@ -75,12 +94,25 @@ const CuttingActions: React.FC<{}> = () => { ...(flexGapReplacementStyle(30, true)), }) + const sliderStyle = css({ + width: '100px', + "& .MuiSlider-thumb": { + color: `${theme.text}`, + }, + "& .MuiSlider-rail": { + color: `${theme.text}`, + }, + "& .MuiSlider-track": { + color: `${theme.text}`, + }, + }) + return (
@@ -88,17 +120,27 @@ const CuttingActions: React.FC<{}> = () => { hotKeyName={(cuttingKeyMap[handlers.delete.name] as KeyMapOptions).sequence} />
+ {/* css({ interface cuttingActionsButtonInterface { iconName: IconProp, actionName: string, - actionHandler: (event: KeyboardEvent | SyntheticEvent, action: ActionCreatorWithoutPayload, ref: React.RefObject | undefined) => void, - action: ActionCreatorWithoutPayload, + actionHandler: ( + event: KeyboardEvent | SyntheticEvent, + action: ActionCreatorWithoutPayload | undefined, + actionWithPayload: ActionCreatorWithPayload | undefined, + payload: any, + ref: React.RefObject | undefined) => void, + action: ActionCreatorWithoutPayload | undefined, + actionWithPayload: ActionCreatorWithPayload | undefined, + payload: any, tooltip: string, ariaLabelText: string, } @@ -135,7 +184,7 @@ interface cuttingActionsButtonInterface { * A button representing a single action a user can take while cutting * @param param0 */ -const CuttingActionsButton: React.FC = ({iconName, actionName, actionHandler, action, tooltip, ariaLabelText}) => { +const CuttingActionsButton: React.FC = ({iconName, actionName, actionHandler, action, actionWithPayload, payload, tooltip, ariaLabelText}) => { const ref = React.useRef(null) const theme = useSelector(selectTheme); @@ -144,9 +193,9 @@ const CuttingActionsButton: React.FC = ({iconName
actionHandler(event, action, ref) } + onClick={ (event: SyntheticEvent) => actionHandler(event, action, actionWithPayload, payload, ref) } onKeyDown={(event: React.KeyboardEvent) => { if (event.key === " " || event.key === "Enter") { - actionHandler(event, action, undefined) + actionHandler(event, action, actionWithPayload, payload, undefined) }}} > @@ -157,7 +206,12 @@ const CuttingActionsButton: React.FC = ({iconName }; interface markAsDeleteButtonInterface { - actionHandler: (event: KeyboardEvent | SyntheticEvent, action: ActionCreatorWithoutPayload, ref: React.RefObject | undefined) => void, + actionHandler: ( + event: KeyboardEvent | SyntheticEvent, + action: ActionCreatorWithoutPayload | undefined, + actionWithPayload: ActionCreatorWithPayload | undefined, + payload: any, + ref: React.RefObject | undefined) => void, action: ActionCreatorWithoutPayload, hotKeyName: KeySequence, } @@ -178,9 +232,9 @@ const MarkAsDeletedButton : React.FC = ({actionHand ref={ref} role="button" tabIndex={0} aria-label={t('cuttingActions.delete-restore-tooltip-aria', { hotkeyName: hotKeyName })} - onClick={(event: SyntheticEvent) => actionHandler(event, action, ref)} + onClick={(event: SyntheticEvent) => actionHandler(event, action, undefined, undefined, ref)} onKeyDown={(event: React.KeyboardEvent) => { if (event.key === " " || event.key === "Enter") { - actionHandler(event, action, undefined) + actionHandler(event, action, undefined, undefined, undefined) }}} > diff --git a/src/main/Timeline.tsx b/src/main/Timeline.tsx index 72af50132..8d1139b55 100644 --- a/src/main/Timeline.tsx +++ b/src/main/Timeline.tsx @@ -7,7 +7,7 @@ import { css } from '@emotion/react' import { useSelector, useDispatch } from 'react-redux'; import { Segment, httpRequestState } from '../types' import { - selectSegments, selectActiveSegmentIndex, selectDuration, selectVideoURL, selectWaveformImages, setWaveformImages + selectSegments, selectActiveSegmentIndex, selectDuration, selectVideoURL, selectWaveformImages, setWaveformImages, selectTimelineZoom } from '../redux/videoSlice' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -26,6 +26,7 @@ import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; import { RootState } from '../redux/store'; import { selectTheme } from '../redux/themeSlice'; import { ThemedTooltip } from './Tooltip'; +import ScrollContainer from 'react-indiana-drag-scroll'; /** * A container for visualizing the cutting of the video, as well as for controlling @@ -54,13 +55,15 @@ const Timeline: React.FC<{ // Init redux variables const dispatch = useDispatch(); const duration = useSelector(selectDuration) + const timelineZoom = useSelector(selectTimelineZoom) const { ref, width = 1, } = useResizeObserver(); + const scrollContainerRef = useRef(null); const timelineStyle = css({ position: 'relative', // Need to set position for Draggable bounds to work height: timelineHeight + 'px', - width: '100%', + width: (timelineZoom) * 100 + '%', // Width modified by zoom }); // Update the current time based on the position clicked on the timeline @@ -72,20 +75,29 @@ const Timeline: React.FC<{ } return ( -
setCurrentlyAtToClick(e)}> - -
- - + +
setCurrentlyAtToClick(e)}> + + +
+ + +
+
-
+ ); }; @@ -260,7 +272,7 @@ export const Scrubber: React.FC<{ position={controlledPosition} nodeRef={nodeRef} > -
+
) => { state.hasChanges = action.payload }, + setTimelineZoom: (state, action: PayloadAction) => { + state.timelineZoom = action.payload + }, setWaveformImages: (state, action: PayloadAction) => { state.waveformImages = action.payload }, @@ -336,8 +341,8 @@ const setThumbnailHelper = (state: WritableDraft