diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 54e1a2ba5..e8953185b 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -15,8 +15,8 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" - build: + # build a file for windows + buildwin: # The type of runner that the job will run on runs-on: windows-latest env: @@ -55,3 +55,44 @@ jobs: files: ./server/dist/ontime-win64.exe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # build a file for mac + buildmac: + # The type of runner that the job will run on + runs-on: macOS-latest + env: + CI: '' + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: '14.x' + + # install and build react + - name: Install React dependencies + run: yarn install + working-directory: ./client + - name: Build React App + run: yarn build + working-directory: ./client + + # install and build electron + - name: Install Electron + nodejs dependencies + run: yarn install + working-directory: ./server + - name: Build Electron App + run: yarn dist-win + working-directory: ./server + + # release + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: ./server/dist/ontime-macOS.dmg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 24d6a372e..f15eef951 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist/ # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 000000000..5da1733df --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,4 @@ +# Table of contents + +* [Initial page](README.md) + diff --git a/client/src/App.css b/client/src/App.css index bd24333c6..71293f8c2 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -10,4 +10,7 @@ html, overflow: hidden; overflow: clip; height: 100vh; + -webkit-user-select: none; + user-select: none; + -webkit-app-region: drag; } 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/components/buttons/ApplyIconBtn.jsx b/client/src/common/components/buttons/ApplyIconBtn.jsx index 67a4e2f3a..57a7e4609 100644 --- a/client/src/common/components/buttons/ApplyIconBtn.jsx +++ b/client/src/common/components/buttons/ApplyIconBtn.jsx @@ -1,16 +1,19 @@ import { IconButton } from '@chakra-ui/button'; +import { Tooltip } from '@chakra-ui/tooltip'; import { FiChevronsDown } from 'react-icons/fi'; export default function ApplyIconBtn(props) { const { clickhandler, ...rest } = props; return ( - } - colorScheme='orange' - onClick={clickhandler} - _focus={{ boxShadow: 'none' }} - {...rest} - /> + + } + colorScheme='orange' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + ); } diff --git a/client/src/common/components/buttons/LockIconBtn.jsx b/client/src/common/components/buttons/LockIconBtn.jsx index c499594d0..55ab04b35 100644 --- a/client/src/common/components/buttons/LockIconBtn.jsx +++ b/client/src/common/components/buttons/LockIconBtn.jsx @@ -6,13 +6,13 @@ export default function LockIconBtn(props) { const { clickhandler, active, ref } = props; return ( - } color={active ? 'pink.100' : 'pink.300'} borderColor={active ? undefined : 'pink.300'} - backgroundColor={active ? 'pink.300' : undefined} + backgroundColor={active ? 'pink.400' : undefined} variant={active ? 'solid' : 'outline'} onClick={clickhandler} _focus={{ boxShadow: 'none' }} diff --git a/client/src/common/components/buttons/PublicIconBtn.jsx b/client/src/common/components/buttons/PublicIconBtn.jsx new file mode 100644 index 000000000..598090e93 --- /dev/null +++ b/client/src/common/components/buttons/PublicIconBtn.jsx @@ -0,0 +1,22 @@ +import { IconButton } from '@chakra-ui/button'; +import { Tooltip } from '@chakra-ui/tooltip'; +import { FiUsers } from 'react-icons/fi'; + +export default function PublicIconBtn(props) { + const { actionHandler, active, ...rest } = props; + return ( + + } + colorScheme='blue' + variant={active ? 'solid' : 'outline'} + onClick={() => + actionHandler('update', { field: 'isPublic', value: !active }) + } + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/common/components/text/NumberedText.jsx b/client/src/common/components/text/NumberedText.jsx deleted file mode 100644 index a2f3b8896..000000000 --- a/client/src/common/components/text/NumberedText.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Flex, Text } from '@chakra-ui/layout'; -import styles from './NumberedText.module.css'; - -export default function NumberedText({ number = 1, text = '' }) { - return ( - -
{number}
- {text} -
- ); -} diff --git a/client/src/common/components/text/NumberedText.module.css b/client/src/common/components/text/NumberedText.module.css deleted file mode 100644 index 16d6f39ca..000000000 --- a/client/src/common/components/text/NumberedText.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.stylednumber { - display: inline; - text-align: center; - color: white; - font-weight: 600; - - background-color: #ff5b7e; - border: 1px solid rgba(255,255,255,0.17); - width: 1.5em; - border-radius: 1em; - - margin-right: 0.5em; -} diff --git a/client/src/common/input/EditableText.jsx b/client/src/common/input/EditableText.jsx index f41d8ad0a..47b11ce77 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(() => { @@ -30,6 +30,7 @@ export default function EditableText(props) { value={text} placeholder={placeholder} className={style.inline} + {...rest} > @@ -12,6 +10,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(); @@ -31,44 +30,31 @@ export default function Editor() { - - Event List - - +

Event List

- - Display Messages - - +

Display Messages

- - Time Control - - +

Timer Control

- - - Info - - -
+ +

Info

+
+ +
diff --git a/client/src/features/editors/Editor.module.css b/client/src/features/editors/Editor.module.css index 00186eecf..6e365d546 100644 --- a/client/src/features/editors/Editor.module.css +++ b/client/src/features/editors/Editor.module.css @@ -69,6 +69,12 @@ } } +h1 { + font-size: 2.5vh; + color: rgba(255, 255, 255, 0.63); + padding-bottom: 0.25em; +} + .mainContainer > div { border-radius: 0.5em; height: 100%; diff --git a/client/src/features/editors/list/ActionButtons.jsx b/client/src/features/editors/list/ActionButtons.jsx index ddd673728..f571be840 100644 --- a/client/src/features/editors/list/ActionButtons.jsx +++ b/client/src/features/editors/list/ActionButtons.jsx @@ -1,6 +1,6 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; import { IconButton } from '@chakra-ui/button'; -import { FiZap, FiPlus, FiMinusCircle, FiClock } from 'react-icons/fi'; +import { FiPlus, FiMinusCircle, FiClock } from 'react-icons/fi'; export default function ActionButtons(props) { const { showAdd, showDelay, showBlock, actionHandler } = props; @@ -16,7 +16,7 @@ export default function ActionButtons(props) { as={IconButton} aria-label='Options' size='xs' - icon={} + icon={} _expanded={{ bg: 'orange.300', color: 'white' }} _focus={{ boxShadow: 'none' }} backgroundColor={'orange.200'} diff --git a/client/src/features/editors/list/EventBlock.jsx b/client/src/features/editors/list/EventBlock.jsx index 9b26ea583..7569cce53 100644 --- a/client/src/features/editors/list/EventBlock.jsx +++ b/client/src/features/editors/list/EventBlock.jsx @@ -6,7 +6,7 @@ import EventTimes from 'common/components/eventTimes/EventTimes'; import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical'; import EditableText from 'common/input/EditableText'; import ActionButtons from './ActionButtons'; -import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn'; +import PublicIconBtn from 'common/components/buttons/PublicIconBtn'; import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn'; import { millisToMinutes } from 'common/dateConfig'; import style from './EventBlock.module.css'; @@ -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 }) } @@ -93,7 +94,7 @@ const ExpandedBlock = (props) => { onClick={() => props.setCollapsed(true)} />
- + { onClick={() => props.setCollapsed(false)} />
- + { export default function EventBlock(props) { const { data, selected, delay, index, eventIndex, actionHandler } = props; - - // const [collapsed, setCollapsed] = useState(checkLocalStorage(data.id)); - - // const collapsed = useSelector(itemsAtom, (state) => state === data.id); const [collapsed] = useAtom( useMemo(() => SelectCollapse(data.id), [data.id]) ); diff --git a/client/src/features/editors/list/EventBlock.module.css b/client/src/features/editors/list/EventBlock.module.css index 26ddbecf0..bc7f34764 100644 --- a/client/src/features/editors/list/EventBlock.module.css +++ b/client/src/features/editors/list/EventBlock.module.css @@ -30,7 +30,6 @@ grid-template-columns: 2em 2em auto auto 1fr auto 3.7em; grid-template-areas: 'drag indi time time text more btns'; justify-content: center; - align-items: baseline; } .expanded { @@ -152,6 +151,8 @@ font-size: 0.8em; float: right; padding-right: 1em; + -webkit-user-select: text; + user-select: text; } /* ================ MORE ================ */ 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..46d862ab9 --- /dev/null +++ b/client/src/features/info/InfoNif.jsx @@ -0,0 +1,56 @@ +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}`; + + const handleLink = (url) => { + if (window.process.type === 'renderer') { + window.ipcRenderer.send('send-to-link', url); + } + }; + + return ( +
+
+ Network Info + setCollapsed((c) => !c)} + /> +
+ + {!collapsed && ( +
+ {status === 'success' && ( + <> + {data?.networkInterfaces.map((e) => { + return ( + + handleLink(baseURL.replace('__IP__', e.address)) + } + className={style.if} + >{`${e.name} - ${e.address}`} + ); + })} + + )} +
+ )} +
+ ); +} 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/client/src/features/menu/MenuActionButtons.jsx b/client/src/features/menu/MenuActionButtons.jsx index 4bb954e40..5df8ecb95 100644 --- a/client/src/features/menu/MenuActionButtons.jsx +++ b/client/src/features/menu/MenuActionButtons.jsx @@ -1,12 +1,6 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; import { IconButton } from '@chakra-ui/button'; -import { - FiTrash2, - FiZap, - FiPlus, - FiClock, - FiMinusCircle, -} from 'react-icons/fi'; +import { FiTrash2, FiPlus, FiClock, FiMinusCircle } from 'react-icons/fi'; import { Divider } from '@chakra-ui/layout'; export default function MenuActionButtons(props) { @@ -20,9 +14,9 @@ export default function MenuActionButtons(props) { } + icon={} _expanded={{ bg: 'orange.300', color: 'white' }} _focus={{ boxShadow: 'none' }} backgroundColor={'orange.200'} diff --git a/client/src/features/menu/MenuBar.jsx b/client/src/features/menu/MenuBar.jsx index 1dae90257..c11ce2efb 100644 --- a/client/src/features/menu/MenuBar.jsx +++ b/client/src/features/menu/MenuBar.jsx @@ -1,9 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { - downloadEvents, - uploadEvents, - uploadEventsWithPath, -} from 'app/api/ontimeApi'; +import { downloadEvents, uploadEvents } from 'app/api/ontimeApi'; import { EVENTS_TABLE } from 'app/api/apiConstants'; import DownloadIconBtn from './buttons/DownloadIconBtn'; import SettingsIconBtn from './buttons/SettingsIconBtn'; @@ -16,8 +12,6 @@ import HelpIconBtn from './buttons/HelpIconBtn'; import UploadIconBtn from './buttons/UploadIconBtn'; import { useRef } from 'react'; -const { ipcRenderer } = window.require('electron'); - export default function MenuBar(props) { const { onOpen } = props; const hiddenFileInput = useRef(null); @@ -51,21 +45,23 @@ export default function MenuBar(props) { }; const handleIPC = (action) => { - switch (action) { - case 'min': - ipcRenderer.send('set-window', 'to-tray'); - break; - case 'max': - ipcRenderer.send('set-window', 'to-max'); - break; - case 'shutdown': - ipcRenderer.send('shutdown', 'now'); - break; - case 'help': - ipcRenderer.send('send-to-link', 'help'); - break; - default: - break; + if (window.process.type === 'renderer') { + switch (action) { + case 'min': + window.ipcRenderer.send('set-window', 'to-tray'); + break; + case 'max': + window.ipcRenderer.send('set-window', 'to-max'); + break; + case 'shutdown': + window.ipcRenderer.send('shutdown', 'now'); + break; + case 'help': + window.ipcRenderer.send('send-to-link', 'help'); + break; + default: + break; + } } }; @@ -87,7 +83,6 @@ export default function MenuBar(props) { style={{ fontSize: '1.5em' }} size='lg' clickhandler={() => handleIPC('help')} - disabled />
diff --git a/server/__tests__/utils/time.tests.js b/server/__tests__/utils/time.tests.js new file mode 100644 index 000000000..361614ff4 --- /dev/null +++ b/server/__tests__/utils/time.tests.js @@ -0,0 +1,16 @@ +import { stringFromMillis } from '../../src/utils/time'; + +const t1 = { val: null, result: '...' }; +test('test stringFromMillis() on null values', () => { + expect(stringFromMillis(t1.val)).toBe(t1.result); +}); + +const t2 = { val: 3600000, result: '01:00:00' }; +test('test stringFromMillis() on valid millis', () => { + expect(stringFromMillis(t2.val)).toBe(t2.result); +}); + +const t3 = { val: -3600000, result: '-01:00:00' }; +test('test stringFromMillis() on negative millis', () => { + expect(stringFromMillis(t3.val)).toBe(t3.result); +}); diff --git a/server/assets/logo.png b/server/assets/logo.png new file mode 100644 index 000000000..91ffece1e Binary files /dev/null and b/server/assets/logo.png differ diff --git a/server/electron/preload.js b/server/electron/preload.js index fa038c825..40d7ecf94 100644 --- a/server/electron/preload.js +++ b/server/electron/preload.js @@ -1 +1 @@ -window.require = require; +window.ipcRenderer = require('electron').ipcRenderer; diff --git a/server/electron/splash/splash.html b/server/electron/splash/splash.html index 6a5c9cba2..8be547f91 100644 --- a/server/electron/splash/splash.html +++ b/server/electron/splash/splash.html @@ -9,6 +9,10 @@ ontime