diff --git a/editor-settings.toml b/editor-settings.toml index ab3a01da7..c124c77c5 100644 --- a/editor-settings.toml +++ b/editor-settings.toml @@ -79,6 +79,19 @@ # Default: true #show = true +# Ensure that at least one video stream remains selected +# Typically, the track selection ensures that at least one video stream +# remains selected. If you would like your users to be able to create selections +# with only audio streams, set this to false. +# Default: true +#atLeastOneVideo = true + +# Disables track selection for events with more than two videos +# If your Opencast can handle track selection for more than two videos, set this +# to false. +# Default: true +#atMostTwoVideos = true + #### # Subtitles ## diff --git a/public/editor-settings.toml b/public/editor-settings.toml index 94ad8b6b9..32e761456 100644 --- a/public/editor-settings.toml +++ b/public/editor-settings.toml @@ -28,6 +28,8 @@ show = true [trackSelection] show = true +atLeastOneVideo = true +atMostTwoVideos = true [subtitles] show = true diff --git a/src/config.ts b/src/config.ts index 82019acd7..03e5343c2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -55,6 +55,8 @@ interface iSettings { }, trackSelection: { show: boolean, + atLeastOneVideo: boolean, + atMostTwoVideos: boolean, }, thumbnail: { show: boolean, @@ -93,6 +95,8 @@ const defaultSettings: iSettings = { }, trackSelection: { show: true, + atLeastOneVideo: true, + atMostTwoVideos: true, }, thumbnail: { show: false, @@ -403,6 +407,8 @@ const SCHEMA = { }, trackSelection: { show: types.boolean, + atLeastOneVideo: types.boolean, + atMostTwoVideos: types.boolean, }, subtitles: { show: types.boolean, diff --git a/src/cssStyles.tsx b/src/cssStyles.tsx index 1392b59cf..2a7dd05d2 100644 --- a/src/cssStyles.tsx +++ b/src/cssStyles.tsx @@ -433,6 +433,11 @@ export const backgroundBoxStyle = (theme: Theme) => css(({ gap: "25px", })); +export const checkboxStyle = (theme: Theme) => css({ + color: theme.text, + "&.Mui-disabled": { color: theme.disabled }, +}); + export const undisplay = (maxWidth: number) => css({ [`@media (max-width: ${maxWidth}px)`]: { display: "none", diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index accb088a3..8b66cb98b 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -240,13 +240,22 @@ "trackSelection": { "title": "Select track(s) for processing", - "trackInactive": "inactive", - "deleteTrackText": "Delete Track", - "restoreTrackText": "Restore Track", - "cannotDeleteTrackText": "Cannot Delete Track", - "deleteTrackTooltip": "Do not encode and publish this track.", - "restoreTrackTooltip": "Encode and publish this track.", - "cannotDeleteTrackTooltip": "Cannot remove this track from publication." + "help": "At least one track has to be selected.", + "helpAtLeastOneVideo": "At least one video track has to be selected.", + "atMostTwoVideos": "Track Selection is disabled for events with more than two videos", + "customizeLabel": "Customize track selection", + "videoTracksHeader": "Video tracks", + "audioTracksHeader": "Audio tracks", + "confirmText": "Confirm selection", + "confirmTooltip": "Confirm selected tracks", + "noAudioAvailable": "No audio available", + "selectionAlertInfoVideo_zero": "You have not selected any video track.", + "selectionAlertInfoVideo_one": "You have selected 1 video track.", + "selectionAlertInfoVideo_other": "You have selected {{count}} video tracks.", + "selectionAlertInfoAudio_zero": "You have not selected any audio track.", + "selectionAlertInfoAudio_one": "You have selected 1 audio track. It will be duplicated onto all videos.", + "selectionAlertInfoAudio_other": "You have selected {{count}} audio tracks.", + "selectionAlertError": "At least one video or audio track has to be selected." }, "subtitles": { diff --git a/src/img/placeholder-waveform.png b/src/img/placeholder-waveform.png new file mode 100644 index 000000000..eac7b675e Binary files /dev/null and b/src/img/placeholder-waveform.png differ diff --git a/src/img/placeholder_waveform.png b/src/img/placeholder_waveform.png deleted file mode 100644 index 3f6606cb4..000000000 Binary files a/src/img/placeholder_waveform.png and /dev/null differ diff --git a/src/main/Save.tsx b/src/main/Save.tsx index 49582b51e..a6144e9c0 100644 --- a/src/main/Save.tsx +++ b/src/main/Save.tsx @@ -10,6 +10,7 @@ import { LuCheckCircle, LuAlertCircle, LuChevronLeft, LuSave, LuCheck } from "re import { useAppDispatch, useAppSelector } from "../redux/store"; import { + selectCustomizedTrackSelection, selectHasChanges, selectSegments, selectTracks, @@ -115,6 +116,7 @@ export const SaveButton: React.FC<{ const segments = useAppSelector(selectSegments); const tracks = useAppSelector(selectTracks); + const customizedTrackSelection = useAppSelector(selectCustomizedTrackSelection); const subtitles = useAppSelector(selectSubtitles); const metadata = useAppSelector(selectCatalogs); const workflowStatus = useAppSelector(selectStatus); @@ -154,6 +156,7 @@ export const SaveButton: React.FC<{ dispatch(postVideoInformation({ segments: segments, tracks: tracks, + customizedTrackSelection, subtitles: prepareSubtitles(), metadata: metadata, })); diff --git a/src/main/TrackSelection.tsx b/src/main/TrackSelection.tsx index 7be0c65b2..dc162a02d 100644 --- a/src/main/TrackSelection.tsx +++ b/src/main/TrackSelection.tsx @@ -1,74 +1,198 @@ -import React from "react"; +import React, { useEffect } from "react"; import { css } from "@emotion/react"; +import { Alert, Checkbox, FormControlLabel } from "@mui/material"; -import { IconType } from "react-icons"; -import { LuTrash } from "react-icons/lu"; -import TrashRestore from "../img/trash-restore.svg?react"; import ReactPlayer from "react-player"; import { Track } from "../types"; -import { useAppDispatch, useAppSelector } from "../redux/store"; -import { selectVideos, setTrackEnabled } from "../redux/videoSlice"; import { + selectCustomizedTrackSelection, + selectVideos, + selectWaveformImages, + setAudioEnabled, + setCustomizedTrackSelection, + setVideoEnabled, +} from "../redux/videoSlice"; +import { + BREAKPOINTS, backgroundBoxStyle, - basicButtonStyle, - customIconStyle, - deactivatedButtonStyle, + checkboxStyle, titleStyle, titleStyleBold, } from "../cssStyles"; import { useTranslation } from "react-i18next"; -import { Theme, useTheme } from "../themes"; -import { ThemedTooltip } from "./Tooltip"; -import { ProtoButton } from "@opencast/appkit"; +import { useTheme } from "../themes"; +import { outOfBounds } from "../util/utilityFunctions"; +import { useAppDispatch, useAppSelector } from "../redux/store"; +import PlaceholderWaveform from "../img/placeholder-waveform.png"; +import { settings } from "../config"; /** * Creates the track selection. */ const TrackSelection: React.FC = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useAppDispatch(); // Generate list of tracks - const tracks: Track[] = useAppSelector(selectVideos); - const enabledCount = tracks.filter(t => t.video_stream.enabled).length; - const trackItems: JSX.Element[] = tracks.map((track: Track) => - + const tracks = useAppSelector(selectVideos); + let enabledCount = 0; + if (settings.trackSelection.atLeastOneVideo) { + // Only care about at least one video stream being enabled + enabledCount = tracks.reduce( + (memo: number, track: Track) => memo + !!track.video_stream.enabled, + 0 + ); + } else { + // Make sure that at least one track remains enabled + enabledCount = tracks.reduce( + (memo: number, track: Track) => memo + !!track.video_stream.enabled + !!track.audio_stream.enabled, + 0 + ); + } + const images = useAppSelector(selectWaveformImages); + const customizedTrackSelection = !!useAppSelector(selectCustomizedTrackSelection); + + const videoTrackItems = tracks.map( + (track: Track) => ( + ) ); - const trackSelectionStyle = css({ - display: "flex", - width: "auto", - height: "100%", - flexDirection: "column", - alignItems: "center", - }); + const audioTrackItems = tracks.map( + (track: Track, index: number) => ( + + ) + ); - const trackAreaStyle = css({ - display: "flex", - width: "100%", - height: "100%", - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - gap: "10px", - }); + const onChange = () => { + if (customizedTrackSelection) { + tracks.forEach(track => { + if (track.video_stream.available) { + dispatch(setVideoEnabled({ + trackId: track.id, + enabled: true, + })); + } + if (track.audio_stream.available) { + dispatch(setAudioEnabled({ + trackId: track.id, + enabled: true, + })); + } + }); + } + + dispatch(setCustomizedTrackSelection(!customizedTrackSelection)); + }; + + const isDisabledBecauseMoreThanTwoVideos = () => { + if (settings.trackSelection.atMostTwoVideos && tracks.length > 2) { + return true; + } + return false; + }; + + const styles = { + trackSelection: css({ + display: "flex", + height: "100%", + flexDirection: "column", + alignItems: "center", + gap: "2rem", + alignSelf: "center", + }), + + trackArea: css({ + display: "flex", + width: "100%", + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "stretch", + "& > *": { + flex: "1 1 0px", + }, + gap: "10px", + }), + + leftAlignedSection: css({ + alignSelf: "start", + }), + + selectionSection: css({ + transition: "all 0.05s", + width: "100%", + ...( + customizedTrackSelection || isDisabledBecauseMoreThanTwoVideos() + ? {} + : { + opacity: "0.7", + pointerEvents: "none", + filter: "grayscale(80%) blur(1.5px) brightness(80%)", + } + ), + }), + + trackSection: css({ + "& h3": { + marginBlock: "1rem", + }, + }), + }; + + if (isDisabledBecauseMoreThanTwoVideos()) { + return ( +
+
+
+ + {t("trackSelection.atMostTwoVideos")} + +
+
+ ); + } return ( -
+
-
- {trackItems} -
+
+ +
+
+ +
+
+

{t("trackSelection.videoTracksHeader")}

+
{ videoTrackItems }
+
+
+

{t("trackSelection.audioTracksHeader")}

+
{ audioTrackItems }
+
); }; - const Header: React.FC = () => { - const { t } = useTranslation(); const theme = useTheme(); - const description: string = t("trackSelection.title"); return ( @@ -78,154 +202,244 @@ const Header: React.FC = () => { ); }; - -const TrackItem: React.FC<{ track: Track, enabledCount: number; }> = ({ track, enabledCount }) => { - +const TrackSelectionEnabler: React.FC<{ + customizable: boolean, + onChange: () => void, +}> = ({ + customizable, + onChange, +}) => { const theme = useTheme(); - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const header = track.flavor.type + " " - + (track.video_stream.enabled ? "" - : `(${t("trackSelection.trackInactive", "inactive")})`); - - const trackItemStyle = css({ - display: "flex", - flexDirection: "column", - alignItems: "left", - }); + const label = t("trackSelection.customizeLabel"); - const trackitemSubStyle = css({ - display: "flex", - flexDirection: "row", - gap: "20px", + return ( + + } label={label} /> + ); +}; - justifyContent: "space-around", - flexWrap: "wrap", - }); +const VideoTrackItem: React.FC<{ + track: Track, + enabledCount: number, + customizable: boolean, +}> = ({ + track, + enabledCount, + customizable, +}) => { + const dispatch = useAppDispatch(); + const imagesMaxWidth = 300; + const imagesMaxWidthMedium = 150; + const disabled = !customizable || (track.video_stream.enabled && enabledCount === 1); const playerStyle = css({ aspectRatio: "16 / 9", width: "100%", - maxWidth: "457px", - }); - - const headerStyle = css({ - fontWeight: "bold", - fontSize: "larger", - color: `${theme.text}`, - "&:first-letter": { - textTransform: "capitalize", + opacity: track.video_stream.enabled ? "1" : "0.5", + maxWidth: `${imagesMaxWidthMedium}px`, + [`@media (min-width: ${BREAKPOINTS.medium}px)`]: { + "&": { maxWidth: `${imagesMaxWidth}px` }, }, }); - const buttonsStyle = css({ - // TODO: Avoid hard-coding max-width - "@media (max-width: 1550px)": { - width: "100%", + const playerRootStyle = { + filter: track.video_stream.enabled ? "none" : "grayscale(80%) blur(1.5px) brightness(80%)", + opacity: track.video_stream.enabled ? "1" : "0.5", + }; + + const videoEnabledChange = () => { + dispatch(setVideoEnabled({ + trackId: track.id, + enabled: !track.video_stream.enabled, + })); + }; + + return ( + + + + ); +}; + +const AudioTrackItem: React.FC<{ + track: Track, + waveform: string | undefined + enabledCount: number, + customizable: boolean, +}> = ({ + track, + waveform, + enabledCount, + customizable, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const imagesMaxWidth = 300; + const imagesMaxWidthMedium = 150; + const disabled = !customizable || + (!settings.trackSelection.atLeastOneVideo && track.audio_stream.enabled && enabledCount === 1); + + const imgStyle = css({ + height: "54px", // Keep height consistent in case the image does not render + width: "100%", + filter: `${theme.invert_wave}`, + color: `${theme.inverted_text}`, + maxWidth: `${imagesMaxWidthMedium}px`, + [`@media (min-width: ${BREAKPOINTS.medium}px)`]: { + "&": { maxWidth: `${imagesMaxWidth}px` }, }, - display: "flex", - flexDirection: "column", }); - // What state is the track in and can it be deactivated? - // We do not permit deactivating the last remaining track - // 2 -> Track is enabled and can be deactivated - // 1 -> Track is enabled but is the last and cannot be deactivated - // 0 -> Track is disabled and can be restored - const deleteStatus = track.video_stream.enabled ? (enabledCount > 1 ? 0 : 1) : 2; - const deleteEnabled = deleteStatus !== 1; - const deleteText = [ - t("trackSelection.deleteTrackText", "Delete Track"), - t("trackSelection.cannotDeleteTrackText", "Cannot Delete Track"), - t("trackSelection.restoreTrackText", "Restore Track"), - ][deleteStatus]; - const deleteTooltip = [ - t("trackSelection.deleteTrackTooltip", "Do not encode and publish this track."), - t("trackSelection.cannotDeleteTrackTooltip", "Cannot remove this track from publication."), - t("trackSelection.restoreTrackTooltip", "Encode and publish this track."), - ][deleteStatus]; - const deleteIcon = [LuTrash, LuTrash, TrashRestore][deleteStatus]; - const trackEnabledChange = () => { - dispatch(setTrackEnabled({ - id: track.id, - enabled: !track.video_stream.enabled, + const audioEnabledChange = () => { + dispatch(setAudioEnabled({ + trackId: track.id, + enabled: !track.audio_stream.enabled, })); }; + useEffect(() => { + if (!track.audio_stream.available) { + dispatch(setAudioEnabled({ + trackId: track.id, + enabled: false, + })); + } + }, [track.audio_stream.available]); + return ( -
-
{header}
-
- + {track.audio_stream.available ? + placeholder for audio stream -
- + : + {t("trackSelection.noAudioAvailable")} + } + + ); +}; + +const TrackItem: React.FC<{ + header: string, + checked: boolean, + disabled: boolean, + onChange: () => void, + children: React.ReactNode, +}> = ({ + header, + checked, + disabled, + onChange, + children, +}) => { + const theme = useTheme(); + + const styles = { + trackItem: css({ + display: "flex", + flexDirection: "column", + alignItems: "left", + cursor: disabled ? "not-allowed" : "pointer", + }), + + trackitemSub: css({ + display: "flex", + flexDirection: "row", + gap: "20px", + justifyContent: "space-around", + flexWrap: "wrap", + flexGrow: "1", + alignItems: "center", + }), + + images: css({ + display: "flex", + flexDirection: "column", + gap: "20px", + }), + + header: css({ + fontWeight: "bold", + textTransform: "capitalize", + display: "flex", + alignItems: "center", + gap: "0.5em", + }), + }; + + return ( +
+ ); }; -interface selectButtonInterface { - handler: () => void, - text: string, - Icon: IconType | React.FunctionComponent, - tooltip: string, - active: boolean, +interface selectionAlertInterface { + tracks: Track[], + customizable: boolean, } -const SelectButton: React.FC = ({ handler, text, Icon, tooltip, active }) => { - +const SelectionAlert: React.FC = ({ + tracks, + customizable, +}) => { + const { t } = useTranslation(); const theme = useTheme(); + const video = tracks.filter(t => t.video_stream.enabled).length; + const audio = tracks.filter(t => t.audio_stream.enabled).length; - const buttonStyle = (theme: Theme) => [ - active ? basicButtonStyle(theme) : deactivatedButtonStyle, - css({ - padding: "16px", - maxHeight: "21px", - boxShadow: "", - background: `${theme.element_bg}`, - textWrap: "nowrap", - })]; - - const clickHandler = () => { - if (active) { handler(); } - ref.current?.blur(); - }; - - const keyHandler = (event: React.KeyboardEvent) => { - if (active && (event.key === " " || event.key === "Enter")) { - handler(); - } - }; - - const ref = React.useRef(null); + const lines = customizable ? [ + t("trackSelection.selectionAlertInfoVideo", { count: video }), + t("trackSelection.selectionAlertInfoAudio", { count: audio }), + ] : []; return ( - - - - {text} - - + +
+ {settings.trackSelection.atLeastOneVideo ? t("trackSelection.helpAtLeastOneVideo") : t("trackSelection.help")} +
+ + {lines.map((line, index) => (
{line}
))} +
); }; diff --git a/src/main/WorkflowConfiguration.tsx b/src/main/WorkflowConfiguration.tsx index ae8ef7f63..dfcd1fb43 100644 --- a/src/main/WorkflowConfiguration.tsx +++ b/src/main/WorkflowConfiguration.tsx @@ -9,9 +9,7 @@ import { import { LuChevronLeft, LuMoreHorizontal } from "react-icons/lu"; import { useAppSelector } from "../redux/store"; - import { PageButton } from "./Finish"; - import { useTranslation } from "react-i18next"; import { useTheme } from "../themes"; import { selectError, selectStatus } from "../redux/workflowPostSlice"; diff --git a/src/redux/videoSlice.ts b/src/redux/videoSlice.ts index abd040880..f5d666b4f 100644 --- a/src/redux/videoSlice.ts +++ b/src/redux/videoSlice.ts @@ -1,5 +1,5 @@ import { clamp } from "lodash"; -import { createSlice, nanoid, PayloadAction, createSelector } from "@reduxjs/toolkit"; +import { createSlice, nanoid, PayloadAction } from "@reduxjs/toolkit"; import { client } from "../util/client"; import { Segment, httpRequestState, Track, Workflow, SubtitlesFromOpencast } from "../types"; @@ -18,6 +18,7 @@ export interface video { currentlyAt: number, // Position in the video in milliseconds segments: Segment[], tracks: Track[], + customizedTrackSelection: boolean, // Did user select tracks for processing subtitlesFromOpencast: SubtitlesFromOpencast[], activeSegmentIndex: number, // Index of the segment that is currenlty hovered selectedWorkflowId: string, // Id of the currently selected workflow @@ -53,6 +54,7 @@ export const initialState: video & httpRequestState = { currentlyAt: 0, // Position in the video in milliseconds segments: [{ id: nanoid(), start: 0, end: 1, deleted: false }], tracks: [], + customizedTrackSelection: false, subtitlesFromOpencast: [], activeSegmentIndex: 0, selectedWorkflowId: "", @@ -115,15 +117,22 @@ const videoSlice = createSlice({ name: "videoState", initialState, reducers: { - setTrackEnabled: (state, action) => { + setVideoEnabled: (state, action: PayloadAction<{trackId: string, enabled: boolean}>) => { for (const track of state.tracks) { - if (track.id === action.payload.id) { - track.audio_stream.enabled = action.payload.enabled; + if (track.id === action.payload.trackId) { track.video_stream.enabled = action.payload.enabled; } } state.hasChanges = true; }, + setAudioEnabled: (state, action: PayloadAction<{trackId: string, enabled: boolean}>) => { + for (const track of state.tracks) { + if (track.id === action.payload.trackId) { + track.audio_stream.enabled = action.payload.enabled; + } + } + state.hasChanges = true; + }, setIsPlaying: (state, action: PayloadAction) => { state.isPlaying = action.payload; }, @@ -292,6 +301,9 @@ const videoSlice = createSlice({ mergeSegments(state, state.activeSegmentIndex, state.segments.length - 1); state.hasChanges = true; }, + setCustomizedTrackSelection: (state, action: PayloadAction) => { + state.customizedTrackSelection = action.payload; + }, timelineZoomIn: state => { state.timelineZoom = clamp(state.timelineZoom + 1, 1, timelineZoomMax(state)); }, @@ -306,19 +318,19 @@ const videoSlice = createSlice({ state.status = "loading"; }); builder.addCase( - fetchVideoInformation.fulfilled, (state, action) => { + fetchVideoInformation.fulfilled, (state, { payload }) => { state.status = "success"; - if (action.payload.workflow_active) { + if (payload.workflow_active) { state.status = "failed"; state.errorReason = "workflowActive"; state.error = "This event is being processed. Please wait until the process is finished."; } - state.tracks = action.payload.tracks + state.tracks = payload.tracks .sort((a: { thumbnailPriority: number; }, b: { thumbnailPriority: number; }) => { return a.thumbnailPriority - b.thumbnailPriority; }).map((track: Track) => { - if (action.payload.local && settings.opencast.local) { + if (payload.local && settings.opencast.local) { console.debug("Replacing track URL"); track.uri = track.uri.replace(/https?:\/\/[^/]*/g, window.location.origin); } @@ -328,22 +340,23 @@ const videoSlice = createSlice({ // eslint-disable-next-line no-sequences state.videoURLs = videos.reduce((a: string[], o: { uri: string; }) => (a.push(o.uri), a), []); state.videoCount = state.videoURLs.length; - state.subtitlesFromOpencast = action.payload.subtitles ? - state.subtitlesFromOpencast = action.payload.subtitles : []; - state.duration = action.payload.duration; - state.title = action.payload.title; - state.segments = parseSegments(action.payload.segments, action.payload.duration); - state.workflows = action.payload.workflows; - state.waveformImages = action.payload.waveformURIs ? action.payload.waveformURIs : state.waveformImages; + state.subtitlesFromOpencast = payload.subtitles ? + state.subtitlesFromOpencast = payload.subtitles : []; + state.duration = payload.duration; + state.title = payload.title; + state.segments = parseSegments(payload.segments, payload.duration); + state.workflows = payload.workflows; + state.waveformImages = payload.waveformURIs ? payload.waveformURIs : state.waveformImages; state.originalThumbnails = state.tracks.map( (track: Track) => { return { id: track.id, uri: track.thumbnailUri }; } ); state.aspectRatios = new Array(state.videoCount); - state.lockingActive = action.payload.locking_active; - state.lockRefresh = action.payload.lock_refresh; - state.lock.uuid = action.payload.lock_uuid; - state.lock.user = action.payload.lock_user; + state.lockingActive = payload.locking_active; + state.lockRefresh = payload.lock_refresh; + state.lock.uuid = payload.lock_uuid; + state.lock.user = payload.lock_user; + state.customizedTrackSelection = payload.customizedTrackSelection; }); builder.addCase( fetchVideoInformation.rejected, (state, action) => { @@ -372,12 +385,14 @@ const videoSlice = createSlice({ selectWaveformImages: state => state.waveformImages, selectOriginalThumbnails: state => state.originalThumbnails, // Selectors mainly pertaining to the information fetched from Opencast + selectVideos: state => state.tracks.filter((track: Track) => track.video_stream.available === true), selectVideoURL: state => state.videoURLs, selectVideoCount: state => state.videoCount, selectDuration: state => state.duration, selectDurationInSeconds: state => state.duration / 1000, selectTitle: state => state.title, selectTracks: state => state.tracks, + selectCustomizedTrackSelection: state => state.customizedTrackSelection, selectWorkflows: state => state.workflows, selectAspectRatio: state => calculateTotalAspectRatio(state.aspectRatios), selectSubtitlesFromOpencast: state => state.subtitlesFromOpencast, @@ -516,43 +531,40 @@ function timelineZoomMax(state: { duration: number }) { } export const { - setTrackEnabled, - setIsPlaying, - setIsPlayPreview, - setIsMuted, - setVolume, - setCurrentlyAt, - setCurrentlyAtInSeconds, addSegment, - setAspectRatio, - setHasChanges, - setWaveformImages, - setThumbnails, - setThumbnail, - removeThumbnail, - setLock, cut, - moveCut, - markAsDeletedOrAlive, - setSelectedWorkflowIndex, - mergeLeft, - mergeRight, - mergeAll, - setPreviewTriggered, - setClickTriggered, setTimelineZoom, timelineZoomIn, timelineZoomOut, - setJumpTriggered, jumpToPreviousSegment, jumpToNextSegment, + markAsDeletedOrAlive, + mergeAll, + mergeLeft, + mergeRight, + moveCut, + removeThumbnail, + setAspectRatio, + setAudioEnabled, + setClickTriggered, + setCurrentlyAt, + setCurrentlyAtInSeconds, + setCustomizedTrackSelection, + setHasChanges, + setIsMuted, + setIsPlayPreview, + setIsPlaying, + setJumpTriggered, + setLock, + setPreviewTriggered, + setSelectedWorkflowIndex, + setThumbnail, + setThumbnails, + setVideoEnabled, + setVolume, + setWaveformImages, } = videoSlice.actions; -export const selectVideos = createSelector( - [(state: { videoState: { tracks: video["tracks"]; }; }) => state.videoState.tracks], - tracks => tracks.filter((track: Track) => track.video_stream.available === true) -); - // Export selectors export const { selectIsPlaying, @@ -564,6 +576,7 @@ export const { selectJumpTriggered, selectCurrentlyAt, selectCurrentlyAtInSeconds, + selectCustomizedTrackSelection, selectSegments, selectActiveSegmentIndex, selectIsCurrentSegmentAlive, @@ -583,6 +596,7 @@ export const { selectAspectRatio, selectSubtitlesFromOpencast, selectSubtitlesFromOpencastById, + selectVideos, } = videoSlice.selectors; export default videoSlice.reducer; diff --git a/src/redux/workflowPostSlice.ts b/src/redux/workflowPostSlice.ts index c1ec1ec6d..b9acdb404 100644 --- a/src/redux/workflowPostSlice.ts +++ b/src/redux/workflowPostSlice.ts @@ -20,6 +20,7 @@ export const postVideoInformation = { segments: convertSegments(argument.segments), tracks: argument.tracks, + customizedTrackSelection: argument.customizedTrackSelection, subtitles: argument.subtitles, workflows: argument.workflow, metadataJSON: JSON.stringify(argument.metadata), diff --git a/src/types.ts b/src/types.ts index 600ee8f52..799b1b539 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,6 +70,7 @@ export interface ExtendedSubtitleCue extends SubtitleCue { export interface PostEditArgument { segments: Segment[] tracks: Track[] + customizedTrackSelection: boolean subtitles: SubtitlesFromOpencast[] workflow?: [{id: string}] metadata: Catalog[] diff --git a/src/util/utilityFunctions.ts b/src/util/utilityFunctions.ts index ed0d86288..347744390 100644 --- a/src/util/utilityFunctions.ts +++ b/src/util/utilityFunctions.ts @@ -203,3 +203,11 @@ export function useInterval(callback: IntervalFunction, delay: number | null) { } }, [callback, delay]); } + +// Returns true if the given index is out of bounds on the given array +export function outOfBounds(array: unknown[], index: number) { + if (index >= array.length) { + return true; + } + return false; +}