From c2d92f67c0f9d9885f54d726001a02ae2aa57434 Mon Sep 17 00:00:00 2001 From: Gauravjeet Singh Date: Fri, 17 Feb 2023 21:01:33 +0530 Subject: [PATCH] feat: add subtitle recorder feature --- app.js | 4 + package-lock.json | 11 ++ package.json | 1 + www/configs/navigator-settings.json | 4 +- www/main/navigator/Navigator.jsx | 17 ++- .../navigator/misc/components/MiscContent.jsx | 2 + .../navigator/misc/components/MiscHeader.jsx | 19 ++- .../navigator/misc/components/RecordPane.jsx | 125 ++++++++++++++++++ www/main/navigator/misc/components/index.js | 1 + www/main/navigator/shabad/ShabadContent.jsx | 75 +++++++---- www/src/scss/navigator/misc/_misc.scss | 32 +++++ 11 files changed, 258 insertions(+), 33 deletions(-) create mode 100644 www/main/navigator/misc/components/RecordPane.jsx diff --git a/app.js b/app.js index 325b1854c..6889505a6 100644 --- a/app.js +++ b/app.js @@ -752,6 +752,10 @@ ipcMain.on('set-user-setting', (event, settingChanger) => { mainWindow.webContents.send('set-user-setting', settingChanger); }); +ipcMain.on('download-subtitle', (event, filename) => { + mainWindow.webContents.downloadURL(filename); +}) + module.exports = { openSecondaryWindow, appVersion, diff --git a/package-lock.json b/package-lock.json index d925aebcd..c37a07490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "scroll": "^3.0.1", "sharp": "^0.31.3", "socket.io": "^4.5.1", + "subtitle-generator": "^0.0.4", "tippy.js": "^6.3.7", "universal-analytics": "^0.5.3", "update": "^0.7.4", @@ -22731,6 +22732,11 @@ "node": ">=8" } }, + "node_modules/subtitle-generator": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/subtitle-generator/-/subtitle-generator-0.0.4.tgz", + "integrity": "sha512-mE2+E8iGWYCDWpkW8b3KCD0PckcBiHd2QIVby9nW73rozYG9A1+23IaxQe5p0N2dzxe2yeNyoqRdSXVJh4yesQ==" + }, "node_modules/success-symbol": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", @@ -42785,6 +42791,11 @@ "postcss-sorting": "^7.0.1" } }, + "subtitle-generator": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/subtitle-generator/-/subtitle-generator-0.0.4.tgz", + "integrity": "sha512-mE2+E8iGWYCDWpkW8b3KCD0PckcBiHd2QIVby9nW73rozYG9A1+23IaxQe5p0N2dzxe2yeNyoqRdSXVJh4yesQ==" + }, "success-symbol": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", diff --git a/package.json b/package.json index df8697259..055290885 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "scroll": "^3.0.1", "sharp": "^0.31.3", "socket.io": "^4.5.1", + "subtitle-generator": "^0.0.4", "tippy.js": "^6.3.7", "universal-analytics": "^0.5.3", "update": "^0.7.4", diff --git a/www/configs/navigator-settings.json b/www/configs/navigator-settings.json index 19e49a7c7..a12b17c18 100644 --- a/www/configs/navigator-settings.json +++ b/www/configs/navigator-settings.json @@ -40,5 +40,7 @@ "copyToClipboard": false }, "minimizedBySingleDisplay": false, - "isDontSaveHistory": false + "isDontSaveHistory": false, + "isSubtitleRecording": false, + "subtitleData": [] } diff --git a/www/main/navigator/Navigator.jsx b/www/main/navigator/Navigator.jsx index 4ef2b0976..ce42333df 100644 --- a/www/main/navigator/Navigator.jsx +++ b/www/main/navigator/Navigator.jsx @@ -12,8 +12,8 @@ const remote = require('@electron/remote'); const analytics = remote.getGlobal('analytics'); const Navigator = () => { - const { isSingleDisplayMode, akhandpatt } = useStoreState(state => state.userSettings); - const { setAkhandpatt } = useStoreState(state => state.userSettings); + const { isSingleDisplayMode, akhandpatt } = useStoreState((state) => state.userSettings); + const { setAkhandpatt } = useStoreState((state) => state.userSettings); const { minimizedBySingleDisplay, shortcuts, @@ -23,7 +23,8 @@ const Navigator = () => { isSundarGutkaBani, isCeremonyBani, ceremonyId, - } = useStoreState(state => state.navigator); + isSubtitleRecording, + } = useStoreState((state) => state.navigator); const { setShortcuts, setIsMiscSlide, @@ -32,9 +33,9 @@ const Navigator = () => { setIsSundarGutkaBani, setIsCeremonyBani, setCeremonyId, - } = useStoreActions(state => state.navigator); + } = useStoreActions((state) => state.navigator); - const addMiscSlide = givenText => { + const addMiscSlide = (givenText) => { if (isAnnoucement) { setIsAnnoucement(false); } @@ -134,6 +135,12 @@ const Navigator = () => {
+ {isSubtitleRecording && ( +
+ + Recording +
+ )} { const [{ miscPanel }] = useDataLayerValue(); @@ -12,6 +13,7 @@ export const MiscContent = () => { + ); }; diff --git a/www/main/navigator/misc/components/MiscHeader.jsx b/www/main/navigator/misc/components/MiscHeader.jsx index de45271b8..47c6d94f8 100644 --- a/www/main/navigator/misc/components/MiscHeader.jsx +++ b/www/main/navigator/misc/components/MiscHeader.jsx @@ -3,7 +3,7 @@ import { useDataLayerValue } from '../state-manager/DataLayer'; export const MiscHeader = () => { const [{ miscPanel }, dispatch] = useDataLayerValue(); - const SetOpenTab = event => { + const SetOpenTab = (event) => { dispatch({ type: 'SET_PANEL', miscPanel: event.target.textContent, @@ -12,11 +12,12 @@ export const MiscHeader = () => { const isHistory = miscPanel === 'History'; const isInsert = miscPanel === 'Insert'; const isOther = miscPanel === 'Others'; + const isRecord = miscPanel === 'Record'; return (
SetOpenTab(event)} + onClick={(event) => SetOpenTab(event)} > @@ -27,7 +28,7 @@ export const MiscHeader = () => { SetOpenTab(event)} + onClick={(event) => SetOpenTab(event)} > @@ -35,9 +36,19 @@ export const MiscHeader = () => { + SetOpenTab(event)} + > + + + Record + + + SetOpenTab(event)} + onClick={(event) => SetOpenTab(event)} > diff --git a/www/main/navigator/misc/components/RecordPane.jsx b/www/main/navigator/misc/components/RecordPane.jsx new file mode 100644 index 000000000..06b83e802 --- /dev/null +++ b/www/main/navigator/misc/components/RecordPane.jsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useStoreState, useStoreActions } from 'easy-peasy'; +import { ipcRenderer } from 'electron/renderer'; +import anvaad from 'anvaad-js'; + +import { generateSRT } from 'subtitle-generator'; + +const path = require('path'); +const remote = require('@electron/remote'); +const { i18n } = remote.require('./app'); +const fs = require('fs'); + +export const RecordPane = ({ className }) => { + const { isSubtitleRecording, subtitleData } = useStoreState((state) => state.navigator); + const { setIsSubtitleRecording } = useStoreActions((state) => state.navigator); + const [fileList, setFileList] = useState([]); + + const userData = remote.app.getPath('userData'); + const directory = path.resolve(userData, 'subtitles'); + + const startRecording = () => { + if (!isSubtitleRecording) { + setIsSubtitleRecording(true); + } + }; + + const stopRecording = () => { + const parsed = subtitleData.map((data, index) => { + return { + id: index, + seconds: data.seconds, + content: anvaad.unicode(data.content), + }; + }); + const srt = generateSRT(parsed, 'seconds'); + if (isSubtitleRecording) { + setIsSubtitleRecording(false); + } + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory); + } + const currentDate = new Date(); + const filename = `${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}-${currentDate.getHours()}${currentDate.getMinutes()}${currentDate.getSeconds()}.srt`; + const filePath = path.resolve(directory, filename); + + fs.writeFile(filePath, srt, updateList); + }; + + const updateList = () => { + fs.readdir(directory, (error, files) => { + const sortedFiles = files + .map((fileName) => ({ + name: fileName, + time: fs.statSync(`${directory}/${fileName}`).mtime.getTime(), + })) + .sort((a, b) => b.time - a.time) + .map((file) => file.name); + setFileList(sortedFiles); + }); + }; + + useEffect(() => { + updateList(); + }); + + const download = (fileName) => { + const filePath = path.resolve(directory, fileName); + ipcRenderer.send('download-subtitle', filePath); + }; + + const deleteFile = (filename) => { + const filePath = path.resolve(directory, filename); + fs.unlink(filePath, (err) => { + if (err) throw err; + }); + updateList(); + }; + + return ( +
+

Record Subtitles

+

To generate subtitles as you go through the shabads, click on the record button below:

+ {isSubtitleRecording ? ( + + ) : ( + + )} + {fileList.length > 0 &&

Previous recordings

} + {fileList.map((file) => { + return ( +
+

{file}

+
+ + +
+
+ ); + })} +
+ ); +}; + +RecordPane.propTypes = { + className: PropTypes.string, +}; diff --git a/www/main/navigator/misc/components/index.js b/www/main/navigator/misc/components/index.js index 290246dd9..1037b1fa5 100644 --- a/www/main/navigator/misc/components/index.js +++ b/www/main/navigator/misc/components/index.js @@ -2,3 +2,4 @@ export { HistoryPane } from './HistoryPane'; export { InsertPane } from './InsertPane'; export { OtherPane } from './OtherPane'; export { MiscPane } from './MiscPane'; +export { RecordPane } from './RecordPane'; diff --git a/www/main/navigator/shabad/ShabadContent.jsx b/www/main/navigator/shabad/ShabadContent.jsx index 2c41c14b0..19045a511 100644 --- a/www/main/navigator/shabad/ShabadContent.jsx +++ b/www/main/navigator/shabad/ShabadContent.jsx @@ -32,7 +32,9 @@ const ShabadContent = () => { minimizedBySingleDisplay, isDontSaveHistory, savedCrossPlatformId, - } = useStoreState(state => state.navigator); + isSubtitleRecording, + subtitleData, + } = useStoreState((state) => state.navigator); const { setVersesRead, @@ -43,16 +45,17 @@ const ShabadContent = () => { setVerseHistory, setIsMiscSlide, setIsDontSaveHistory, - } = useStoreActions(state => state.navigator); + } = useStoreActions((state) => state.navigator); // mangalPosition was removed from below settings const { autoplayToggle, autoplayDelay, baniLength, liveFeed } = useStoreState( - state => state.userSettings, + (state) => state.userSettings, ); const [previousActiveVerse, setPreviousActiveVerse] = useState(activeVerseId); const [activeShabad, setActiveShabad] = useState([]); const [activeVerse, setActiveVerse] = useState({}); + const [timestamp, setTimestamp] = useState(); const activeVerseRef = useRef(null); const virtuosoRef = useRef(null); @@ -63,6 +66,14 @@ const ShabadContent = () => { extralong: 'existsBuddhaDal', }; + useEffect(() => { + if(isSubtitleRecording) { + setTimestamp(new Date()); + } else { + setTimestamp(''); + } + }, [isSubtitleRecording]); + const filterRequiredVerseItems = (verses) => { try { verses = verses.flat(1); @@ -88,7 +99,7 @@ const ShabadContent = () => { const filterOverlayVerseItems = (verses, verseId = activeVerseId) => { if (verses) { - const currentIndex = verses.findIndex(obj => obj.ID === verseId); + const currentIndex = verses.findIndex((obj) => obj.ID === verseId); const currentVerse = verses[currentIndex]; if (currentVerse) { const Line = { ...currentVerse.toJSON() }; @@ -129,11 +140,11 @@ const ShabadContent = () => { currentShabad = activeShabadId; } const currentIndex = verseHistory.findIndex( - historyObj => historyObj.shabadId === currentShabad, + (historyObj) => historyObj.shabadId === currentShabad, ); if (verseHistory[currentIndex]) { verseHistory[currentIndex].continueFrom = newTraversedVerse; - if (!versesRead.some(traversedVerse => traversedVerse === newTraversedVerse)) { + if (!versesRead.some((traversedVerse) => traversedVerse === newTraversedVerse)) { verseHistory[currentIndex].versesRead = [...versesRead, newTraversedVerse]; setVersesRead([...versesRead, newTraversedVerse]); } @@ -141,12 +152,20 @@ const ShabadContent = () => { setActiveVerse({ [verseIndex]: newTraversedVerse }); if (activeVerseId !== newTraversedVerse) { setActiveVerseId(newTraversedVerse); + if (isSubtitleRecording) { + const currentTimestamp = new Date(); + subtitleData.push({ + seconds: Math.abs(currentTimestamp - timestamp)/1000, + content: activeShabad[verseIndex].Gurmukhi, + }); + setTimestamp(currentTimestamp); + } } if (window.socket !== undefined && window.socket !== null) { let baniVerse; if (!crossPlatformId) { - baniVerse = activeShabad.find(obj => obj.ID === newTraversedVerse); + baniVerse = activeShabad.find((obj) => obj.ID === newTraversedVerse); } if (isSundarGutkaBani) { window.socket.emit('data', { @@ -185,7 +204,7 @@ const ShabadContent = () => { const openNextVerse = () => { if (Object.entries(activeVerse).length !== 0) { const mappedShabadArray = filterRequiredVerseItems(activeShabad); - Object.keys(activeVerse).forEach(activeVerseIndex => { + Object.keys(activeVerse).forEach((activeVerseIndex) => { if (mappedShabadArray.length - 1 > parseInt(activeVerseIndex, 10)) { const newVerseIndex = parseInt(activeVerseIndex, 10) + 1; const newVerseId = mappedShabadArray[newVerseIndex].verseId; @@ -198,7 +217,7 @@ const ShabadContent = () => { const openPrevVerse = () => { if (Object.entries(activeVerse).length !== 0) { const mappedShabadArray = filterRequiredVerseItems(activeShabad); - Object.keys(activeVerse).forEach(activeVerseIndex => { + Object.keys(activeVerse).forEach((activeVerseIndex) => { if (parseInt(activeVerseIndex, 10) > 0) { const newVerseIndex = parseInt(activeVerseIndex, 10) - 1; const newVerseId = mappedShabadArray[newVerseIndex].verseId; @@ -208,8 +227,8 @@ const ShabadContent = () => { } }; - const scrollToVerse = verseId => { - const verseIndex = activeShabad.findIndex(obj => obj.ID === verseId); + const scrollToVerse = (verseId) => { + const verseIndex = activeShabad.findIndex((obj) => obj.ID === verseId); virtuosoRef.current.scrollToIndex({ index: verseIndex, behavior: 'smooth', @@ -226,7 +245,7 @@ const ShabadContent = () => { if (homeVerseId === activeVerseId) { const previousVerseIndex = activeShabad.findIndex( - verseObj => verseObj.ID === previousActiveVerse, + (verseObj) => verseObj.ID === previousActiveVerse, ); if (previousVerseIndex >= 0) { @@ -244,10 +263,10 @@ const ShabadContent = () => { } }; - const changeHomeVerse = verseIndex => { + const changeHomeVerse = (verseIndex) => { if (homeVerse !== verseIndex) { const currentIndex = verseHistory.findIndex( - historyObj => historyObj.shabadId === activeShabadId, + (historyObj) => historyObj.shabadId === activeShabadId, ); if (verseHistory[currentIndex]) { verseHistory[currentIndex].homeVerse = verseIndex; @@ -270,11 +289,11 @@ const ShabadContent = () => { const firstVerse = verses[0]; let shabadId = firstVerse.Shabads ? firstVerse.Shabads[0].ShabadID : firstVerse.shabadId; const verseId = initialVerse || firstVerse.ID; - const firstVerseIndex = verses.findIndex(v => v.ID === verseId); + const firstVerseIndex = verses.findIndex((v) => v.ID === verseId); let verse; if (verseType === 'shabad') { if (initialVerse) { - const clickedVerse = verses.filter(verseObj => { + const clickedVerse = verses.filter((verseObj) => { return verseObj.ID === initialVerse; }); verse = clickedVerse.length && clickedVerse[0].Gurmukhi; @@ -288,7 +307,7 @@ const ShabadContent = () => { verse = firstVerse.ceremonyName; shabadId = firstVerse.ceremonyId; } - const check = verseHistory.filter(historyObj => historyObj.shabadId === shabadId); + const check = verseHistory.filter((historyObj) => historyObj.shabadId === shabadId); if (check.length === 0) { const updatedHistory = [ { @@ -313,7 +332,7 @@ const ShabadContent = () => { const scrollToView = () => { setTimeout(() => { - const currentIndex = activeShabad.findIndex(obj => obj.ID === activeVerseId); + const currentIndex = activeShabad.findIndex((obj) => obj.ID === activeVerseId); virtuosoRef.current.scrollToIndex({ index: currentIndex, behavior: 'smooth', @@ -337,7 +356,7 @@ const ShabadContent = () => { useEffect(() => { const baniVerseIndex = activeShabad.findIndex( - obj => obj.crossPlatformID === savedCrossPlatformId, + (obj) => obj.crossPlatformID === savedCrossPlatformId, ); if (baniVerseIndex >= 0) { updateTraversedVerse(activeShabad[baniVerseIndex].ID, baniVerseIndex); @@ -347,16 +366,16 @@ const ShabadContent = () => { useEffect(() => { if (isSundarGutkaBani && sundarGutkaBaniId) { // mangalPosition was removed from loadBani 3rd argument - loadBani(sundarGutkaBaniId, baniLengthCols[baniLength]).then(sundarGutkaVerses => { + loadBani(sundarGutkaBaniId, baniLengthCols[baniLength]).then((sundarGutkaVerses) => { setActiveShabad(sundarGutkaVerses); saveToHistory(sundarGutkaVerses, 'bani'); - const check = sundarGutkaVerses.findIndex(verse => verse.ID === activeVerseId); + const check = sundarGutkaVerses.findIndex((verse) => verse.ID === activeVerseId); if (check < 0) { openFirstVerse(sundarGutkaVerses[0].ID, sundarGutkaVerses[0].crossPlatformID); } }); } else if (isCeremonyBani && ceremonyId) { - loadCeremony(ceremonyId).then(ceremonyVerses => { + loadCeremony(ceremonyId).then((ceremonyVerses) => { if (ceremonyVerses) { setActiveShabad(ceremonyVerses); const newEntry = saveToHistory(ceremonyVerses, 'ceremony'); @@ -366,10 +385,20 @@ const ShabadContent = () => { } }); } else { - loadShabad(activeShabadId, initialVerseId).then(verses => { + loadShabad(activeShabadId, initialVerseId).then((verses) => { if (verses) { setActiveShabad(verses); if (initialVerseId) { + if (isSubtitleRecording) { + const mappedShabadArray = filterRequiredVerseItems(verses); + const currentIndex = mappedShabadArray.findIndex((obj) => obj.verseId === initialVerseId); + const currentTimestamp = new Date(); + subtitleData.push({ + seconds: Math.abs(currentTimestamp - timestamp)/1000, + content: mappedShabadArray[currentIndex].verse, + }); + setTimestamp(currentTimestamp); + } if (!isDontSaveHistory) { saveToHistory(verses, 'shabad', initialVerseId); } diff --git a/www/src/scss/navigator/misc/_misc.scss b/www/src/scss/navigator/misc/_misc.scss index a9a226cde..77b44a038 100644 --- a/www/src/scss/navigator/misc/_misc.scss +++ b/www/src/scss/navigator/misc/_misc.scss @@ -155,4 +155,36 @@ $misc-footer-active: 70px; } } } + .subtitle-pane { + padding: 0 16px 80px 16px; + overflow-y: auto; + + .subtitle-item { + border-bottom: 1px dashed $not-so-black; + display: flex; + flex-direction: row; + justify-content: space-between; + + button { + background-color: $dull-blue; + border-radius: 8px; + margin: 8px; + } + } + + } } +.subtitle-status { + background-color: white; + border: 2px dashed $red; + color: $red; + display: flex; + padding: 8px; + position: absolute; + right: 20px; + z-index: 100; + + > span { + margin-left: 8px; + } +} \ No newline at end of file