diff --git a/client/src/app/api/apiConstants.js b/client/src/app/api/apiConstants.js
index ba1a35c67..d6a0f2d2f 100644
--- a/client/src/app/api/apiConstants.js
+++ b/client/src/app/api/apiConstants.js
@@ -1,6 +1,7 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const EVENTS_TABLE = 'events';
+export const APP_TABLE = 'appinfo';
const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
diff --git a/client/src/app/api/ontimeApi.js b/client/src/app/api/ontimeApi.js
index 71b4678b7..15b81ee29 100644
--- a/client/src/app/api/ontimeApi.js
+++ b/client/src/app/api/ontimeApi.js
@@ -1,6 +1,15 @@
import axios from 'axios';
import { ontimeURL } from './apiConstants';
+export const ontimePlaceholderInfo = {
+ networkInterfaces: [],
+};
+
+export const getInfo = async () => {
+ const res = await axios.get(ontimeURL + '/info');
+ return res.data;
+};
+
export const downloadEvents = async () => {
await axios({
url: ontimeURL + '/db',
diff --git a/client/src/common/input/EditableText.jsx b/client/src/common/input/EditableText.jsx
index f41d8ad0a..ec8eec120 100644
--- a/client/src/common/input/EditableText.jsx
+++ b/client/src/common/input/EditableText.jsx
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import style from './EditableText.module.css';
export default function EditableText(props) {
- const { label, defaultValue, placeholder, submitHandler } = props;
+ const { label, defaultValue, placeholder, submitHandler, ...rest } = props;
const [text, setText] = useState(defaultValue || '');
useEffect(() => {
@@ -22,7 +22,7 @@ export default function EditableText(props) {
};
return (
-
+
{label}
handleChange(v)}
diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx
index d0894a730..f9d079b33 100644
--- a/client/src/features/editors/Editor.jsx
+++ b/client/src/features/editors/Editor.jsx
@@ -2,10 +2,10 @@
import { lazy, useEffect } from 'react';
import { Heading } from '@chakra-ui/layout';
import { Box } from '@chakra-ui/layout';
-import NumberedText from 'common/components/text/NumberedText';
-import styles from './Editor.module.css';
import { useDisclosure } from '@chakra-ui/hooks';
-import SettingsModal from '../modals/SettingsModal';
+import styles from './Editor.module.css';
+import NumberedText from 'common/components/text/NumberedText';
+import SettingsModal from 'features/modals/SettingsModal';
import MenuBar from 'features/menu/MenuBar';
const EventListWrapper = lazy(() =>
@@ -13,6 +13,7 @@ const EventListWrapper = lazy(() =>
);
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
const MessageControl = lazy(() => import('features/control/MessageControl'));
+const Info = lazy(() => import('features/info/Info'));
export default function Editor() {
const { isOpen, onOpen, onClose } = useDisclosure();
diff --git a/client/src/features/editors/list/EventBlock.jsx b/client/src/features/editors/list/EventBlock.jsx
index 210f55828..19708b60b 100644
--- a/client/src/features/editors/list/EventBlock.jsx
+++ b/client/src/features/editors/list/EventBlock.jsx
@@ -77,6 +77,7 @@ const ExpandedBlock = (props) => {
label='Note'
defaultValue={data.note}
placeholder='Add Note'
+ style={{ color: '#d69e2e' }}
submitHandler={(v) =>
actionHandler('update', { field: 'note', value: v })
}
diff --git a/client/src/features/info/Info.jsx b/client/src/features/info/Info.jsx
new file mode 100644
index 000000000..445dfdf51
--- /dev/null
+++ b/client/src/features/info/Info.jsx
@@ -0,0 +1,80 @@
+import { useEffect } from 'react';
+import { useSocket } from 'app/context/socketContext';
+import style from './Info.module.css';
+import InfoTitle from './InfoTitle';
+import InfoLogger from './InfoLogger';
+import InfoNif from './InfoNif';
+import { useState } from 'react';
+
+export default function Info() {
+ const socket = useSocket();
+ const [titles, setTitles] = useState({
+ titleNow: '',
+ subtitleNow: '',
+ presenterNow: '',
+ noteNow: '',
+ titleNext: '',
+ subtitleNext: '',
+ presenterNext: '',
+ noteNext: '',
+ });
+ const [selected, setSelected] = useState('-/-');
+ const logData = [];
+
+ // handle incoming messages
+ useEffect(() => {
+ if (socket == null) return;
+
+ // Ask for titles
+ socket.emit('get-titles');
+
+ // Handle titles
+ socket.on('titles', (data) => {
+ setTitles(data);
+ });
+
+ // Ask for selection data
+ socket.emit('get-selected');
+
+ // Handle selection data
+ socket.on('selected', (data) => {
+ const formatedCurrent = `Event ${
+ data.index != null ? data.index + 1 : '-'
+ }/${data.total != null ? data.total : '-'}`;
+
+ setSelected(formatedCurrent);
+ });
+
+ // Clear listener
+ return () => {
+ socket.off('titles');
+ socket.off('selected');
+ };
+ }, [socket]);
+
+ // TODO: Put this in use effect
+ // prepare data
+ const titlesNow = {
+ title: titles.titleNow,
+ subtitle: titles.subtitleNow,
+ presenter: titles.presenterNow,
+ note: titles.noteNow,
+ };
+
+ const titlesNext = {
+ title: titles.titleNext,
+ subtitle: titles.subtitleNext,
+ presenter: titles.presenterNext,
+ note: titles.noteNext,
+ };
+
+ return (
+ <>
+ {selected}
+ {/* */}
+
+
+
+ >
+ );
+}
diff --git a/client/src/features/info/Info.module.css b/client/src/features/info/Info.module.css
new file mode 100644
index 000000000..314822f08
--- /dev/null
+++ b/client/src/features/info/Info.module.css
@@ -0,0 +1,82 @@
+.container {
+ margin-top: 1em;
+ display: flex;
+ flex-direction: column;
+ background-color: rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ border-radius: 4px;
+ padding: 8px;
+}
+
+.main {
+ font-size: 0.9em;
+ text-align: right;
+ color: #ff7597;
+}
+
+.header {
+ padding: 0;
+ margin: 0;
+ font-size: 0.9em;
+ color: #aaa;
+ display: flex;
+ justify-content: space-between;
+}
+
+.label {
+ padding: 0;
+ margin: 0;
+ font-size: 0.9em;
+ color: #ccc;
+}
+
+.notes {
+ color: #d69e2e;
+}
+
+.if {
+ font-size: 0.8em;
+ color: #4bffabcc;
+ background-color: rgba(0, 0, 0, 0.13);
+ border-radius: 2px;
+ padding: 0 0.5em;
+ margin: 0 0.5em;
+}
+
+ul > li {
+ font-size: 0.9em;
+ color: #fff;
+}
+
+.log {
+ overflow-y: scroll;
+ height: 30vh;
+}
+
+.info {
+ color: #fff;
+}
+
+.error {
+ color: red;
+}
+
+.client {
+ color: lightblue;
+}
+
+.moreExpanded,
+.moreCollapsed {
+ cursor: pointer;
+ color: #fff;
+}
+
+.moreExpanded {
+ transform: scaleY(-1);
+ transition: transform 0.3s;
+}
+
+.moreCollapsed {
+ transform: scaleY(1);
+ transition: transform 0.3s;
+}
diff --git a/client/src/features/info/InfoLogger.jsx b/client/src/features/info/InfoLogger.jsx
new file mode 100644
index 000000000..f6307b73e
--- /dev/null
+++ b/client/src/features/info/InfoLogger.jsx
@@ -0,0 +1,35 @@
+import { Icon } from '@chakra-ui/react';
+import { useState } from 'react';
+import { FiChevronUp } from 'react-icons/fi';
+import style from './Info.module.css';
+
+export default function InfoLogger(props) {
+ const [collapsed, setCollapsed] = useState(false);
+
+ const { logData } = props;
+
+ return (
+
+
+ Log
+ setCollapsed((c) => !c)}
+ />
+
+ {!collapsed && (
+
+ - 10:35:23 [PLAYBACK] Next
+ -
+ 10:32:10 [CLIENT] New socket client (total: 3)
+
+ - 10:28:23 [PLAYBACK] Next
+ - 10:25:23 [PLAYBACK] Play
+ - 10:23:13 [SERVER] Server Reconnected
+ - 10:23:10 [SERVER] Server Disconnected
+
+ )}
+
+ );
+}
diff --git a/client/src/features/info/InfoNif.jsx b/client/src/features/info/InfoNif.jsx
new file mode 100644
index 000000000..cc46f9cf9
--- /dev/null
+++ b/client/src/features/info/InfoNif.jsx
@@ -0,0 +1,49 @@
+import { Icon } from '@chakra-ui/react';
+import { useState } from 'react';
+import { FiChevronUp } from 'react-icons/fi';
+import { APP_TABLE } from 'app/api/apiConstants';
+import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
+import { useFetch } from 'app/hooks/useFetch';
+import style from './Info.module.css';
+
+export default function InfoNif() {
+ const { data, status } = useFetch(APP_TABLE, getInfo, {
+ placeholderData: ontimePlaceholderInfo,
+ });
+ const [collapsed, setCollapsed] = useState(false);
+
+ const isDev = process.env.NODE_ENV === 'development';
+ const baseURL = `http://__IP__:${isDev ? 3000 : 4001}`;
+
+ return (
+
+
+ Network Info
+ setCollapsed((c) => !c)}
+ />
+
+
+ {!collapsed && (
+
+ )}
+
+ );
+}
diff --git a/client/src/features/info/InfoTitle.jsx b/client/src/features/info/InfoTitle.jsx
new file mode 100644
index 000000000..a6e8a46cb
--- /dev/null
+++ b/client/src/features/info/InfoTitle.jsx
@@ -0,0 +1,43 @@
+import { Icon } from '@chakra-ui/react';
+import { useState } from 'react';
+import { FiChevronUp } from 'react-icons/fi';
+import style from './Info.module.css';
+
+export default function InfoTitle(props) {
+ const [collapsed, setCollapsed] = useState(false);
+
+ const { title, data } = props;
+ return (
+
+
+ {title}
+ setCollapsed((c) => !c)}
+ />
+
+
+ {!collapsed && (
+ <>
+
+ Title:
+ {data.title}
+
+
+ Presenter:
+ {data.presenter}
+
+
+ Subtitle:
+ {data.subtitle}
+
+
+ Note:
+ {data.note}
+
+ >
+ )}
+
+ );
+}
diff --git a/server/src/classes/EventTimer.js b/server/src/classes/EventTimer.js
index 1b23f58ed..d7f9d5e3e 100644
--- a/server/src/classes/EventTimer.js
+++ b/server/src/classes/EventTimer.js
@@ -44,9 +44,11 @@ export class EventTimer extends Timer {
titleNow: null,
subtitleNow: null,
presenterNow: null,
+ noteNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
+ noteNext: null,
};
selectedEventIndex = null;
@@ -101,6 +103,7 @@ export class EventTimer extends Timer {
this.io.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
+ total: this.numEvents,
});
this.io.emit('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId);
@@ -297,6 +300,7 @@ export class EventTimer extends Timer {
socket.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
+ total: this.numEvents,
});
});
@@ -596,6 +600,7 @@ export class EventTimer extends Timer {
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
+ this.titles.noteNow = e.note;
this.selectedEventId = e.id;
break;
@@ -609,6 +614,7 @@ export class EventTimer extends Timer {
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
+ this.titles.noteNow = e.note;
this.selectedEventId = e.id;
break;
@@ -624,6 +630,7 @@ export class EventTimer extends Timer {
this.titles.titleNext = e.title;
this.titles.subtitleNext = e.subtitle;
this.titles.presenterNext = e.presenter;
+ this.titles.noteNext = e.note;
this.nextEventId = e.id;
break;
case 'next-public':
@@ -636,6 +643,7 @@ export class EventTimer extends Timer {
this.titles.titleNext = e.title;
this.titles.subtitleNext = e.subtitle;
this.titles.presenterNext = e.presenter;
+ this.titles.noteNext = e.note;
this.nextEventId = e.id;
break;
@@ -652,6 +660,7 @@ export class EventTimer extends Timer {
this.titles.titleNext = null;
this.titles.subtitleNext = null;
this.titles.presenterNext = null;
+ this.titles.noteNext = null;
this.nextEventId = null;
this.titlesPublic.titleNext = null;
@@ -690,9 +699,11 @@ export class EventTimer extends Timer {
titleNow: null,
subtitleNow: null,
presenterNow: null,
+ noteNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
+ noteNext: null,
};
this.publicTitles = {
@@ -739,9 +750,11 @@ export class EventTimer extends Timer {
Title Now = ${this.titles.titleNow}
Subtitle Now = ${this.titles.subtitleNow}
Presenter Now = ${this.titles.presenterNow}
+ Note Now = ${this.titles.noteNow}
Title Next = ${this.titles.titleNext}
Subtitle Next = ${this.titles.subtitleNext}
Presenter Next = ${this.titles.presenterNext}
+ Note Next = ${this.titles.noteNext}
Public Titles
------------------------------
diff --git a/server/src/controllers/ontimeController.js b/server/src/controllers/ontimeController.js
index 99126e19f..12dad04dd 100644
--- a/server/src/controllers/ontimeController.js
+++ b/server/src/controllers/ontimeController.js
@@ -10,6 +10,7 @@ import {
block as blockDef,
} from '../data/eventsDefinition.js';
import { dbModel } from '../data/dataModel.js';
+import { networkInterfaces } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -132,6 +133,37 @@ const upload = async (file, req, res) => {
}
};
+/**
+ * @description Gets information on IPV4 non internal interfaces
+ * @returns {array} - Array of objects {name: ip}
+ */
+const getNetworkInterfaces = () => {
+ const nets = networkInterfaces();
+ const results = [];
+
+ for (const name of Object.keys(nets)) {
+ for (const net of nets[name]) {
+ // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
+ if (net.family === 'IPv4' && !net.internal) {
+ results.push({
+ name: name,
+ address: net.address,
+ });
+ }
+ }
+ }
+ return results;
+};
+
+// Create controller for POST request to '/ontime/info'
+// Returns -
+// TODO: Add version
+export const getInfo = async (req, res) => {
+ const ni = getNetworkInterfaces();
+ res.status(200).send({
+ networkInterfaces: ni,
+ });
+
// Create controller for POST request to '/ontime/db'
// Returns -
export const dbUpload = async (req, res) => {
@@ -152,4 +184,5 @@ export const dbPathToUpload = async (req, res) => {
return;
}
upload(req.body.path, req, res);
+
};
diff --git a/server/src/routes/ontimeRouter.js b/server/src/routes/ontimeRouter.js
index 8c1141170..9602b927a 100644
--- a/server/src/routes/ontimeRouter.js
+++ b/server/src/routes/ontimeRouter.js
@@ -5,6 +5,7 @@ export const router = express.Router();
import {
dbDownload,
dbUpload,
+ getInfo,
dbPathToUpload,
} from '../controllers/ontimeController.js';
@@ -14,5 +15,8 @@ router.get('/db', dbDownload);
// create route between controller and '/ontime/db' endpoint
router.post('/db', uploadJson, dbUpload);
+// create route between controller and '/ontime/info' endpoint
+router.get('/info', uploadJson, getInfo);
+
// create route between controller and '/ontime/dbpath' endpoint
router.post('/dbpath', dbPathToUpload);