Skip to content

Commit

Permalink
Add upload subtitle button
Browse files Browse the repository at this point in the history
Adds an "Upload" button next to the "Download" button in the
subtitle edit view. The button opens a file dialog where you can select
a vtt file. The contents of the selected file will then replace the current
subtitle cues. Tags (optional metadata from Opencast that specifiy e.g. the language of the subtitle) remain unaffected by uploading.
  • Loading branch information
Arnei committed Dec 12, 2023
1 parent 452c507 commit 674f302
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 22 deletions.
6 changes: 6 additions & 0 deletions src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@
"backButton-tooltip": "Return to subtitle selection",
"downloadButton-title": "Download",
"downloadButton-tooltip": "Download subtitle as vtt file",
"uploadButton-title": "Upload",
"uploadButton-tooltip": "Upload subtitle as vtt file",
"uploadButton-warning": "Caution! Uploading will overwrite the current subtitle. This cannot be undone. Are you sure?",
"uploadButton-error": "Upload failed.",
"uploadButton-error-filetype": "Wrong file type.",
"uploadButton-error-parse": "Could not parse subtitle file. Please ensure that the file contains valid WebVTT.",
"editTitle": "Subtitle Editor - {{title}}",
"editTitle-loading": "Loading",
"generic": "Generic",
Expand Down
130 changes: 108 additions & 22 deletions src/main/SubtitleEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { css } from "@emotion/react";
import { basicButtonStyle, flexGapReplacementStyle } from "../cssStyles";
import { LuChevronLeft, LuDownload} from "react-icons/lu";
import { basicButtonStyle, errorBoxStyle, flexGapReplacementStyle } from "../cssStyles";
import { LuChevronLeft, LuDownload, LuUpload} from "react-icons/lu";
import {
selectSubtitlesFromOpencastById,
} from '../redux/videoSlice'
Expand All @@ -16,7 +16,7 @@ import {
import SubtitleVideoArea from "./SubtitleVideoArea";
import SubtitleTimeline from "./SubtitleTimeline";
import { useTranslation } from "react-i18next";
import { useTheme } from "../themes";
import { Theme, useTheme } from "../themes";
import { parseSubtitle, serializeSubtitle } from "../util/utilityFunctions";
import { ThemedTooltip } from "./Tooltip";
import { titleStyle, titleStyleBold } from "../cssStyles";
Expand Down Expand Up @@ -82,6 +82,12 @@ const SubtitleEditor : React.FC = () => {
width: '100%',
})

const topRightButtons = css({
display: 'flex',
flexDirection: 'row',
...(flexGapReplacementStyle(10, false)),
})

const subAreaStyle = css({
display: 'flex',
flexDirection: 'row',
Expand Down Expand Up @@ -109,7 +115,10 @@ const SubtitleEditor : React.FC = () => {
<div css={[titleStyle(theme), titleStyleBold(theme)]}>
{t("subtitles.editTitle", {title: getTitle()})}
</div>
<DownloadButton/>
<div css={topRightButtons}>
<UploadButton />
<DownloadButton />
</div>
</div>
<div css={subAreaStyle}>
<SubtitleListEditor />
Expand All @@ -128,6 +137,15 @@ const SubtitleEditor : React.FC = () => {
);
}

const subtitleButtonStyle = (theme: Theme) => css({
fontSize: '16px',
height: '10px',
padding: '16px',
justifyContent: 'space-around',
boxShadow: `${theme.boxShadow}`,
background: `${theme.element_bg}`,
})

const DownloadButton: React.FC = () => {

const subtitle = useSelector(selectSelectedSubtitleById);
Expand All @@ -147,18 +165,10 @@ const DownloadButton: React.FC = () => {

const { t } = useTranslation();
const theme = useTheme();
const style = css({
fontSize: '16px',
height: '10px',
padding: '16px',
justifyContent: 'space-around',
boxShadow: `${theme.boxShadow}`,
background: `${theme.element_bg}`,
});

return (
<ThemedTooltip title={t("subtitles.downloadButton-tooltip")}>
<div css={[basicButtonStyle(theme), style]}
<div css={[basicButtonStyle(theme), subtitleButtonStyle(theme)]}
role="button"
onClick={() => downloadSubtitles()}
>
Expand All @@ -169,6 +179,90 @@ const DownloadButton: React.FC = () => {
);
}

const UploadButton: React.FC = () => {

const { t } = useTranslation();
const theme = useTheme();
const dispatch = useDispatch()

const [errorState, setErrorState] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const subtitle = useSelector(selectSelectedSubtitleById)
const selectedId = useSelector(selectSelectedSubtitleId)
// Upload Ref
const inputRef = React.useRef<HTMLInputElement>(null);

const uploadSubtitles = () => {
// open file input box on click of other element
const ref = inputRef.current
if (ref !== null) {
if (confirm(t("subtitles.uploadButton-warning"))) {
ref.click();
}
}
}

// Save uploaded file in redux
const uploadCallback = (event: React.ChangeEvent<HTMLInputElement>) => {
const fileObj = event.target.files && event.target.files[0];
if (!fileObj) {
return;
}

// Check if image
if (fileObj.type.split('/')[0] !== 'text') {
setErrorState(true)
setErrorMessage(t("subtitles.uploadButton-error-filetype"))
return
}

const reader = new FileReader();
reader.onload = e => {
// the result image data
if (e.target && e.target.result) {
try {
const text = e.target.result.toString()
const subtitleParsed = parseSubtitle(text)
dispatch(setSubtitle({identifier: selectedId, subtitles: {cues: subtitleParsed, tags: subtitle.tags}}))
setErrorState(false)
} catch (e) {
console.error(e)
setErrorState(true)
setErrorMessage(t("subtitles.uploadButton-error-parse"))
}
}
}
reader.readAsText(fileObj)
};

return (
<>
<ThemedTooltip title={t("subtitles.uploadButton-tooltip")}>
<div css={[basicButtonStyle(theme), subtitleButtonStyle(theme)]}
role="button"
onClick={() => uploadSubtitles()}
>
<LuUpload css={{fontSize: '16px'}}/>
<span>{t("subtitles.uploadButton-title")}</span>
</div>
</ThemedTooltip>
<div css={errorBoxStyle(errorState, theme)} role="alert">
<span>{t("subtitles.uploadButton-error")}</span><br />
{errorMessage ? t("various.error-details-text", {errorMessage: errorMessage}) : t("various.error-text")}<br/>
</div>
{/* Hidden input field for upload */}
<input
style={{display: 'none'}}
ref={inputRef}
type="file"
accept="text/vtt"
onChange={event => uploadCallback(event)}
aria-hidden="true"
/>
</>
);
}


/**
* Takes you to a different page
Expand All @@ -179,17 +273,9 @@ export const BackButton : React.FC = () => {
const theme = useTheme()
const dispatch = useDispatch();

const backButtonStyle = css({
height: '10px',
padding: '16px',
boxShadow: `${theme.boxShadow}`,
background: `${theme.element_bg}`,
justifyContent: 'space-around'
})

return (
<ThemedTooltip title={t("subtitles.backButton-tooltip")}>
<div css={[basicButtonStyle(theme), backButtonStyle]}
<div css={[basicButtonStyle(theme), subtitleButtonStyle(theme)]}
role="button" tabIndex={0}
aria-label={t("subtitles.backButton-tooltip")}
onClick={() => dispatch(setIsDisplayEditView(false))}
Expand Down

0 comments on commit 674f302

Please sign in to comment.