diff --git a/public/electron.js b/public/electron.js index 96c38ca..844dcba 100644 --- a/public/electron.js +++ b/public/electron.js @@ -182,6 +182,71 @@ ipcMain.on("getActReactCouples", (event, arg) => { } }); +ipcMain.on("getSubtitlesSettings", (event, arg) => { + if (isDev) { + let res = { + statusCode: 200, + message: "OK", + data: { + length: 2, + text_fields: [ + { + "uuid": "27705058-7a54-4057-ad17-a810c08e8db9", + "name": "Text lambda 1", + }, + { + "uuid": "4f712d61-094a-4b7b-9905-4fa928329de4", + "name": "Text EN", + } + ], + } + }; + event.returnValue = res; + return res; + } else { + return tcpConn.getSubtitlesSettings().then((res) => { + console.log("getSubtitlesSettings : " + res); + event.returnValue = res; + }); + } +}); + +ipcMain.on("getAllTextFields", (event, arg) => { + if (isDev) { + let res = { + statusCode: 200, + message: "OK", + data: { + length: 3, + text_fields: [ + { + "name": "Text lambda 1", + "parent_scene": "Scene 1", + "uuid": "27705058-7a54-4057-ad17-a810c08e8db9", + }, + { + "name": "Text EN", + "parent_scene": "Scene 1", + "uuid": "4f712d61-094a-4b7b-9905-4fa928329de4", + }, + { + "name": "Blop 3", + "parent_scene": "Scene 2", + "uuid": "4fa92094a-832-94fde4-712d61-4b7b-9905", + } + ], + } + }; + event.returnValue = res; + return res; + } else { + return tcpConn.getAllTextFields().then((res) => { + console.log("getAllTextFields : " + res); + event.returnValue = res; + }); + } +}); + ipcMain.on("setCompressorLevel", (event, arg) => { if (isDev) { let res = { @@ -263,6 +328,14 @@ ipcMain.on("scenes-updated", (evt, arg) => { // Get from socket broadcast mainWindow.webContents.send('scenes-updated'); // To renderer }); +ipcMain.on("actions-reactions-updated", (evt, arg) => { // Get from socket broadcast + mainWindow.webContents.send('actions-reactions-updated'); // To renderer +}); + +ipcMain.on("subtitles-updated", (evt, arg) => { // Get from socket broadcast + mainWindow.webContents.send('subtitles-updated'); // To renderer +}); + ipcMain.on("connection-server-lost", (evt, arg) => { // Quit main app if (mainWindow) diff --git a/src/Components/ActionsReactions/ActionsReactions.tsx b/src/Components/ActionsReactions/ActionsReactions.tsx index 0e841f7..549aa6f 100644 --- a/src/Components/ActionsReactions/ActionsReactions.tsx +++ b/src/Components/ActionsReactions/ActionsReactions.tsx @@ -1,4 +1,4 @@ -import React, { Component, useEffect, useState } from "react"; +import React, { useEffect } from "react"; import "./ActionsReactions.css"; import Card from "@mui/material/Card"; @@ -133,7 +133,18 @@ export const ActionsReactions = () => { // Hook to load actions and reactions on component mount useEffect(() => { - ipcRenderer.on('scenes-updated', (evt: any, message: any) => { + const handleActionReactionUpdated = (evt: any, message: any) => { + getActionReactionFromServer().then((res) => { + if (res.statusCode === 200) { + toast("Actions/Reactions have been updated !", { + type: "info", + }); + console.log("New Array", res); + setActionsReactionsList(res.data.actReacts); + } + }); + }; + const handleScenesUpdated = (evt: any, message: any) => { getAllScenes().then((res) => { if (res.statusCode === 200) { toast("Scenes have been updated.", { @@ -142,7 +153,11 @@ export const ActionsReactions = () => { setAvailableScenes(res.data.scenes); } }); - }); + }; + + ipcRenderer.on('actions-reactions-updated', handleActionReactionUpdated); + ipcRenderer.on('scenes-updated', handleScenesUpdated); + async function sleep(): Promise { return new Promise((resolve) => { getActionReactionFromServer().then((res) => { @@ -174,6 +189,11 @@ export const ActionsReactions = () => { } sleep().then((res) => setload(!res)); + + return () => { + ipcRenderer.removeListener('actions-reactions-updated', handleActionReactionUpdated); + ipcRenderer.removeListener('scenes-updated', handleScenesUpdated); + }; }, []); /** diff --git a/src/Components/ActionsReactions/AddAppLaunch/AppLaunchModal/AppLaunchModal.tsx b/src/Components/ActionsReactions/AddAppLaunch/AppLaunchModal/AppLaunchModal.tsx index 4da3640..4ca4022 100644 --- a/src/Components/ActionsReactions/AddAppLaunch/AppLaunchModal/AppLaunchModal.tsx +++ b/src/Components/ActionsReactions/AddAppLaunch/AppLaunchModal/AppLaunchModal.tsx @@ -172,7 +172,7 @@ export const AppLaunchModal = (props: any) => { - sources + reactions { - sources + reactions { >([]); const [load, setload] = React.useState(true); const [point, setpoint] = React.useState("."); + const axiosPrivate = useAxiosPrivate(); const style = { @@ -31,8 +32,6 @@ export const CompressorLevel = () => { }, }; - let timeoutCommit: NodeJS.Timeout | undefined = undefined; - const getAllCompressors = (): Promise => { return new Promise(async (resolve, reject) => { const result: AllMics = await ipcRenderer.sendSync("getAllMics", "ping"); @@ -69,13 +68,16 @@ export const CompressorLevel = () => { }; useEffect(() => { - ipcRenderer.on('compressor-level-updated', (evt: any, message: any) => { + const handleCompressorLevelUpdated = (evt: any, message: any) => { getAllCompressors().then((res) => { if (res.statusCode === 200) { setExampleCompressorArray(res.data.mics); } }); - }) + }; + + ipcRenderer.on('compressor-level-updated', handleCompressorLevelUpdated); + async function sleep(): Promise { return new Promise((resolve) => { getAllCompressors().then((res) => { @@ -88,6 +90,10 @@ export const CompressorLevel = () => { } sleep().then((res) => setload(res)); + + return () => { + ipcRenderer.removeListener('compressor-level-updated', handleCompressorLevelUpdated); + }; }, []); function addpoint() { diff --git a/src/Components/Slider/Slider.tsx b/src/Components/Slider/Slider.tsx index 0cc2e65..9d94a7e 100644 --- a/src/Components/Slider/Slider.tsx +++ b/src/Components/Slider/Slider.tsx @@ -105,6 +105,7 @@ const CustomizedSlider = (props: any) => { sx={{ display: "flex", alignItems: "center", + justifyContent: "space-evenly", p: 1, m: 1, }} @@ -120,7 +121,7 @@ const CustomizedSlider = (props: any) => { /> diff --git a/src/Components/Subtitles/Subtitles.css b/src/Components/Subtitles/Subtitles.css new file mode 100644 index 0000000..fef7cd4 --- /dev/null +++ b/src/Components/Subtitles/Subtitles.css @@ -0,0 +1,21 @@ +.subtitlesSettingsBox { + border: 3px solid orange; + background-color: #565d68; + /* rounded */ + border-radius: 10px; + + overflow:scroll; + overflow-y:scroll; + overflow-x:hidden; + + max-height: 65vh; +} + +.subtitlesSettingsItem:hover { + box-shadow: 20px; + transform: scale(1.02); +} + +.MuiListItemText-secondary { + color: orange !important; +} \ No newline at end of file diff --git a/src/Components/Subtitles/Subtitles.tsx b/src/Components/Subtitles/Subtitles.tsx index a20f425..2a7a8ce 100644 --- a/src/Components/Subtitles/Subtitles.tsx +++ b/src/Components/Subtitles/Subtitles.tsx @@ -1,117 +1,397 @@ -import { Switch } from "@mui/material"; -import { Box } from "@mui/system"; -import React from "react"; -import CSS from 'csstype'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, List, ListItem, ListItemText, Switch, TextField } from "@mui/material"; +import React, { useEffect } from "react"; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; -import FormHelperText from '@mui/material/FormHelperText'; -import FormControl from '@mui/material/FormControl'; import Select, { SelectChangeEvent } from '@mui/material/Select'; -import { languages } from "../Language/LanguageData"; -import Grid from '@mui/material/Grid'; -import { LocalStorage } from "../../LocalStorage/LocalStorage"; -import { resultFormat } from "../../Socket/interfaces"; +import { AllSubtitlesSettings, AllTextFields, TextFieldSimple, TextFieldDetailed, resultFormat } from "../../Socket/interfaces"; +import { toast } from "react-toastify"; +import { BsTrash } from "react-icons/bs"; +import "./Subtitles.css"; const ipcRenderer = window.require('electron').ipcRenderer export const Subtitles = () => { - const boxStyles: CSS.Properties = { - border: "solid", - borderColor: "#FD7014", - borderWidth: "4", - paddingLeft: "3vw", - paddingRight: "3vw", - paddingTop: "2vh", - paddingBottom: "2vh", - backgroundColor: "#222831", - borderRadius: "15px", - alignItems: "center", - cursor: "pointer", - borderCollapse: "separate", - borderSpacing: "0 0vh", - marginBottom: "1vh", - position: "relative", - minWidth: "calc(40vw + 231px)", - maxWidth: "calc(40vw + 231px)", - lineHeight: 1.6, - justifyContent: "center", + // Dialog + const [open, setOpen] = React.useState(false); + const [newSubtitleParam, setNewSubtitleParam] = React.useState(""); + + // Loading + const [load, setload] = React.useState(true); + const [point, setpoint] = React.useState("."); + + + + const [subtitlesSettings, setSubtitlesSettings] = React.useState([]); + + const [availableTextFields, setAvailableTextFields] = React.useState([]); + + /** + * Event handler for opening the dialog + */ + const handleClickOpen = () => { + setOpen(true); }; - const itemStyles: CSS.Properties = { - padding: "25px !important", - justifyContent: "center", - alignItems: "center" + /** + * Event handler for canceling the dialog + */ + const handleCancel = () => { + setOpen(false); }; - const [subtitlesActivated, setSubtitlesActivated] = React.useState(LocalStorage.getItemObject("subtitlesActivated") || false); - const [languageSelected, setLanguageSelected] = React.useState(LocalStorage.getItemObject("languageSelected") || "en"); + + const addSubtitleTextField = (uuid: string): Promise => { + return new Promise(async (resolve, reject) => { + const param = { + enable: true, + uuid: uuid, + } + const result: resultFormat = ipcRenderer.sendSync('setSubtitles', param); + resolve(result); + }); + } - const updateChangesToServer = (): Promise => { + const removeSubtitleTextField = (uuid: string): Promise => { return new Promise(async (resolve, reject) => { - let params = { - enable: subtitlesActivated, - language: languageSelected + const param = { + enable: false, + uuid: uuid, } - const result: resultFormat = await ipcRenderer.sendSync('setSubtitles', params); - console.log('updateChangesToServer invoke', result); - resolve(result) - }); + const result: resultFormat = ipcRenderer.sendSync('setSubtitles', param); + resolve(result); + }); + } + + const getSubtitlesSettings = (): Promise => { + return new Promise(async (resolve, reject) => { + const result: any = await ipcRenderer.sendSync( + "getSubtitlesSettings", + "ping" + ); + resolve(result); + }); } + + const getAllTextFields = (): Promise => { + return new Promise(async (resolve, reject) => { + const result: any = await ipcRenderer.sendSync( + "getAllTextFields", + "ping" + ); + resolve(result); + }); + } + + const handleSave = () => { + if (newSubtitleParam === "") { + toast("You must select a text field to add", { + type: "error", + }); + return; + } - const handleToggleChange = (event: SelectChangeEvent) => { - LocalStorage.setItemObject("subtitlesActivated", !subtitlesActivated) - setSubtitlesActivated(!subtitlesActivated); + const textField: TextFieldDetailed = JSON.parse(newSubtitleParam); + addSubtitleTextField(textField.uuid).then((res: resultFormat) => { + if (res.statusCode === 200) { + + // Reset + setNewSubtitleParam(""); - updateChangesToServer(); - }; + // Refresh + getSubtitlesSettings().then((subtitles_res) => { + if (subtitles_res.statusCode === 200) { + console.log("getSubtitlesSettings", subtitles_res); + setSubtitlesSettings(subtitles_res.data.text_fields); + + // Remove the text field from availableTextFields + const availableTextFieldsCopy: TextFieldDetailed[] = availableTextFields.slice(); + const index = availableTextFieldsCopy.findIndex((tf: TextFieldDetailed) => { + return tf.uuid === textField.uuid; + }); - const handleSelectChange = (event: any) => { - LocalStorage.setItemObject("languageSelected", event.target.value) - setLanguageSelected(event.target.value); + if (index !== -1) { + availableTextFieldsCopy.splice(index, 1); - updateChangesToServer(); - }; + setAvailableTextFields(availableTextFieldsCopy); + + if (availableTextFields.length > 0) { + setNewSubtitleParam( JSON.stringify(availableTextFields[0]) ) + } + } + + // toast("Subtitle text field added !", { + // type: "success", + // }); + + // Close the dialog + setOpen(false); + + return; + + } else { + toast("Error listing all subtitles settings. Verify the internet connection", { + type: "error", + }); + return; + } + }); + } else { + toast("Error adding subtitle text field", { + type: "error", + }); + return; + } + }); + } + + const deleteSubtitleTextField = (uuid: string) => { + return () => { + removeSubtitleTextField(uuid).then((res: resultFormat) => { + if (res.statusCode === 200) { + + // remove locally + const subtitlesSettingsCopy: TextFieldSimple[] = subtitlesSettings.slice(); + const index = subtitlesSettingsCopy.findIndex((tf: TextFieldSimple) => { + return tf.uuid === uuid; + }); + + if (index !== -1) { + subtitlesSettingsCopy.splice(index, 1); + setSubtitlesSettings(subtitlesSettingsCopy); + } + + // toast("Subtitle text field deleted !", { + // type: "success", + // }); + + // Refresh + getSubtitlesSettings().then((subtitles_res) => { + if (subtitles_res.statusCode === 200) { + console.log("getSubtitlesSettings", subtitles_res); + setSubtitlesSettings(subtitles_res.data.text_fields); + + getAllTextFields().then((txt_field_res) => { + if (txt_field_res.statusCode === 200) { + console.log("getAllTextFields", txt_field_res); + + // Filter txt_field_res.data.text_fields to only keep the ones that are not in subtitlesSettings + const availableTextFields: TextFieldDetailed[] = txt_field_res.data.text_fields.filter((textField: TextFieldDetailed) => { + return !subtitles_res.data.text_fields?.some((subtitlesSetting: TextFieldSimple) => { + return subtitlesSetting.uuid === textField.uuid; + }); + }); + + setAvailableTextFields(availableTextFields); + + if (availableTextFields.length > 0) { + setNewSubtitleParam( JSON.stringify(availableTextFields[0]) ) + } + + return; + } + }); + + + } else { + toast("Error listing all subtitles settings. Verify the internet connection", { + type: "error", + }); + return; + } + }); + } else { + toast("Error deleting subtitle text field", { + type: "error", + }); + return; + } + }); + } + } + + useEffect(() => { + const handleSubtitlesUpdated = (evt: any, message: any) => { + getSubtitlesSettings().then((subtitles_res) => { + if (subtitles_res.statusCode === 200) { + console.log("getSubtitlesSettings", subtitles_res); + setSubtitlesSettings(subtitles_res.data.text_fields); + + getAllTextFields().then((txt_field_res) => { + if (txt_field_res.statusCode === 200) { + toast("Subtitles settings have been updated !", { + type: "info", + }); + console.log("New Array", txt_field_res); + + // Filter txt_field_res.data.text_fields to only keep the ones that are not in subtitlesSettings + const availableTextFields: TextFieldDetailed[] = txt_field_res.data.text_fields.filter((textField: TextFieldDetailed) => { + return !subtitles_res.data.text_fields?.some((subtitlesSetting: TextFieldSimple) => { + return subtitlesSetting.uuid === textField.uuid; + }); + }); + + setAvailableTextFields(availableTextFields); + + if (availableTextFields.length > 0) { + setNewSubtitleParam( JSON.stringify(availableTextFields[0]) ) + } + } + }); + } + }); + }; + + ipcRenderer.on('subtitles-updated', handleSubtitlesUpdated); + + async function sleep(): Promise { + return new Promise((resolve) => { + getSubtitlesSettings().then((subtitles_res) => { + if (subtitles_res.statusCode === 200) { + console.log("getSubtitlesSettings", subtitles_res); + setSubtitlesSettings(subtitles_res.data.text_fields); + + getAllTextFields().then((txt_field_res) => { + if (txt_field_res.statusCode === 200) { + console.log("getAllTextFields", txt_field_res); + + // Filter txt_field_res.data.text_fields to only keep the ones that are not in subtitlesSettings + const availableTextFields: TextFieldDetailed[] = txt_field_res.data.text_fields.filter((textField: TextFieldDetailed) => { + return !subtitles_res.data.text_fields?.some((subtitlesSetting: TextFieldSimple) => { + return subtitlesSetting.uuid === textField.uuid; + }); + }); + + setAvailableTextFields(availableTextFields); + + if (availableTextFields.length > 0) { + setNewSubtitleParam( JSON.stringify(availableTextFields[0]) ) + } + + resolve(true) + + } else { + toast("Error listing all text fields. Verify the internet connection", { + type: "error", + }); + resolve(false); + } + }); + } else { + toast("Error listing all subtitles settings. Verify the internet connection", { + type: "error", + }); + resolve(false); + } + }); + }); + } + + sleep().then((res) => setload(!res)); + + return () => { + ipcRenderer.removeListener('subtitles-updated', handleSubtitlesUpdated); + }; + }, []) + + /** + * Function to add points to the loading text + */ + function addpoint() { + { + setInterval(() => { + point.length >= 3 ? setpoint(".") : setpoint(point + "."); + }, 2000); + } + } return ( <> - - - -

Enable subtitles during the stream:

- -
- + {load ? ( + <> +

Easystream is loading

+

{point}

+ {addpoint()} + + ) : ( + <> { - subtitlesActivated === true ? ( - - - - Language - - Select language - - - ) : (<>) + + + + ) } -
-
+ + + Link Subtitle Text Field + + + Text fields are used to display subtitles on the stream. You can + link as many text fields as you want. + + + All text fields available + + + + + + + + + +
+ +
+ + )} ); } diff --git a/src/Socket/interfaces.ts b/src/Socket/interfaces.ts index 9daef09..315a0f1 100644 --- a/src/Socket/interfaces.ts +++ b/src/Socket/interfaces.ts @@ -79,6 +79,35 @@ export interface AllScenes { }; } +export interface TextFieldSimple { + name: string; + uuid: string; +} + +export interface TextFieldDetailed { + name: string; + uuid: string; + parent_scene: string; +} + +export interface AllSubtitlesSettings { + statusCode: number; + messages: string; + data: { + length: number; + text_fields: [TextFieldSimple]; + }; +} + +export interface AllTextFields { + statusCode: number; + messages: string; + data: { + length: number; + text_fields: [TextFieldDetailed]; + } +} + export interface TimeRange { timeRange: number; ref: "seconds" | "minutes" | "ad vitam aeternam"; diff --git a/src/Socket/socket.js b/src/Socket/socket.js index 0c9ac76..aacf119 100644 --- a/src/Socket/socket.js +++ b/src/Socket/socket.js @@ -68,10 +68,14 @@ class TCPConnection { if (payload.message === 'BROADCAST') { let type = payload.data.type - if (type === 'audioSourceCreated' || type === 'audioSourceRemoved' || type === 'audioSourceNameChanged' || type === 'micLevelChanged') { + if (type === 'audioSourceCreated' || type === 'audioSourceRemoved' || type === 'audioSourceNameChanged' || type === 'compressorSettingsChanged') { this.ipcMain.emit('compressor-level-updated') } else if (type === 'sceneCreated' || type === 'sceneRemoved' || type === 'sceneNameChanged') { this.ipcMain.emit('scenes-updated') + } else if (type === 'areasChanged') { + this.ipcMain.emit('actions-reactions-updated') + } else if (type === 'subtitlesSettingsChanged') { + this.ipcMain.emit('subtitles-updated') } } } @@ -183,6 +187,46 @@ class TCPConnection { }); } + getSubtitlesSettings() { + let obj = { + command: 'getSubtitlesSettings', + }; + console.log('getSubtitlesSettings -> ', JSON.stringify(obj)); + return new Promise((resolve, reject) => { + this.sendData(obj, (data, error) => { + if (data) { + console.log('getSubtitlesSettings resolve', data); + resolve(data); + } else { + console.log('getSubtitlesSettings error', error); + this.socket.end(); + this.ipcMain.emit('connection-server-lost') + reject(error); + } + }); + }); + } + + getAllTextFields() { + let obj = { + command: 'getAllTextFields', + }; + console.log('getAllTextFields -> ', JSON.stringify(obj)); + return new Promise((resolve, reject) => { + this.sendData(obj, (data, error) => { + if (data) { + console.log('getAllTextFields resolve', data); + resolve(data); + } else { + console.log('getAllTextFields error', error); + this.socket.end(); + this.ipcMain.emit('connection-server-lost') + reject(error); + } + }); + }); + } + setVolumeToMic(args) { let obj = { command: 'setCompressorLevel',