Skip to content

Commit

Permalink
Merge pull request opencast#1229 from Arnei/upload-subtitles
Browse files Browse the repository at this point in the history
Add upload subtitle button
  • Loading branch information
lkiesow authored Jun 10, 2024
2 parents a5eaccd + f2a47b3 commit 44afe77
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 30 deletions.
35 changes: 30 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@fontsource-variable/roboto-flex": "^5.0.15",
"@iarna/toml": "^2.2.5",
"@mui/material": "^5.15.19",
"@opencast/appkit": "^0.2.4",
"@opencast/appkit": "^0.3.0",
"@reduxjs/toolkit": "^2.2.5",
"@testing-library/jest-dom": "^6.4.5",
"@types/iarna__toml": "^2.0.5",
Expand Down
16 changes: 15 additions & 1 deletion src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
"subtitles": {
"selectSubtitleButton-tooltip": "Edit subtitles for {{title}}",
"selectSubtitleButton-tooltip-aria": "Select {{title}} for subtitle editing",
"createSubtitleButton-tooltip": "Opens a dialog for creating new subtitles",
"createSubtitleButton-tooltip": "Opens a dialog for creating/uploading new subtitles",
"createSubtitleButton-clicked-tooltip-aria": "Contains a dialog for creating new subtitles",
"createSubtitleButton-createButton": "Create",
"createSubtitleButton-createButton-tooltip": "Start a new subtitle file with the chosen title.",
Expand All @@ -257,6 +257,13 @@
"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-header": "Caution!",
"uploadButton-warning": "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 Expand Up @@ -311,5 +318,12 @@

"language": {
"language": "Language"
},

"modal": {
"areYouSure": "Are you sure?",
"cancel": "Cancel",
"close": "Close",
"confirm": "Confirm"
}
}
166 changes: 143 additions & 23 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 { LuChevronLeft, LuDownload, LuUpload } from "react-icons/lu";
import {
selectSubtitlesFromOpencastById,
} from "../redux/videoSlice";
Expand All @@ -16,11 +16,12 @@ 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";
import { generateButtonTitle } from "./SubtitleSelect";
import { ConfirmationModal, ConfirmationModalHandle, Modal, ModalHandle } from "@opencast/appkit";

/**
* Displays an editor view for a selected subtitle file
Expand All @@ -35,6 +36,8 @@ const SubtitleEditor: React.FC = () => {
const selectedId = useAppSelector(selectSelectedSubtitleId);
const captionTrack = useAppSelector(state => selectSubtitlesFromOpencastById(state, selectedId));
const theme = useTheme();
const modalRef = React.useRef<ModalHandle>(null);
const [uploadErrorMessage, setUploadErrorMessage] = useState<string | undefined>(undefined);

// Prepare subtitle in redux
useEffect(() => {
Expand All @@ -61,6 +64,17 @@ const SubtitleEditor: React.FC = () => {
}
}, [dispatch, captionTrack, subtitle, selectedId]);

// Display error modal
useEffect(() => {
if (modalRef.current && uploadErrorMessage) {
modalRef.current?.open();
}
if (modalRef.current && modalRef.current.close && !uploadErrorMessage) {
setUploadErrorMessage(undefined);
modalRef.current.close();
}
}, [uploadErrorMessage]);

const getTitle = () => {
if (subtitle) {
return generateButtonTitle(subtitle.tags, t);
Expand All @@ -84,6 +98,14 @@ const SubtitleEditor: React.FC = () => {
justifyContent: "space-between",
alignItems: "center",
width: "100%",
...(flexGapReplacementStyle(10, false)),
padding: "15px",
});

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

const subAreaStyle = css({
Expand All @@ -99,7 +121,6 @@ const SubtitleEditor: React.FC = () => {
borderBottom: `${theme.menuBorder}`,
});


const render = () => {
if (getError !== undefined) {
return (
Expand All @@ -110,10 +131,20 @@ const SubtitleEditor: React.FC = () => {
<>
<div css={headerRowStyle}>
<BackButton />
<div css={[titleStyle(theme), titleStyleBold(theme)]}>
<div css={[titleStyle(theme), titleStyleBold(theme), { padding: "0px" }]}>
{t("subtitles.editTitle", { title: getTitle() })}
</div>
<DownloadButton />
<div css={topRightButtons}>
<UploadButton setErrorMessage={setUploadErrorMessage} />
<DownloadButton />
<Modal
ref={modalRef}
title={t("subtitles.uploadButton-error")}
text={{ close: t("modal.close") }}
>
{uploadErrorMessage}
</Modal>
</div>
</div>
<div css={subAreaStyle}>
<SubtitleListEditor />
Expand All @@ -132,6 +163,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 = useAppSelector(selectSelectedSubtitleById);
Expand All @@ -151,18 +191,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 @@ -173,6 +205,102 @@ const DownloadButton: React.FC = () => {
);
};

const UploadButton: React.FC<{
setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>,
}> = ({
setErrorMessage,
}) => {

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

const [isFileUploadTriggered, setisFileUploadTriggered] = useState(false);
const subtitle = useAppSelector(selectSelectedSubtitleById);
const selectedId = useAppSelector(selectSelectedSubtitleId);
// Upload Ref
const inputRef = React.useRef<HTMLInputElement>(null);
// Modal Ref
const modalRef = React.useRef<ConfirmationModalHandle>(null);

const triggerFileUpload = () => {
modalRef.current?.done();
setisFileUploadTriggered(true);
};

useEffect(() => {
if (isFileUploadTriggered) {
inputRef.current?.click();
setisFileUploadTriggered(false);
}
}, [isFileUploadTriggered]);

// 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 not text
if (fileObj.type.split("/")[0] !== "text") {
setErrorMessage(t("subtitles.uploadButton-error-filetype"));
return;
}

const reader = new FileReader();
reader.onload = e => {
if (e.target && (e.target.result || e.target.result === "")) {
try {
const text = e.target.result.toString();
const subtitleParsed = parseSubtitle(text);
dispatch(setSubtitle({ identifier: selectedId, subtitles: { cues: subtitleParsed, tags: subtitle.tags } }));
} catch (e) {
console.error(e);
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={() => modalRef.current?.open()}
>
<LuUpload css={{ fontSize: "16px" }}/>
<span>{t("subtitles.uploadButton-title")}</span>
</div>
</ThemedTooltip>
{/* Hidden input field for upload */}
<input
style={{ display: "none" }}
ref={inputRef}
type="file"
accept="text/vtt"
onChange={event => uploadCallback(event)}
aria-hidden="true"
/>
<ConfirmationModal
title={t("subtitles.uploadButton-warning-header")}
buttonContent={t("modal.confirm")}
onSubmit={triggerFileUpload}
ref={modalRef}
text={{
cancel: t("modal.cancel"),
close: t("modal.close"),
areYouSure: t("modal.areYouSure"),
}}
>
{t("subtitles.uploadButton-warning")}
</ConfirmationModal>
</>
);
};


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

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
4 changes: 4 additions & 0 deletions src/util/utilityFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export function parseSubtitle(subtitle: string): SubtitleCue[] {
// - Pros: Parses styles, can also parse SRT, actively maintained
// - Cons: Uses node streaming library, can"t polyfill without ejecting CreateReactApp
// TODO: Parse caption
if (subtitle === "") {
throw new Error("File is empty");
}

const parser = new WebVTTParser();
const tree = parser.parse(subtitle, "metadata");
if (tree.errors.length !== 0) {
Expand Down

0 comments on commit 44afe77

Please sign in to comment.