Compare commits

...

12 Commits

Author SHA1 Message Date
Carlos Valente dd1b9bd38c version bump
version bump from fixes in #106
2022-04-10 10:07:13 +02:00
Carlos Valente b01145ca4b Fix/102 (#106)
* fix/102 improve osx feedback on navigation
* fix/102 refactor enable / disable OSC in
2022-04-10 10:05:35 +02:00
Carlos Valente 67ddcd8c68 Feat/table (#105)
* feat/table add react-table
* feat/table add protected table route
* feat/table upgrade dependencies
* feat/table support parsing of new properties

Co-authored-by: DeepSource Bot <bot@deepsource.io>
2022-04-09 22:05:56 +02:00
DeepSource Bot 8841a02754 Add .deepsource.toml 2022-02-06 14:08:33 +00:00
Carlos Valente 1a0d7810c4 Create codeql-analysis.yml 2022-01-29 10:01:10 +01:00
Carlos Valente f2dd24b74a Feat/delays (#96)
* feat/delays allow negative delays
* feat/delays recover expand all
* feat/delays refact: icons import
* feat/delays refact: upgrade chakra
* feat/delays version bump
2022-01-23 14:32:15 +01:00
Carlos Valente 6a6f68b2f9 Feat/block improvements (#95)
* feat/89-block-improvements style: replace reload icon
* feat/block-feat/block-improvements add cursor buttons
* feat/block-improvements hover to create
* feat/block-improvements change settings in modal
* feat/block-improvements version bump
2022-01-22 23:23:31 +01:00
Carlos Valente 906a6631a7 Update README.md 2022-01-14 22:26:23 +01:00
Carlos Valente 79a24a134e Hotfix/6.0.2 (#94)
* hotfix/6.0.2 fix issue where events could not have an ID
* hotfix/6.0.2 version bump
* hotfix/6.0.2 testing
2022-01-14 22:11:33 +01:00
Carlos Valente acb5d8ae08 chore/update-deps update socket.io (#93) 2022-01-14 21:00:51 +01:00
cv 5ca8202368 version bump 2022-01-14 20:49:24 +01:00
Carlos Valente ba782d5d22 hotfix/92-osc timer is absolute (#92)
Fixes issues with approximating timers in the app views
2022-01-14 20:47:39 +01:00
166 changed files with 8406 additions and 6321 deletions
+18
View File
@@ -0,0 +1,18 @@
version = 1
test_patterns = [
"__mocks__/**",
"__tests__/**",
"cypress/**",
"*.test.*",
"*.mock.*",
"*.spec.*"
]
[[analyzers]]
name = "javascript"
enabled = true
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs","browser","jest"]
+70
View File
@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '33 02 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+13
View File
@@ -1,6 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myCustomPropertiesEnabled" value="true" />
<option name="myIgnoreVendorSpecificProperties" value="false" />
<option name="myCustomPropertiesList">
<value>
<list size="3">
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
<item index="2" class="java.lang.String" itemvalue="app-region" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
+12 -10
View File
@@ -2,7 +2,7 @@
Download the latest releases here
- [Windows](https://gitreleases.dev/gh/cpvalente/ontime/latest/ontime-win64.exe)
- [Windows](https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe)
# Ontime
Ontime is an application for managing event rundowns and running stage timers.
@@ -26,17 +26,18 @@ and extend with using the URL aliases feature
```
For the presentation views...
-------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/preseter > Presenter / Stage timer view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/presenter > Presenter / Stage timer view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
IP.ADDRESS:4001/cuesheet > Cue Sheet
...and for the editor (the control interface, same as the app)
-------------------------------------------------------------
IP.ADDRESS:4001/studio > Studio Clock
IP.ADDRESS:4001/studio > Studio Clock
```
@@ -48,7 +49,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- Backstage Info
- Public Info
- Picture in Picture
- Studio Clock
- [x] Configurable realtime Lower Thirds
- [x] Cuesheets with additional custom fields
- [x] Send live messages to different screen types
- [x] Ability to differentiate between backstage and public data
- [x] Manage delays workflow
@@ -62,7 +65,6 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
## Unopinionated
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
- [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same
- [x] If you want just the info screens, no need to use the timer!
- [x] Don't have or care for a schedule?
+73 -4
View File
@@ -1,13 +1,82 @@
{
"extends": [
"react-app",
"react-app/jest"
"react-app/jest",
"plugin:react/recommended"
],
"plugins": [
"react",
"testing-library",
"jest"
],
"plugins": ["react", "testing-library", "jest"],
"rules": {
"jest/no-mocks-import": "warn",
"no-useless-concat": "warn",
"prefer-template": "warn"
"prefer-template": "warn",
"react/jsx-no-bind": [
"error",
{
"ignoreRefs": true,
"allowArrowFunctions": true,
"allowFunctions": false,
"allowBind": false,
"ignoreDOMComponents": true
}
],
"react/jsx-boolean-value": [
"error",
"never",
{
"always": []
}
],
"react/jsx-handler-names": [
"off",
{
"eventHandlerPrefix": "handle",
"eventHandlerPropPrefix": "on"
}
],
"react/no-danger": "warn",
"react/no-deprecated": [
"error"
],
"react/jsx-no-undef": "error",
"react/jsx-closing-tag-location": "warn",
"react/jsx-no-useless-fragment": "warn",
"react/no-unescaped-entities": "error",
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "never",
"beforeClosing": "never"
}
],
"react/jsx-space-before-closing": [
"off",
"always"
],
"react/jsx-curly-brace-presence": [
"error",
{
"props": "never",
"children": "never"
}
],
"react/destructuring-assignment": [
"error",
"always"
],
"react/no-unsafe": "off",
"react/prop-types": [
"error",
{
"ignore": [],
"customValidators": [],
"skipUndeclared": true
}
]
}
}
+18 -13
View File
@@ -3,29 +3,30 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^1.7.3",
"@chakra-ui/react": "^1.8.6",
"@dnd-kit/core": "^5.0.3",
"@dnd-kit/sortable": "^6.0.1",
"@dnd-kit/utilities": "^3.1.0",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.24.0",
"framer-motion": "^4.1.6",
"jotai": "^0.16.5",
"@react-icons/all-files": "^4.1.0",
"autosize": "^5.0.1",
"axios": "^0.25.0",
"color": "^4.2.1",
"framer-motion": "4.1.17",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-icons": "^4.3.1",
"react-qr-code": "2.0.3",
"react-query": "^3.34.5",
"react-query": "^3.34.12",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"socket.io-client": "4.4.0",
"react-table": "^7.7.0",
"socket.io-client": "^4.4.1",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1"
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "set BROWSER=none&&react-scripts start",
@@ -47,9 +48,13 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"prop-types": "^15.8.1",
"react-scripts": "^5.0.0",
"react-test-renderer": "^17.0.2",
"sass": "^1.44.0"
"sass": "^1.49.0"
}
}
+28 -23
View File
@@ -1,4 +1,4 @@
import { lazy, Suspense, useCallback, useEffect } from 'react';
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
import withSocket from 'features/viewers/ViewWrapper';
@@ -7,23 +7,17 @@ import ProtectRoute from './common/components/protectRoute/ProtectRoute';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
import { TableSettingsProvider } from './app/context/TableSettingsContext';
const Editor = lazy(() => import('features/editors/Editor'));
const Table = lazy(() => import('features/table/TableWrapper'));
const TimerView = lazy(() =>
import('features/viewers/timer/Timer')
);
const MinimalTimerView = lazy(() =>
import('features/viewers/timer/MinimalTimer')
);
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
const StageManager = lazy(() =>
import('features/viewers/backstage/StageManager')
);
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
const Public = lazy(() => import('features/viewers/foh/Public'));
const Lower = lazy(() =>
import('features/viewers/production/lower/LowerWrapper')
);
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
@@ -35,6 +29,20 @@ const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
const ProtectedEditor = () => (
<ProtectRoute>
<Editor />
</ProtectRoute>
);
const ProtectedTable = () => (
<ProtectRoute>
<TableSettingsProvider>
<Table />
</TableSettingsProvider>
</ProtectRoute>
);
function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
@@ -48,8 +56,7 @@ function App() {
if (e.altKey) {
if (e.key === 't' || e.key === 'T') {
// if we are in electron
if (window.process?.type === undefined) return;
if (window.process.type === 'renderer') {
if (window.process?.type === 'renderer') {
// ask to see debug
window.ipcRenderer.send('set-window', 'show-dev');
}
@@ -101,15 +108,13 @@ function App() {
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route
path='/editor'
element={
<ProtectRoute>
<Editor />
</ProtectRoute>
}
/>
<Route path='/editor' element={<ProtectedEditor />} />
<Route path='/cuesheet' element={<ProtectedTable />} />
<Route path='/cuelist' element={<ProtectedTable />} />
<Route path='/table' element={<ProtectedTable />} />
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
+3 -2
View File
@@ -8,7 +8,7 @@
body,
html,
.App {
margin: 0px auto;
margin: 0 auto;
overflow: hidden;
overflow: clip;
height: 100vh;
@@ -23,7 +23,8 @@ option {
/* width */
::-webkit-scrollbar {
width: 0.5em;
width: 0.5rem;
height: 0.5rem;
}
/* Track */
+3 -2
View File
@@ -1,6 +1,7 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const USERFIELDS = 'userFields';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
@@ -13,5 +14,5 @@ const calculateServer = () => {
export const serverURL = calculateServer();
export const eventURL = serverURL + EVENT_TABLE;
export const eventsURL = serverURL + EVENTS_TABLE;
export const playbackURL = serverURL + 'playback';
export const ontimeURL = serverURL + 'ontime';
export const playbackURL = `${serverURL}playback`;
export const ontimeURL = `${serverURL}ontime`;
+8 -16
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { eventsURL } from '../api/apiConstants';
import { eventsURL } from './apiConstants';
export const fetchAllEvents = async () => {
const res = await axios.get(eventsURL);
@@ -7,38 +7,30 @@ export const fetchAllEvents = async () => {
};
export const requestPost = async (data) => {
const res = await axios.post(eventsURL, data);
return res;
await axios.post(eventsURL, data);
};
export const requestPut = async (data) => {
const res = await axios.put(eventsURL, data);
return res;
await axios.put(eventsURL, data);
};
export const requestPatch = async (data) => {
const res = await axios.patch(eventsURL, data);
return res;
await axios.patch(eventsURL, data);
};
export const requestReorder = async (data) => {
const action = 'reorder';
const res = await axios.patch(eventsURL + '/' + action, data);
return res;
await axios.patch(`${eventsURL}/reorder`, data);
};
export const requestApplyDelay = async (eventId) => {
const action = 'applydelay';
const res = await axios.patch(eventsURL + '/' + action + '/' + eventId);
return res;
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
};
export const requestDelete = async (eventId) => {
const res = await axios.delete(eventsURL + '/' + eventId);
return res;
await axios.delete(`${eventsURL}/${eventId}`);
};
export const requestDeleteAll = async () => {
const res = await axios.delete(eventsURL + '/all');
return res;
await axios.delete(`${eventsURL}/all`);
};
+27 -4
View File
@@ -21,6 +21,19 @@ export const eventPlaceholderSettings = {
endMessage: '',
};
export const userFieldsPlaceholder = {
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
};
export const oscPlaceholderSettings = {
port: '',
portOut: '',
@@ -92,7 +105,7 @@ export const getSettings = async () => {
};
export const postSettings = async (data) => {
return await axios.post(`${ontimeURL}/settings`, data);
await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => {
@@ -101,7 +114,7 @@ export const getInfo = async () => {
};
export const postInfo = async (data) => {
return await axios.post(`${ontimeURL}/info`, data);
await axios.post(`${ontimeURL}/info`, data);
};
export const getAliases = async () => {
@@ -110,7 +123,17 @@ export const getAliases = async () => {
};
export const postAliases = async (data) => {
return await axios.post(`${ontimeURL}/aliases`, data);
await axios.post(`${ontimeURL}/aliases`, data);
};
export const getUserFields = async () => {
const res = await axios.get(`${ontimeURL}/userfields`);
return res.data;
};
export const postUserFields = async (data) => {
await axios.post(`${ontimeURL}/userfields`, data);
};
export const getOSC = async () => {
@@ -119,7 +142,7 @@ export const getOSC = async () => {
};
export const postOSC = async (data) => {
return await axios.post(`${ontimeURL}/osc`, data);
await axios.post(`${ontimeURL}/osc`, data);
};
export const downloadEvents = async () => {
+14 -12
View File
@@ -1,14 +1,16 @@
// Exported viewer links
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
// Exported viewer link location
const speakerLocation = 'speaker';
const smLocation = 'sm';
const publicLocation = 'public';
const pipLocation = 'pip';
const studioLocation = 'studio';
const cuesheetLocation = 'cuesheet';
export const viewerLinks = [
{ link: speakerLink, label: 'Speaker Screen' },
{ link: smLink, label: 'Backstage Screen' },
{ link: publicLink, label: 'Public Screen' },
{ link: pipLink, label: 'Picture in Picture' },
{ link: studioLink, label: 'Studio Clock' }
export const viewerLocations = [
{ link: speakerLocation, label: 'Speaker Screen' },
{ link: smLocation, label: 'Backstage Screen' },
{ link: publicLocation, label: 'Public Screen' },
{ link: pipLocation, label: 'Picture in Picture' },
{ link: studioLocation, label: 'Studio Clock' },
{ link: cuesheetLocation, label: 'Cuesheet' }
];
+14 -15
View File
@@ -1,4 +1,4 @@
import { createContext, useCallback, useEffect, useState } from 'react';
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
@@ -6,11 +6,11 @@ import { getSettings } from '../api/ontimeApi';
export const AppContext = createContext({
auth: false,
data: {
pinCode: null
}
pinCode: null,
},
});
export const AppContextProvider = (props) => {
export const AppContextProvider = ({ children }) => {
const [auth, setAuth] = useState(true);
const { data } = useFetch(APP_SETTINGS, getSettings);
@@ -21,17 +21,16 @@ export const AppContextProvider = (props) => {
} else {
setAuth(false);
}
},[data])
const validate = useCallback((pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
}, [data]);
return (
<AppContext.Provider value={{ auth, validate }}>
{props.children}
</AppContext.Provider>
const validate = useCallback(
(pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
},
[data]
);
};
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
};
+71
View File
@@ -0,0 +1,71 @@
import React, { createContext, useCallback } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const CollapseContext = createContext({
collapsed: {},
setCollapsed: () => undefined,
clearCollapsed: () => undefined,
isCollapsed: () => undefined,
});
export const CollapseProvider = ({ children }) => {
const [collapsed, saveCollapsed] = useLocalStorage('collapsed', {});
/**
* @description Sets collapsed state for a single id
* @param {string} - id
* @param {boolean} - collapsed / not collapsed
*/
const setCollapsed = useCallback(
(id, isCollapsed) => {
if (isCollapsed) {
saveCollapsed((prev) => ({ ...prev, [id]: true }));
} else {
saveCollapsed((prev) => {
const newObject = { ...prev };
delete newObject[id];
return { ...newObject };
});
}
},
[saveCollapsed]
);
/**
* @description Clears collapsed state in local-storage
*/
const collapseMultiple = useCallback(
(events) => {
const newOptions = {};
for (const event of events) {
newOptions[event.id] = true;
}
saveCollapsed(newOptions);
},
[saveCollapsed]
);
/**
* @description Clears collapsed state in local-storage
*/
const expandAll = useCallback(() => {
saveCollapsed({});
}, [saveCollapsed]);
/**
* @description Clears collapsed state in local-storage
* @return {boolean} - collapsed / not collapsed
*/
const isCollapsed = useCallback(
(id) => {
return id in collapsed;
},
[collapsed]
);
return (
<CollapseContext.Provider value={{ setCollapsed, collapseMultiple, expandAll, isCollapsed }}>
{children}
</CollapseContext.Provider>
);
};
+63
View File
@@ -0,0 +1,63 @@
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const CursorContext = createContext({
cursor: 0,
isCursorLocked: false,
setCursor: () => undefined,
moveCursorUp: () => undefined,
moveCursorDown: () => undefined,
});
export const CursorProvider = ({ children }) => {
const [cursor, setCursor] = useState(0);
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
const cursorLockedOff = useCallback(() => _setCursorLocked('unlocked'), [_setCursorLocked]);
const cursorLockedOn = useCallback(() => _setCursorLocked('locked'), [_setCursorLocked]);
const moveCursorUp = useCallback(() => {
setCursor((prev) => Math.max(prev - 1, 0));
}, []);
const moveCursorDown = useCallback(() => {
setCursor((prev) => prev + 1);
}, []);
/**
* @param {boolean | undefined} newValue
*/
const toggleCursorLocked = useCallback(
(newValue = undefined) => {
if (newValue === undefined) {
if (isCursorLocked) {
cursorLockedOff();
} else {
cursorLockedOn();
}
} else if (!newValue) {
cursorLockedOff();
} else if (newValue) {
cursorLockedOn();
}
},
[cursorLockedOff, cursorLockedOn, isCursorLocked]
);
return (
<CursorContext.Provider
value={{
cursor,
isCursorLocked,
toggleCursorLocked,
setCursor,
moveCursorUp,
moveCursorDown,
}}
>
{children}
</CursorContext.Provider>
);
};
@@ -0,0 +1,32 @@
import React, { createContext, useState } from 'react';
export const LocalEventSettingsContext = createContext({
showQuickEntry: false,
starTimeIsLastEnd: true,
defaultPublic: true,
setShowQuickEntry: () => undefined,
setStarTimeIsLastEnd: () => undefined,
setDefaultPublic: () => undefined,
});
export const LocalEventSettingsProvider = ({ children }) => {
const [showQuickEntry, setShowQuickEntry] = useState(false);
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
const [defaultPublic, setDefaultPublic] = useState(false);
return (
<LocalEventSettingsContext.Provider
value={{
showQuickEntry,
setShowQuickEntry,
starTimeIsLastEnd,
setStarTimeIsLastEnd,
defaultPublic,
setDefaultPublic,
}}
>
{children}
</LocalEventSettingsContext.Provider>
);
};
+44 -32
View File
@@ -1,5 +1,5 @@
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react';
import { generateId } from 'ontime-utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
@@ -8,10 +8,10 @@ export const LoggingContext = createContext({
emitInfo: () => undefined,
emitWarning: () => undefined,
emitError: () => undefined,
clearLog: () => undefined
clearLog: () => undefined,
});
export const LoggingProvider = (props) => {
export const LoggingProvider = ({ children }) => {
const MAX_MESSAGES = 100;
const socket = useSocket();
const [logData, setLogData] = useState([]);
@@ -40,57 +40,69 @@ export const LoggingProvider = (props) => {
* @param level
* @private
*/
const _send = useCallback((text, level) => {
if (socket != null) {
const m = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
level,
text
const _send = useCallback(
(text, level) => {
if (socket != null) {
const m = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
level,
text,
};
setLogData((l) => [m, ...l]);
socket.emit('logger', m);
}
setLogData((l) => [m, ...l]);
socket.emit('logger', m);
}
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
}
},[logData, socket]);
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
}
},
[logData, socket]
);
/**
* Sends a message with level INFO
* @param text
*/
const emitInfo = useCallback((text) => {
_send(text, 'INFO');
}, [_send]);
const emitInfo = useCallback(
(text) => {
_send(text, 'INFO');
},
[_send]
);
/**
* Sends a message with level WARN
* @param text
*/
const emitWarning = useCallback((text) => {
_send(text, 'WARN');
}, [_send]);
const emitWarning = useCallback(
(text) => {
_send(text, 'WARN');
},
[_send]
);
/**
* Sends a message with level ERROR
* @param text
*/
const emitError = useCallback((text) => {
_send(text, 'ERROR');
}, [_send]);
const emitError = useCallback(
(text) => {
_send(text, 'ERROR');
},
[_send]
);
/**
* Clears running log
*/
const clearLog = useCallback(() => {
setLogData([])
setLogData([]);
}, []);
return (
<LoggingContext.Provider value = {{ emitInfo, logData, emitWarning, emitError, clearLog }}>
{props.children}
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
{children}
</LoggingContext.Provider>
)
}
);
};
@@ -0,0 +1,78 @@
import React, { createContext, useCallback, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const TableSettingsContext = createContext({
theme: '',
showSettings: false,
followSelected: false,
toggleSettings: () => undefined,
toggleTheme: () => undefined,
toggleFollow: () => undefined,
});
export const TableSettingsProvider = ({ children }) => {
const [theme, setTheme] = useLocalStorage('table-color-theme', 'dark');
const [followSelected, setFollowSelected] = useLocalStorage('table-follow-selected', false);
const [showSettings, setShowSettings] = useState(false);
/**
* @description Toggles the current value of dark mode
* @param {string} val - 'light' or 'dark'
*/
const toggleTheme = useCallback(
(val) => {
if (val === undefined) {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
} else {
setTheme(val);
}
},
[setTheme]
);
/**
* @description Toggles visibility state for settings
* @param {boolean} val - whether the settings window is visible
*/
const toggleSettings = useCallback(
(val) => {
if (val === undefined) {
setShowSettings((prev) => !prev);
} else {
setShowSettings(val);
}
},
[setShowSettings]
);
/**
* @description Toggles follow option
* @param {boolean} val - whether the window follows selected event
*/
const toggleFollow = useCallback(
(val) => {
if (val === undefined) {
setFollowSelected((prev) => !prev);
} else {
setFollowSelected(val);
}
},
[setFollowSelected]
);
return (
<TableSettingsContext.Provider
value={{
theme,
showSettings,
followSelected,
toggleSettings,
toggleTheme,
toggleFollow,
}}
>
{children}
</TableSettingsContext.Provider>
);
};
-60
View File
@@ -1,60 +0,0 @@
const { atom } = require('jotai');
const PATH = 'option-collapse';
const initialValue = {};
// collapse options object, initialised from local storage
// REFRACT: When do we read from local storage?
// on every render?
export const collapseAtom = atom(
(get) => {
const storedOptions = localStorage.getItem(PATH);
if (storedOptions == null) return initialValue;
return JSON.parse(storedOptions);
},
(get, set, newValues) => {
set(collapseAtom, newValues);
localStorage.setItem(PATH, JSON.stringify(newValues));
}
);
// get a single option, if it exists
export const SelectCollapse = (id) => {
return atom((get) => get(collapseAtom)[id]);
};
// change a single item in object
export const HandleCollapse = atom(null, (get, set, payload) => {
const updatedVal = {
...get(collapseAtom),
...payload,
};
set(collapseAtom, updatedVal);
localStorage.setItem(PATH, JSON.stringify(updatedVal));
});
// change collapsed in several items
export const BatchOperation = atom(null, (get, set, payload) => {
let prevOptions = get(collapseAtom);
let newOptions = {};
for (const item of payload.items) {
newOptions[item.id] = payload.isCollapsed;
}
if (payload.clear) {
// clear object
prevOptions = {};
// clear localstorage
localStorage.removeItem(PATH);
}
const options = { ...prevOptions, ...newOptions };
set(collapseAtom, options);
localStorage.setItem(PATH, JSON.stringify(options));
});
-34
View File
@@ -1,34 +0,0 @@
const { atom } = require('jotai');
const PATH = 'option-gen';
const initialValue = {
cursor: 'locked',
};
export const settingsAtom = atom(
(get) => {
const storedOptions = localStorage.getItem(PATH);
if (storedOptions == null) return initialValue;
return JSON.parse(storedOptions);
},
(get, set, newValues) => {
set(settingsAtom, newValues);
localStorage.setItem(PATH, JSON.stringify(newValues));
}
);
// get a single option, if it exists
export const SelectSetting = (setting) => {
return atom((get) => get(settingsAtom)[setting]);
};
// change a single item in object
export const HandleOptions = atom(null, (get, set, payload) => {
const updatedVal = {
...get(settingsAtom),
...payload,
};
set(settingsAtom, updatedVal);
localStorage.setItem(PATH, JSON.stringify(updatedVal));
});
+4 -7
View File
@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, useContext, useEffect, useState } from 'react';
import io from 'socket.io-client';
import { serverURL } from 'app/api/apiConstants';
@@ -8,7 +8,7 @@ export const useSocket = () => {
return useContext(SocketContext);
};
function SocketProvider(props) {
function SocketProvider({ children }) {
const [socket, setSocket] = useState();
useEffect(() => {
@@ -17,10 +17,7 @@ function SocketProvider(props) {
return () => s.disconnect();
}, []);
return (
<SocketContext.Provider value={socket}>
{props.children}
</SocketContext.Provider>
);
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
}
export default SocketProvider;
+28
View File
@@ -0,0 +1,28 @@
import { useState } from 'react';
// Roughly from useHooks - useLocalStorage
export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(`ontime-${key}`);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
+32
View File
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from 'react-query';
import { EVENTS_TABLE } from '../api/apiConstants';
export default function useMutateEvents(mutation){
const queryClient = useQueryClient();
return useMutation(mutation, {
onMutate: async (newEvent) => {
// cancel ongoing queries
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
// Return a context with the previous and new event
return { previousEvent, newEvent };
},
// Mutation fails, rollback undoes optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: (newEvent) => {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
},
});
}
-38
View File
@@ -1,38 +0,0 @@
import { useEffect } from 'react';
//
// useTimeout React Hook
//
// React hook for delaying calls with time
export const useTimeout = (
callback, // function to call. No args passed.
timeout = 0, // delay, ms (default: immediately put into JS Event Queue)
{
// manage re-render behavior.
// by default, a re-render in your component will re-define the callback,
// which will cause this timeout to cancel itself.
// to avoid cancelling on re-renders (but still cancel on unmounts),
// set `persistRenders: true,`.
persistRenders = false,
} = {},
// These dependencies are injected for testing purposes.
// (pure functions - where all dependencies are arguments - is often easier to test)
_setTimeout = setTimeout,
_clearTimeout = clearTimeout,
_useEffect = useEffect
) => {
let timeoutId;
const cancel = () => timeoutId && _clearTimeout(timeoutId);
_useEffect(
() => {
timeoutId = _setTimeout(callback, timeout);
return cancel;
},
persistRenders
? [_setTimeout, _clearTimeout]
: [callback, timeout, _setTimeout, _clearTimeout]
);
return cancel;
};
@@ -1,5 +1,5 @@
import { IconButton } from '@chakra-ui/button';
import { FiPlus } from 'react-icons/fi';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
export default function AddIconBtn(props) {
const { clickhandler, ...rest } = props;
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsDown } from 'react-icons/fi';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
export default function ApplyIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Apply delays'>
<IconButton
size={props.size || 'xs'}
icon={<FiChevronsDown />}
size={size || 'xs'}
icon={<FiCheck />}
colorScheme='orange'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
@@ -1,5 +1,5 @@
import { IconButton } from '@chakra-ui/button';
import { FiMinusCircle } from 'react-icons/fi';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
export default function BlockIconBtn(props) {
const { clickhandler, ...rest } = props;
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsUp } from 'react-icons/fi';
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
export default function CollapseBtn(props) {
const { clickhandler } = props;
const { clickhandler, size } = props;
return (
<Tooltip label='Collapse all'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiChevronsUp />}
colorScheme='white'
variant='outline'
@@ -1,5 +1,5 @@
import { Button } from '@chakra-ui/button';
import { FiTarget } from 'react-icons/fi';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
export default function CurrentBtn(props) {
const { clickhandler, active } = props;
@@ -0,0 +1,23 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
export default function CursorDownBtn(props) {
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Move cursor down Alt + ↓'>
<IconButton
ref={ref}
size={size || 'xs'}
icon={<IoCaretDown />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
backgroundColor={active ? 'pink.400' : undefined}
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiTarget } from 'react-icons/fi';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
export default function LockIconBtn(props) {
const { clickhandler, active, ref } = props;
export default function CursorLockedBtn(props) {
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Lock cursor to current'>
<IconButton
<IconButton
ref={ref}
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiTarget />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
@@ -0,0 +1,23 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
export default function CursorUpBtn(props) {
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Move cursor up Alt + ↑'>
<IconButton
ref={ref}
size={size || 'xs'}
icon={<IoCaretUp />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
backgroundColor={active ? 'pink.400' : undefined}
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -1,5 +1,5 @@
import { IconButton } from '@chakra-ui/button';
import { FiClock } from 'react-icons/fi';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
export default function DelayIconBtn(props) {
const { clickhandler, ...rest } = props;
@@ -1,10 +1,11 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { useState } from 'react';
import { IoRemove } from 'react-icons/io5';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { Tooltip } from '@chakra-ui/tooltip';
export default function DeleteIconBtn(props) {
const { actionHandler, ...rest } = props;
const { actionHandler, size, ...rest } = props;
const [loading, setLoading] = useState(false);
const handleClick = () => {
@@ -15,7 +16,7 @@ export default function DeleteIconBtn(props) {
return (
<Tooltip label='Delete'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<IoRemove />}
colorScheme='red'
onClick={handleClick}
@@ -1,11 +1,13 @@
import { IoCloseSharp, IoCheckmarkSharp } from 'react-icons/io5';
import React from 'react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { Button } from '@chakra-ui/button';
export default function EnableBtn(props) {
const { active, text, actionHandler } = props;
const { active, text, actionHandler, size } = props;
return (
<Button
size={props.size || 'xs'}
size={size || 'xs'}
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsDown } from 'react-icons/fi';
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
export default function ExpandBtn(props) {
const { clickhandler } = props;
const { clickhandler, size } = props;
return (
<Tooltip label='Expand all'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiChevronsDown />}
colorScheme='white'
variant='outline'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlaySkipForward } from 'react-icons/io5';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { Tooltip } from '@chakra-ui/tooltip';
export default function NextIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlaySkipForward size='22px' />}
colorScheme='whiteAlpha'
@@ -1,19 +1,19 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoMicSharp, IoMicOffOutline } from 'react-icons/io5';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { Tooltip } from '@chakra-ui/tooltip';
export default function OnAirIconBtn(props) {
const { actionHandler, active, ...rest } = props;
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() =>
actionHandler('update', { field: 'isPublic', value: !active })
}
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPause } from 'react-icons/io5';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { Tooltip } from '@chakra-ui/tooltip';
export default function PauseIconBtn(props) {
const { clickhandler, active, ...rest } = props;
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPause size='24px' />}
colorScheme='orange'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlaySkipBack } from 'react-icons/io5';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { Tooltip } from '@chakra-ui/tooltip';
export default function PrevIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlaySkipBack size='22px' />}
colorScheme='whiteAlpha'
@@ -1,19 +1,18 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiUsers } from 'react-icons/fi';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
export default function PublicIconBtn(props) {
const { actionHandler, active, ...rest } = props;
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiUsers />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() =>
actionHandler('update', { field: 'isPublic', value: !active })
}
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoReload } from 'react-icons/io5';
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
import { Tooltip } from '@chakra-ui/tooltip';
export default function ReloadIconButton(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoReload size='22px' />}
icon={<IoArrowUndo size='22px' />}
colorScheme='whiteAlpha'
backgroundColor='#ffffff05'
variant='outline'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoTimeOutline } from 'react-icons/io5';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Tooltip } from '@chakra-ui/tooltip';
export default function RollIconBtn(props) {
const { clickhandler, active, ...rest } = props;
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoTimeOutline size='24px' />}
colorScheme='blue'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlay } from 'react-icons/io5';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { Tooltip } from '@chakra-ui/tooltip';
export default function StartIconBtn(props) {
const { clickhandler, active, ...rest } = props;
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlay size='24px' />}
colorScheme='green'
@@ -1,5 +1,5 @@
import { IconButton } from '@chakra-ui/button';
import { FiTrash2 } from 'react-icons/fi';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
export default function TrashIconBtn(props) {
const { clickhandler, ...rest } = props;
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoStop } from 'react-icons/io5';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Tooltip } from '@chakra-ui/tooltip';
export default function UnloadIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoStop size='22px' />}
colorScheme='red'
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoSunny } from 'react-icons/io5';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { Tooltip } from '@chakra-ui/tooltip';
export default function VisibleIconBtn(props) {
const { actionHandler, active, ...rest } = props;
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
<IconButton
size={props.size || 'xs'}
icon={<IoSunny size={'18px'}/>}
size={size || 'xs'}
icon={<IoSunny size='18px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() =>
@@ -1,7 +1,8 @@
import React from 'react';
import PropTypes from "prop-types";
import style from "../../../features/info/Info.module.scss";
import {Icon} from "@chakra-ui/react";
import {FiChevronUp} from "react-icons/fi";
import {FiChevronUp} from "@react-icons/all-files/fi/FiChevronUp";
export default function CollapseBar(props) {
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
@@ -1,15 +1,16 @@
import { memo } from 'react';
import React, { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import styles from './Countdown.module.css';
const Countdown = ({ time, small, negative, hideZeroHours }) => {
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
// prepare display string
const display =
time != null && !isNaN(time)
? formatDisplay(time, hideZeroHours)
: '-- : -- : --';
const colour = negative ? '#ff7597' : '#fffffa';
const colour = isNegative ? '#ff7597' : '#fffffa';
return (
<div
@@ -22,3 +23,10 @@ const Countdown = ({ time, small, negative, hideZeroHours }) => {
};
export default memo(Countdown);
Countdown.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -1,3 +1,4 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
@@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types';
@@ -8,6 +8,13 @@ export default function EventTimes(props) {
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext);
/**
* This code is duplicated from EventTimesVertical
* @description Validates a time input against its pair
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
* @param {number} val - field value
* @return {boolean}
*/
const handleValidate = (entry, val) => {
if (val == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true;
@@ -18,8 +25,10 @@ export default function EventTimes(props) {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else if (entry === 'durationOverride'){
return true;
} else {
return;
return false
}
const valid = validateTimes(start, end);
@@ -54,8 +63,8 @@ export default function EventTimes(props) {
EventTimes.propTypes = {
actionHandler: PropTypes.func.isRequired,
delay: PropTypes.number.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
delay: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
previousEnd: PropTypes.number,
};
@@ -1,6 +1,6 @@
import React, { useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-utils/time';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types';
@@ -56,11 +56,11 @@ const TimesDelayed = (props) => {
TimesDelayed.propTypes = {
handleValidate: PropTypes.func.isRequired,
actionHandler: PropTypes.func.isRequired,
delay: PropTypes.number.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
delay: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
};
const Times = (props) => {
@@ -102,16 +102,23 @@ const Times = (props) => {
Times.propTypes = {
handleValidate: PropTypes.func.isRequired,
actionHandler: PropTypes.func.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
};
export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration, previousEnd } = props;
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
const { emitWarning } = useContext(LoggingContext);
/**
* This code is duplicated from EventTimes
* @description Validates a time input against its pair
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
* @param {number} val - field value
* @return {boolean}
*/
const handleValidate = (entry, val) => {
if (val == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true;
@@ -122,8 +129,10 @@ export default function EventTimesVertical(props) {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else if (entry === 'durationOverride') {
return true;
} else {
return;
return false;
}
const valid = validateTimes(start, end);
@@ -134,10 +143,10 @@ export default function EventTimesVertical(props) {
return valid.value;
};
return delay != null && delay > 0 ? (
return delay != null && delay !== 0 ? (
<TimesDelayed
handleValidate={handleValidate}
actionHandler={props.actionHandler}
actionHandler={actionHandler}
delay={delay}
timeStart={timeStart}
timeEnd={timeEnd}
@@ -147,7 +156,7 @@ export default function EventTimesVertical(props) {
) : (
<Times
handleValidate={handleValidate}
actionHandler={props.actionHandler}
actionHandler={actionHandler}
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
@@ -157,9 +166,10 @@ export default function EventTimesVertical(props) {
}
EventTimesVertical.propTypes = {
delay: PropTypes.number.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
};
delay: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
};
@@ -1,3 +1,4 @@
import React from 'react';
import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css';
+1 -1
View File
@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from "prop-types";
import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react';
import navlogo from 'assets/images/logos/LOGO-72.png';
import style from './NavLogo.module.scss';
@@ -1,14 +1,14 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/button';
import { FiCheck } from 'react-icons/fi';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { AppContext } from '../../../app/context/AppContext';
import { useContext, useEffect, useState } from 'react';
export default function ProtectRoute(props) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
export default function ProtectRoute({ children }) {
const isLocal =
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext);
@@ -23,7 +23,7 @@ export default function ProtectRoute(props) {
if (!r) {
setFailed(true);
}
}
};
return (
<>
@@ -56,12 +56,12 @@ export default function ProtectRoute(props) {
</div>
</div>
) : (
props.children
children
)}
</>
);
}
ProtectRoute.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react';
import TodayItem from './TodayItem';
import style from './Paginator.module.css';
import { useEffect, useState } from 'react';
import { useInterval } from 'app/hooks/useInterval';
import PropTypes from 'prop-types';
export default function Paginator(props) {
const { events, selectedId } = props;
const LIMIT_PER_PAGE = props.limit || 8;
const SCROLL_TIME = props.time * 1000 || 10000;
const { events, selectedId, limit = 7, time = 10, isBackstage } = props;
const LIMIT_PER_PAGE = limit;
const SCROLL_TIME = time * 1000 || 10000;
const [numEvents, setNumEvents] = useState(0);
const [page, setPage] = useState([]);
const [pages, setPages] = useState(0);
@@ -46,10 +47,7 @@ export default function Paginator(props) {
<div className={style.nav}>
{pages > 1 &&
[...Array(pages)].map((p, i) => (
<div
key={i}
className={i === selPage ? style.navItemSelected : style.navItem}
/>
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
))}
</div>
<div className={style.entries}>
@@ -63,6 +61,7 @@ export default function Paginator(props) {
timeStart={e.timeStart}
timeEnd={e.timeEnd}
title={e.title}
colour={isBackstage ? e.colour : ''}
backstageEvent={!e.isPublic}
/>
);
@@ -71,3 +70,11 @@ export default function Paginator(props) {
</>
);
}
Paginator.propTypes = {
events: PropTypes.array,
selectedId: PropTypes.string,
limit: PropTypes.number,
time: PropTypes.number,
isBackstage: PropTypes.bool,
};
@@ -1,3 +1,4 @@
import React from 'react';
import style from './TitleCard.module.css';
export default function TitleCard(props) {
@@ -1,3 +1,4 @@
import React from 'react';
import style from './TitleSide.module.css';
export default function TitleSide(props) {
@@ -1,25 +1,38 @@
import React from 'react';
import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css';
import PropTypes from 'prop-types';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
// Format timers
const start = stringFromMillis(timeStart, false) || '';
const end = stringFromMillis(timeEnd, false) || '';
// user colours
const userColour = colour !== '' ? colour : 'transparent';
// select styling
let selectStyle = style.entryPast;
if (selected === 1) selectStyle = style.entryNow;
else if (selected === 2) selectStyle = style.entryFuture;
return (
<div className={selectStyle}>
<div
className={`${style.entryTimes} ${
backstageEvent ? style.backstage : undefined
}`}
>{`${start} · ${end}`}</div>
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : undefined}`}>
{`${start} · ${end}`}
</div>
<div className={style.entryTitle}>{title}</div>
{backstageEvent && <div className={style.backstageInd}/>}
{backstageEvent && <div className={style.backstageInd} />}
</div>
);
}
TodayItem.propTypes = {
selected: PropTypes.bool,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
title: PropTypes.string,
backstageEvent: PropTypes.bool,
colour: PropTypes.string,
};
+27
View File
@@ -0,0 +1,27 @@
import React, { useEffect, useRef } from 'react';
import { Textarea } from '@chakra-ui/react';
import autosize from 'autosize/dist/autosize';
export const AutoTextArea = (props) => {
const ref = useRef();
useEffect(() => {
const node = ref.current;
autosize(ref.current);
return () => {
autosize.destroy(node);
};
}, []);
return (
<Textarea
overflow='hidden'
w='100%'
resize='none'
ref={ref}
transition='height none'
{...props}
/>
);
};
+25 -21
View File
@@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { clamp } from '../../app/utils/math';
import style from './TimeInput.module.css';
import PropTypes from 'prop-types';
const inputProps = {
width: 20,
@@ -8,46 +10,48 @@ const inputProps = {
backgroundColor: 'rgba(0,0,0,0.05)',
color: '#fff',
border: '1px solid #ecc94b55',
borderRadius: '4px',
borderRadius: '8px',
placeholder: '-',
textAlign: 'center',
};
export default function DelayInput(props) {
const [value, setValue] = useState(props.value);
const { actionHandler, value } = props;
const [_value, setValue] = useState(value);
useEffect(() => {
if (props.value == null) return;
setValue(props.value);
}, [props.value]);
const handleChange = (val) => {
if (val <= 666) setValue(val);
};
if (value == null) return;
setValue(value);
}, [value]);
const handleSubmit = (val) => {
if (val === props.value) return;
if (val === value) return;
if (val === '') setValue(0);
if (val >= 0 && val <= 60) {
// convert to ms and updates
props.actionHandler('update', { field: 'duration', value: val * 60000 });
} else {
props.actionHandler('update', { field: 'duration', value: 3600000 });
}
// convert to ms and updates
const msVal = clamp(val, -60, 60) * 60000;
actionHandler('update', { field: 'duration', value: msVal });
};
const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`;
return (
<div className={style.timeInput}>
<Editable
{...inputProps}
value={value}
onChange={(v) => handleChange(v)}
value={_value}
onChange={(v) => setValue(v)}
onSubmit={(v) => handleSubmit(v)}
>
<EditablePreview />
<EditableInput type='number' min='0' max='60' />
<EditableInput type='number' min='-60' max='60' />
</Editable>
<span className={style.label}>{'minutes'}</span>
<span className={style.label}>{labelText}</span>
</div>
);
}
DelayInput.propTypes = {
actionHandler: PropTypes.func,
value: PropTypes.number,
};
+23 -12
View File
@@ -1,21 +1,27 @@
import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useEffect, useState } from 'react';
import style from './EditableText.module.css';
import style from './EditableText.module.scss';
import PropTypes from 'prop-types';
export default function EditableText(props) {
const { label, defaultValue, placeholder, submitHandler, ...rest } = props;
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
const [text, setText] = useState(defaultValue || '');
const maxchar = props.maxchar || 40;
useEffect(() => {
if (defaultValue == null) setText('');
else setText(defaultValue);
}, [defaultValue]);
const handleSubmit = (submitedVal) => {
const handleSubmit = (submittedVal) => {
// No need to update if it hasnt changed
if (submitedVal === defaultValue) return;
submitHandler(submitedVal);
if (submittedVal === defaultValue) return;
// submit a cleaned up version of the string
const cleanVal = submittedVal.trim();
submitHandler(cleanVal);
if (cleanVal !== submittedVal) {
setText(cleanVal);
}
};
const handleChange = (val) => {
@@ -33,12 +39,17 @@ export default function EditableText(props) {
className={style.inline}
{...rest}
>
<EditablePreview
color={text === '' ? '#666' : 'inherit'}
maxWidth='75%'
/>
<EditableInput overflowX='hidden' maxWidth='75%' />
<EditablePreview className={text === '' ? style.preview : ''} />
<EditableInput />
</Editable>
</div>
);
}
EditableText.propTypes = {
label: PropTypes.string,
defaultValue: PropTypes.string,
placeholder: PropTypes.string,
submitHandler: PropTypes.func.isRequired,
maxchar: PropTypes.number,
};
@@ -1,19 +0,0 @@
.title {
padding-left: 1em;
font-size: 0.75em;
color: #aaa;
display: inline-block;
width: 6em;
}
.block {
display: 'block';
overflow: hidden;
}
.inline {
display: inline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -0,0 +1,28 @@
@use '../../styles/main' as *;
.block {
overflow: hidden;
display: flex;
align-items: center;
width: 100%;
.title {
padding-left: 1em;
font-size: 0.75em;
color: $label-gray;
display: inline-block;
min-width: 6em;
}
.preview {
color: #666;
}
.inline {
display: inline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
+21 -16
View File
@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useContext, useEffect, useState } from 'react';
import { forgivingStringToMillis } from '../utils/dateConfig';
import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
@@ -12,6 +12,16 @@ export default function EditableTimer(props) {
const [value, setValue] = useState('');
// prepare time fields
const validateValue = (value) => {
const success = handleSubmit(value);
if (success) {
const ms = forgivingStringToMillis(value);
setValue(stringFromMillis(ms + delay));
} else {
setValue(stringFromMillis(time + delay));
}
};
useEffect(() => {
if (time == null) return;
try {
@@ -21,27 +31,19 @@ export default function EditableTimer(props) {
}
}, [time, delay, emitError]);
const validateValue = (value) => {
const success = handleSubmit(value);
if (success) setValue(value);
else setValue(stringFromMillis(time + delay, true));
};
const handleSubmit = (value) => {
// Check if there is anything there
if (value === '') return false;
let newValMillis;
let newValMillis = 0;
// check for known aliases
if (value === 'p' || value === 'prev' || value === 'previous') {
// string to pass should be the time of the end before
if (previousEnd != null) {
newValMillis = previousEnd;
} else {
newValMillis = 0;
}
} else if (value.startsWith('+')) {
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
// string to pass should add to the end before
const val = value.substring(1);
newValMillis = previousEnd + forgivingStringToMillis(val);
@@ -65,16 +67,19 @@ export default function EditableTimer(props) {
return true;
};
const isDelayed = delay != null && delay !== 0;
return (
<Editable
data-testid='editable-timer'
onChange={(v) => setValue(v)}
onSubmit={(v) => validateValue(v)}
onCancel={() => setValue(stringFromMillis(time + delay, true))}
value={value}
className={delay > 0 ? style.delayedEditable : style.editable}
className={isDelayed ? style.delayedEditable : style.editable}
>
<EditablePreview />
<EditableInput type='text' placeholder='--:--:--' />
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
</Editable>
);
}
@@ -82,8 +87,8 @@ export default function EditableTimer(props) {
EditableTimer.propTypes = {
name: PropTypes.string.isRequired,
actionHandler: PropTypes.func.isRequired,
time: PropTypes.number.isRequired,
delay: PropTypes.number.isRequired,
time: PropTypes.number,
delay: PropTypes.number,
validate: PropTypes.func.isRequired,
previousEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number,
};
@@ -2,13 +2,12 @@
.delayedEditable {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 4px;
width: 6.5em;
letter-spacing: 1px;
height: fit-content;
text-align: center;
border-radius: 4px;
border-radius: 8px;
}
.delayedEditable {
@@ -0,0 +1,32 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import EditableTimer from '../EditableTimer';
const renderEditableTimer = (
name = 'test',
actionHandler = () => undefined,
validate = () => undefined
) => render(<EditableTimer name={name} actionHandler={actionHandler} validate={validate} />);
describe('test EditableTimer component', () => {
const testName = "test";
const actionHandler = jest.fn();
const validate = jest.fn();
renderEditableTimer(testName, actionHandler, validate);
const editableTimer = screen.getByTestId('editable-timer');
const editableInput = screen.getByTestId('editable-timer-input');
it('renders correctly', () => {
expect(editableTimer).toBeInTheDocument();
expect(editableInput).toBeInTheDocument();
userEvent.type(editableInput, 'p');
expect(editableInput).toHaveValue('p');
userEvent.type(editableInput, '{enter}');
// no previous is given, defaults to 0
expect(validate).toHaveBeenCalledWith(testName, 0);
});
});
+1
View File
@@ -1,3 +1,4 @@
import React from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import style from './Empty.module.css';
@@ -1,11 +1,12 @@
import {
forgivingStringToMillis,
formatDisplay,
isTimeString,
millisToMinutes,
millisToSeconds,
forgivingStringToMillis,
timeStringToMillis,
} from '../dateConfig';
import { stringFromMillis } from 'ontime-utils/time';
describe('test string from formatDisplay function', () => {
it('test with null values', () => {
@@ -49,6 +50,13 @@ describe('test string from formatDisplay function', () => {
});
});
describe('test formatDisplay handles partial secs', () => {
it('test with 1795829', () => {
const t = { val: 1795829, result: '00:29:55' };
expect(stringFromMillis(t.val)).toBe(t.result);
});
});
describe('test string from formatDisplay function with hidezero', () => {
it('test with null values', () => {
const t = { val: null, result: '00:00' };
@@ -94,22 +102,22 @@ describe('test string from formatDisplay function with hidezero', () => {
describe('test millisToSeconds function', () => {
it('test with null values', () => {
const t = { val: null, result: 0 };
expect(millisToSeconds(t.val, false)).toBe(t.result);
expect(millisToSeconds(t.val)).toBe(t.result);
});
it('test with valid millis', () => {
const t = { val: 3600000, result: 3600 };
expect(millisToSeconds(t.val, false)).toBe(t.result);
expect(millisToSeconds(t.val)).toBe(t.result);
});
it('test with negative millis', () => {
const t = { val: -3600000, result: -3600 };
expect(millisToSeconds(t.val, false)).toBe(t.result);
expect(millisToSeconds(t.val)).toBe(t.result);
});
it('test with 0', () => {
const t = { val: 0, result: 0 };
expect(millisToSeconds(t.val, false)).toBe(t.result);
expect(millisToSeconds(t.val)).toBe(t.result);
});
it('test with -0', () => {
@@ -272,33 +280,87 @@ describe('test isTimeString() function handle different separators', () => {
}
});
describe('test timeHelper() function handles separators', () => {
const ts = ['1:2:3:10', '2,10', '2.10'];
for (const s of ts) {
test(`it handles ${s}`, () => {
expect(typeof forgivingStringToMillis(s)).toBe('number');
});
}
});
describe('test forgivingStringToMillis()', () => {
describe('function handles separators', () => {
const testData = [
{ value: '1:2:3:10', expect: 3723000 },
{ value: '2,10', expect: 130000 },
{ value: '2.10', expect: 130000 },
{ value: '2 10', expect: 130000 },
];
describe('test timeHelper() parses strings correctly', () => {
const ts = [
{ value: '', expect: 0 },
{ value: '0', expect: 0 },
{ value: '-0', expect: 0 },
{ value: '1', expect: 60 * 1000 },
{ value: '-1', expect: 60 * 1000 },
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
];
for (const s of testData) {
test(`it handles ${s.value}`, () => {
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
for (const s of ts) {
test(`it handles ${s.value}`, () => {
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
describe('function handles time with no separators', () => {
const testData = [
{ value: '000000', expect: 0 },
{ value: '000001', expect: 1000 },
{ value: '000100', expect: 1000*60 },
{ value: '010000', expect: 1000*60*60 },
{ value: '230000', expect: 1000*60*60*23 },
{ value: '121212', expect: 12*1000+12*60*1000+12*1000*60*60 },
];
for (const s of testData) {
test(`it handles ${s.value}`, () => {
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
describe('parses strings correctly', () => {
const ts = [
{ value: '', expect: 0 },
{ value: '0', expect: 0 },
{ value: '-0', expect: 0 },
{ value: '1', expect: 60 * 1000 },
{ value: '-1', expect: 60 * 1000 },
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
];
for (const s of ts) {
test(`it handles ${s.value}`, () => {
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
describe('handles overflows', () => {
const ts = [
// minutes overflow
{ value: '120', expect: 1000*60*120 },
{ value: '2.0.0', expect: 1000*60*120 },
{ value: '99', expect: 1000*60*99 },
{ value: '1.39.0', expect: 1000*60*99 },
// seconds overflow
{ value: '0.120', expect: 120*1000 },
{ value: '0.0.120', expect: 120*1000 },
{ value: '0.2.0', expect: 120*1000 },
{ value: '0.99', expect: 99*1000 },
{ value: '0.0.99', expect: 99*1000 },
{ value: '0.1.39', expect: 99*1000 },
// hours overflow
{ value: '25.0.0', expect: 1000*60*60*25 },
// hours overflow
{ value: '50.0.0', expect: 1000*60*60*50 },
];
for (const s of ts) {
test(`it handles ${s.value}`, () => {
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
});
@@ -1,111 +1,122 @@
import {formatEventList, getEventsWithDelay, trimEventlist} from "../eventsManager";
import { formatEventList, getEventsWithDelay, trimEventlist } from '../eventsManager';
test('getEventsWithDelay function', () => {
const testData = [
{
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"type": "event",
"id": "5946"
title: 'Welcome to Ontime',
timeStart: 28800000,
timeEnd: 30600000,
colour: '',
type: 'event',
id: '5946',
},
{
"duration": 60000,
"type": "delay",
"id": "24240"
duration: 60000,
type: 'delay',
id: '24240',
},
{
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"type": "event",
"id": "8ee5"
title: 'Unless recalled by the OSC address',
timeStart: 34920000,
timeEnd: 35520000,
colour: '',
type: 'event',
id: '8ee5',
},
{
"title": "Use simpler times to create a timer",
"timeStart": 120000,
"timeEnd": 720000,
"type": "event",
"id": "8222"
title: 'Use simpler times to create a timer',
timeStart: 120000,
timeEnd: 720000,
colour: '',
type: 'event',
id: '8222',
},
{
"duration": 900000,
"type": "delay",
"revision": 0,
"id": "a386"
duration: 900000,
type: 'delay',
revision: 0,
id: 'a386',
},
{
"title": "Add delay blocks to affect all events",
"timeStart": 37320000,
"timeEnd": 38520000,
"type": "event",
"id": "6dce"
title: 'Add delay blocks to affect all events',
timeStart: 37320000,
timeEnd: 38520000,
colour: '',
type: 'event',
id: '6dce',
},
{
"title": "Add and remove events with [+] and [-]",
"timeStart": 38520000,
"timeEnd": 45120000,
"type": "event",
"id": "2651"
title: 'Add and remove events with [+] and [-]',
timeStart: 38520000,
timeEnd: 45120000,
colour: '',
type: 'event',
id: '2651',
},
{
"type": "block",
"id": "e6a1"
type: 'block',
id: 'e6a1',
},
{
"title": "And control whether they are public",
"timeStart": 46800000,
"timeEnd": 57600000,
"type": "event",
"id": "1358"
}
title: 'And control whether they are public',
timeStart: 46800000,
timeEnd: 57600000,
colour: '',
type: 'event',
id: '1358',
},
];
const expected = [
{
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"type": "event",
"id": "5946"
title: 'Welcome to Ontime',
timeStart: 28800000,
timeEnd: 30600000,
colour: '',
type: 'event',
id: '5946',
},
{
"title": "Unless recalled by the OSC address",
"timeStart": 34920000+60000,
"timeEnd": 35520000+60000,
"type": "event",
"id": "8ee5"
title: 'Unless recalled by the OSC address',
timeStart: 34920000 + 60000,
timeEnd: 35520000 + 60000,
colour: '',
type: 'event',
id: '8ee5',
},
{
"title": "Use simpler times to create a timer",
"timeStart": 120000+60000,
"timeEnd": 720000+60000,
"type": "event",
"id": "8222"
title: 'Use simpler times to create a timer',
timeStart: 120000 + 60000,
timeEnd: 720000 + 60000,
colour: '',
type: 'event',
id: '8222',
},
{
"title": "Add delay blocks to affect all events",
"timeStart": 37320000+60000+900000,
"timeEnd": 38520000+60000+900000,
"type": "event",
"id": "6dce"
title: 'Add delay blocks to affect all events',
timeStart: 37320000 + 60000 + 900000,
timeEnd: 38520000 + 60000 + 900000,
colour: '',
type: 'event',
id: '6dce',
},
{
"title": "Add and remove events with [+] and [-]",
"timeStart": 38520000+60000+900000,
"timeEnd": 45120000+60000+900000,
"type": "event",
"id": "2651"
title: 'Add and remove events with [+] and [-]',
timeStart: 38520000 + 60000 + 900000,
timeEnd: 45120000 + 60000 + 900000,
colour: '',
type: 'event',
id: '2651',
},
{
"title": "And control whether they are public",
"timeStart": 46800000,
"timeEnd": 57600000,
"type": "event",
"id": "1358"
}
]
title: 'And control whether they are public',
timeStart: 46800000,
timeEnd: 57600000,
colour: '',
type: 'event',
id: '1358',
},
];
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
});
@@ -135,6 +146,7 @@ describe('getEventsWithDelay edge cases', () => {
{
"title": "Welcome to Ontime",
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
@@ -147,6 +159,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -155,13 +168,15 @@ describe('getEventsWithDelay edge cases', () => {
{
"title": "Welcome to Ontime",
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
{
"title": "Unless recalled by the OSC address",
"timeStart": 34920000+60000,
"timeEnd": 35520000+60000,
"timeStart": 34920000 + 60000,
"timeEnd": 35520000 + 60000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -176,6 +191,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
@@ -187,6 +203,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -196,6 +213,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
@@ -203,6 +221,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -216,32 +235,32 @@ describe('test trimEventlist function', () => {
const limit = 8;
const testData = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{id: '9'},
{id: '10'},
{id: '11'},
{id: '12'},
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
{ id: '9' },
{ id: '10' },
{ id: '11' },
{ id: '12' },
];
test('when we use the first item', () => {
const selectedId = '1';
const expected = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
];
const l = trimEventlist(testData, selectedId, limit);
@@ -252,14 +271,14 @@ describe('test trimEventlist function', () => {
test('when we use the third item', () => {
const selectedId = '3';
const expected = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'}
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' }
];
const l = trimEventlist(testData, selectedId, limit);
@@ -270,14 +289,14 @@ describe('test trimEventlist function', () => {
test('when we use the fourth item', () => {
const selectedId = '4';
const expected = [
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{id: '9'}
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
{ id: '9' }
];
const l = trimEventlist(testData, selectedId, limit);
@@ -288,14 +307,14 @@ describe('test trimEventlist function', () => {
test('if selected is not found', () => {
const selectedId = '15';
const expected = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
];
const l = trimEventlist(testData, selectedId, limit);
@@ -314,25 +333,27 @@ describe('test formatEvents function', () => {
"timeStart": 28800000,
"timeEnd": 30600000,
"isPublic": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "5946"
},
{
"title": "Unless recalled by the OSC address",
"subtitle": "",
"presenter": "",
"note": "In green, below",
"timeStart": 34800000,
"timeEnd": 35400000,
"isPublic": false,
"type": "event",
"revision": 0,
"id": "8ee5"
}
},
{
"title": "Unless recalled by the OSC address",
"subtitle": "",
"presenter": "",
"note": "In green, below",
"timeStart": 34800000,
"timeEnd": 35400000,
"isPublic": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "8ee5"
}
];
test ('it parses correctly', () => {
test('it parses correctly', () => {
const selectedId = 'otherEvent';
const nextId = 'notHere';
const expected = [
@@ -342,6 +363,7 @@ describe('test formatEvents function', () => {
title: 'Welcome to Ontime',
isNow: false,
isNext: false,
colour: ""
},
{
id: '8ee5',
@@ -349,6 +371,7 @@ describe('test formatEvents function', () => {
title: 'Unless recalled by the OSC address',
isNow: false,
isNext: false,
colour: "",
},
]
@@ -357,7 +380,7 @@ describe('test formatEvents function', () => {
expect(parsed).toStrictEqual(expected);
});
test ('it handles selected correctly', () => {
test('it handles selected correctly', () => {
const selectedId = '5946';
const nextId = '8ee5';
const expected = [
@@ -367,6 +390,7 @@ describe('test formatEvents function', () => {
title: 'Welcome to Ontime',
isNow: true,
isNext: false,
colour: "",
},
{
id: '8ee5',
@@ -374,15 +398,16 @@ describe('test formatEvents function', () => {
title: 'Unless recalled by the OSC address',
isNow: false,
isNext: true,
colour: "",
},
]
const parsed = formatEventList(testEvent, selectedId, nextId,true);
const parsed = formatEventList(testEvent, selectedId, nextId, true);
expect(parsed).toStrictEqual(expected);
});
test ('it handles next correctly', () => {
test('it handles next correctly', () => {
const selectedId = '8ee5';
const nextId = 'notHere';
@@ -393,6 +418,7 @@ describe('test formatEvents function', () => {
title: 'Welcome to Ontime',
isNow: false,
isNext: false,
colour: "",
},
{
id: '8ee5',
@@ -400,6 +426,7 @@ describe('test formatEvents function', () => {
title: 'Unless recalled by the OSC address',
isNow: true,
isNext: false,
colour: "",
},
]
+18 -7
View File
@@ -86,28 +86,39 @@ const parse = (valueAsString) => {
/**
* @description Parses a time string to millis
* @param string - time string
* @param {string} value - time string
* @returns {number} - time string in millis
*/
export const forgivingStringToMillis = (string) => {
export const forgivingStringToMillis = (value) => {
let millis = 0;
// split string at known separators : , .
const separatorRegex = /[\s,:.]+/;
const [first, second, third] = string.split(separatorRegex);
const [first, second, third] = value.split(separatorRegex);
if (first != null && second != null && third != null) {
// if string has three sections, treat as [hours] [minutes] [seconds]
millis = parse(first) * mth;
millis += parse(second) * mtm;
millis += parse(third) * mts;
} else if (third == null) {
} else if (first != null && second != null && third == null) {
// if string has two sections, treat as [minutes] [seconds]
millis = parse(first) * mtm;
millis += parse(second) * mts;
} else if (second == null) {
// if string has one section, treat as [minutes]
millis = parse(first) * mtm;
} else if (first != null && second == null && third == null) {
// if string has one section,
// could be a complete string like 121010 - 12:10:10
if (first.length === 6) {
const hours = first.substring(0, 2);
const minutes = first.substring(2, 4);
const seconds = first.substring(4);
millis = parse(hours) * mth;
millis += parse(minutes) * mtm;
millis += parse(seconds) * mts;
} else {
// otherwise lets treat as [minutes]
millis = parse(first) * mtm;
}
}
return millis;
};
+6 -5
View File
@@ -6,7 +6,6 @@ import { stringFromMillis } from 'ontime-utils/time';
* @returns {Object[]} Filtered events with calculated delays
*/
export const getEventsWithDelay = (events) => {
if (events == null) return [];
const unfilteredEvents = [...events];
@@ -43,8 +42,11 @@ export const trimEventlist = (events, selectedId, limit) => {
if (limit != null) {
while (trimmedEvents.length > limit) {
const idx = trimmedEvents.findIndex((e) => e.id === selectedId);
if (idx <= BEFORE) { trimmedEvents.pop(); }
else { trimmedEvents.shift(); }
if (idx <= BEFORE) {
trimmedEvents.pop();
} else {
trimmedEvents.shift();
}
}
}
return trimmedEvents;
@@ -75,10 +77,9 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
title: g.title,
isNow: g.id === selectedId,
isNext: g.id === nextId,
colour: g.colour,
});
}
return formattedEvents;
};
-13
View File
@@ -1,13 +0,0 @@
/**
* Handles link to external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export default function handleLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Returns hostname
* @type {string}
*/
export const host = window?.location?.host;
/**
* Open an external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export function openLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
/**
* Handles opening external links
* @param event
* @param location
*/
export function handleLinks(event, location) {
// we handle the link manually
event.preventDefault();
openLink(`http://${host}/${location}`);
}
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
import OnAirIconBtn from '../../common/components/buttons/OnAirIconBtn';
@@ -10,14 +10,14 @@ const inputProps = {
};
const InputRow = (props) => {
const { label, placeholder, text, visible } = props;
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
return (
<>
<span className={style.label}>{label}</span>
<div className={style.inputItems}>
<Editable
onChange={(event) => props.changeHandler(event)}
onChange={(event) => changeHandler(event)}
value={text}
placeholder={placeholder}
className={style.inline}
@@ -28,7 +28,7 @@ const InputRow = (props) => {
</Editable>
<VisibleIconBtn
active={visible || undefined}
actionHandler={props.actionHandler}
actionHandler={actionHandler}
{...inputProps}
/>
</div>
@@ -1,4 +1,4 @@
import { memo } from 'react';
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import style from './PlaybackControl.module.scss';
import StartIconBtn from 'common/components/buttons/StartIconBtn';
@@ -69,20 +69,20 @@ const Transport = (props) => {
};
const PlaybackButtons = (props) => {
const { playback, selectedId, noEvents } = props;
const { playback, selectedId, noEvents, playbackControl } = props;
return (
<>
<Playback
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
playbackControl={props.playbackControl}
playbackControl={playbackControl}
/>
<Transport
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
playbackControl={props.playbackControl}
playbackControl={playbackControl}
/>
</>
);
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import style from './PlaybackControl.module.scss';
import { useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import PlaybackButtons from './PlaybackButtons';
import PlaybackTimer from './PlaybackTimer';
+17 -16
View File
@@ -1,3 +1,4 @@
import React from 'react';
import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown';
import { stringFromMillis } from 'ontime-utils/time';
@@ -9,6 +10,7 @@ import PropTypes from 'prop-types';
const areEqual = (prevProps, nextProps) => {
return (
prevProps.timer.running === nextProps.timer.running &&
prevProps.timer.isNegative === nextProps.timer.isNegative &&
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
prevProps.timer.startedAt === nextProps.timer.startedAt &&
prevProps.playback === nextProps.playback &&
@@ -17,23 +19,22 @@ const areEqual = (prevProps, nextProps) => {
);
};
const incrementProps = {
size: 'sm',
width: '2.9em',
colorScheme: 'whiteAlpha',
variant: 'outline',
_focus: { boxShadow: 'none' },
};
const PlaybackTimer = (props) => {
const { timer, playback, handleIncrement, selectedId } = props;
const started = stringFromMillis(timer.startedAt, true);
const finish = stringFromMillis(timer.expectedFinish, true);
const isNegative = timer.running < 0;
const isRolling = playback === 'roll';
const isWaiting = timer.secondary > 0 && timer.running == null;
const disableButtons = selectedId == null || isRolling;
const incrementProps = {
size: 'sm',
width: '2.9em',
colorScheme: 'whiteAlpha',
variant: 'outline',
_focus: { boxShadow: 'none' },
};
return (
<>
<div className={style.timeContainer}>
@@ -42,21 +43,21 @@ const PlaybackTimer = (props) => {
<div className={isRolling ? style.indRollActive : style.indRoll} />
</Tooltip>
<div
className={isNegative ? style.indNegativeActive : style.indNegative}
className={timer.isNegative ? style.indNegativeActive : style.indNegative}
/>
<div className={style.indDelay} />
</div>
<div className={style.timer}>
<Countdown
time={isWaiting ? timer.secondary : timer.running}
isNegative={timer.isNegative}
small
negative={isNegative}
/>
</div>
{isWaiting ? (
<div className={style.roll}>
<span className={style.rolltag}>Roll: Countdown to start</span>
<span className={style.time}>{''}</span>
<span className={style.time}>FIX</span>
</div>
) : (
<>
@@ -72,7 +73,7 @@ const PlaybackTimer = (props) => {
)}
<div className={style.btn}>
<Tooltip
label={'Remove 1 minute'}
label='Remove 1 minute'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -85,7 +86,7 @@ const PlaybackTimer = (props) => {
</Button>
</Tooltip>
<Tooltip
label={'Add 1 minute'}
label='Add 1 minute'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -98,7 +99,7 @@ const PlaybackTimer = (props) => {
</Button>
</Tooltip>
<Tooltip
label={'Remove 5 minutes'}
label='Remove 5 minutes'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -111,7 +112,7 @@ const PlaybackTimer = (props) => {
</Button>
</Tooltip>
<Tooltip
label={'Add 5 minutes'}
label='Add 5 minutes'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -1,8 +1,9 @@
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from 'react-icons/fi';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import ActionButtons from '../list/ActionButtons';
import style from './BlockBlock.module.css';
import style from './BlockBlock.module.scss';
import PropTypes from 'prop-types';
export default function BlockBlock(props) {
@@ -1,3 +1,4 @@
@use '../../../styles/main' as *;
/* ============= COMMON ============= */
.block {
@@ -8,7 +9,7 @@
width: 100%;
border-radius: 8px;
font-size: 15px;
border: 1px solid rgba(255, 255, 255, 0.05);
border: $block-border;
display: grid;
grid-template-columns: 2em auto;
@@ -16,7 +17,7 @@
align-items: baseline;
gap: 0.5em;
background-color: #805ad5;
background-color: $block-block-color;
background: linear-gradient(
0deg,
rgba(107, 70, 193, 0.4) 0%,
@@ -34,7 +35,6 @@
/* ============== ACTION ================ */
.actionOverlay {
display: flex;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@@ -1,11 +1,12 @@
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from 'react-icons/fi';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { millisToMinutes } from 'common/utils/dateConfig';
import ActionButtons from '../list/ActionButtons';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
import DelayInput from 'common/input/DelayInput';
import style from './DelayBlock.module.css';
import style from './DelayBlock.module.scss';
import PropTypes from 'prop-types';
export default function DelayBlock(props) {
@@ -1,3 +1,5 @@
@use '../../../styles/main' as *;
/* ============= COMMON ============= */
.delay {
@@ -7,7 +9,7 @@
width: 100%;
border-radius: 8px;
font-size: 15px;
border: 1px solid rgba(255, 255, 255, 0.05);
border: $block-border;
display: grid;
grid-template-columns: 2em 1fr auto;
@@ -17,7 +19,7 @@
align-items: baseline;
gap: 0.5em;
background-color: #ecc94b;
background-color: $block-delay-color;
background: linear-gradient(
180deg,
rgba(214, 158, 46, 0.4) 0%,
@@ -30,7 +32,7 @@
.drag {
grid-area: drag;
color: rgba(255, 255, 255, 0.67);
color: $block-icon-drag;
align-self: center;
}
+54 -47
View File
@@ -1,15 +1,16 @@
import { lazy, useEffect } from 'react';
import React, { lazy, useEffect } from 'react';
import { Box } from '@chakra-ui/layout';
import { useDisclosure } from '@chakra-ui/hooks';
import styles from './Editor.module.scss';
import MenuBar from 'features/menu/MenuBar';
import ModalManager from 'features/modals/ModalManager';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { LoggingProvider } from '../../app/context/LoggingContext';
import { LocalEventSettingsProvider } from '../../app/context/LocalEventSettingsContext';
import { CursorProvider } from '../../app/context/CursorContext';
import { CollapseProvider } from '../../app/context/CollapseContext';
import styles from './Editor.module.scss';
const EventListWrapper = lazy(() =>
import('features/editors/list/EventListWrapper')
);
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
const MessageControl = lazy(() => import('features/control/MessageControl'));
const Info = lazy(() => import('features/info/Info'));
@@ -24,53 +25,59 @@ export default function Editor() {
return (
<LoggingProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<LocalEventSettingsProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</Box>
<div className={styles.mainContainer}>
<CursorProvider>
<CollapseProvider>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</Box>
<Box className={styles.editor}>
<h1>Event List</h1>
<div className={styles.content}>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.editor}>
<h1>Event List</h1>
<div className={styles.content}>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</div>
</Box>
</CollapseProvider>
</CursorProvider>
<Box className={styles.messages}>
<h1>Display Messages</h1>
<div className={styles.content}>
<ErrorBoundary>
<MessageControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.messages}>
<h1>Display Messages</h1>
<div className={styles.content}>
<ErrorBoundary>
<MessageControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.playback}>
<h1>Timer Control</h1>
<div className={styles.content}>
<ErrorBoundary>
<PlaybackControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.playback}>
<h1>Timer Control</h1>
<div className={styles.content}>
<ErrorBoundary>
<PlaybackControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.info}>
<h1>Info</h1>
<div className={styles.content}>
<ErrorBoundary>
<Info />
</ErrorBoundary>
</div>
</Box>
</div>
<Box className={styles.info}>
<h1>Info</h1>
<div className={styles.content}>
<ErrorBoundary>
<Info />
</ErrorBoundary>
</div>
</Box>
</div>
</LocalEventSettingsProvider>
</LoggingProvider>
);
}
@@ -0,0 +1,73 @@
import React, { useContext, useEffect, useState } from 'react';
import { Tooltip } from '@chakra-ui/tooltip';
import { Checkbox } from '@chakra-ui/react';
import style from './EntryBlock.module.scss';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
export default function EntryBlock(props) {
const { showKbd, index, eventsHandler } = props;
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
const [doPublic, setPublic] = useState(defaultPublic);
useEffect(() => {
setStartTime(starTimeIsLastEnd);
}, [starTimeIsLastEnd]);
useEffect(() => {
setPublic(defaultPublic);
}, [defaultPublic]);
return (
<div className={style.create}>
<Tooltip label='Add Event' openDelay={300}>
<span
className={style.createEvent}
onClick={() =>
eventsHandler(
'add',
{ type: 'event', order: index + 1, isPublic: doPublic },
{ startIsLastEnd: doStartTime ? index : undefined }
)
}
>
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
</span>
</Tooltip>
<Tooltip label='Add Delay' openDelay={300}>
<span
className={style.createDelay}
onClick={() => eventsHandler('add', { type: 'delay', order: index + 1 })}
>
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
</span>
</Tooltip>
<Tooltip label='Add Block' openDelay={300}>
<span
className={style.createBlock}
onClick={() => eventsHandler('add', { type: 'block', order: index + 1 })}
>
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
</span>
</Tooltip>
<div className={style.options}>
<Checkbox
size='sm'
colorScheme='blue'
isChecked={doStartTime}
onChange={(e) => setStartTime(e.target.checked)}
>
Start time is last end
</Checkbox>
<Checkbox
size='sm'
colorScheme='blue'
isChecked={doPublic}
onChange={(e) => setPublic(e.target.checked)}
>
Default public
</Checkbox>
</div>
</div>
);
}
@@ -0,0 +1,92 @@
@use '../../../styles/main' as *;
.create {
padding: 0 0.5em;
box-sizing: border-box;
width: 100%;
border-radius: 8px;
font-size: 12px;
position: relative;
height: 20px;
margin: -8px 0;
transition: height 0.1s ease;
* {
display: none;
}
&:hover {
opacity: 1;
height: calc(2.5em + 20px);
display: flex;
align-items: center;
justify-content: center;
gap: 5%;
transition: height 0.15s ease;
* {
display: flex;
}
}
.createEvent,
.createDelay,
.createBlock {
width: auto;
padding: 0 16px;
height: 24px;
text-align: center;
vertical-align: center;
font-weight: 600;
line-height: 21px;
border-radius: 4px;
opacity: 0.6;
.keyboard {
margin-left: 4px;
padding: 0 4px;
color: #ccc;
border-radius: 2px;
font-family: Monospaced, sans-serif;
background-color: rgba(0, 0, 0, 0.24);
}
}
.createEvent {
border: 1px solid #2b6cb0;
color: lighten(#2b6cb0, 30%);
&:hover {
background-color: #2b6cb0;
opacity: 1;
}
}
.createDelay {
border: 1px solid #ecc94b;;
color: lighten(#ecc94b, 30%);
&:hover {
background-color: #ecc94b;
opacity: 1;
}
}
.createBlock {
border: 1px solid #805ad5;
color: lighten(#805ad5, 30%);
&:hover {
background-color: #805ad5;
opacity: 1;
}
}
.options {
display: flex;
flex-direction: column;
opacity: 0.65;
}
}
@@ -1,6 +1,7 @@
import React, { useContext, useMemo } from 'react';
import Icon from '@chakra-ui/icon';
import { FiChevronUp, FiMoreVertical } from 'react-icons/fi';
import { useMemo } from 'react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { Draggable } from 'react-beautiful-dnd';
import EventTimes from 'common/components/eventTimes/EventTimes';
import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical';
@@ -9,38 +10,31 @@ import ActionButtons from '../list/ActionButtons';
import PublicIconBtn from 'common/components/buttons/PublicIconBtn';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import { millisToMinutes } from 'common/utils/dateConfig';
import style from './EventBlock.module.css';
import { HandleCollapse, SelectCollapse } from 'app/context/collapseAtom';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types';
import { CollapseContext } from '../../../app/context/CollapseContext';
import style from './EventBlock.module.css';
const ExpandedBlock = (props) => {
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
const oscid = data.id.length > 4 ? '...' : data.id;
// if end is before, assume is the day after
const duration =
data.timeStart > data.timeEnd
? data.timeEnd + 86400000 - data.timeStart
: data.timeEnd - data.timeStart;
const oscid = data?.id || '...';
return (
<>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
<FiMoreVertical />
</span>
<div className={style.indicators}>
<span className={next ? style.next : style.nextDisabled}>Next</span>
{delayValue != null && <span className={style.delayValue}>+ {delayValue}</span>}
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
</div>
<div className={style.timeExpanded}>
<EventTimesVertical
actionHandler={actionHandler}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
duration={duration}
duration={data.duration}
delay={delay}
previousEnd={previousEnd}
className={style.time}
@@ -93,8 +87,8 @@ ExpandedBlock.propTypes = {
eventIndex: PropTypes.number.isRequired,
next: PropTypes.bool.isRequired,
delay: PropTypes.number,
delayValue: PropTypes.number,
previousEnd: PropTypes.number.isRequired,
delayValue: PropTypes.string,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
};
@@ -104,12 +98,12 @@ const CollapsedBlock = (props) => {
return (
<>
<span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical />
<FiMoreVertical />
</span>
<div className={style.indicators}>
<span className={next ? style.next : style.nextDisabled}>Next</span>
{delayValue != null && <span className={style.delayValue}>+ {delayValue}</span>}
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
</div>
<EventTimes
actionHandler={actionHandler}
@@ -140,25 +134,27 @@ CollapsedBlock.propTypes = {
data: PropTypes.object.isRequired,
next: PropTypes.bool.isRequired,
delay: PropTypes.any,
delayValue: PropTypes.any,
delayValue: PropTypes.string,
previousEnd: PropTypes.number.isRequired,
actionHandler: PropTypes.func.isRequired,
};
export default function EventBlock(props) {
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler } = props;
const [collapsed] = useAtom(useMemo(() => SelectCollapse(data.id), [data.id]));
const [, setCollapsed] = useAtom(HandleCollapse);
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props;
const { isCollapsed, setCollapsed } = useContext(CollapseContext);
const collapsed = useMemo(() => isCollapsed(data.id), [data.id, isCollapsed]);
const isSelected = selected ? style.active : '';
const isCollapsed = collapsed ? style.collapsed : style.expanded;
const classSelect = `${style.event} ${isCollapsed} ${isSelected}`;
const selectedStyle = selected ? style.active : '';
const collapsedStyle = collapsed ? style.collapsed : style.expanded;
const classSelect = `${style.event} ${collapsedStyle} ${selectedStyle}`;
// Calculate delay in min
const delayValue = delay > 0 ? millisToMinutes(delay) : null;
let delayValue = null;
if (delay != null && delay !== 0) {
delayValue = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
}
const handleCollapse = (isCollapsed) => {
setCollapsed({ [data.id]: isCollapsed });
setCollapsed(data.id, isCollapsed);
};
return (
@@ -174,7 +170,7 @@ export default function EventBlock(props) {
<CollapsedBlock
provided={provided}
data={data}
next={props.next}
next={next}
delay={delay}
delayValue={delayValue}
previousEnd={previousEnd}
@@ -185,7 +181,7 @@ export default function EventBlock(props) {
provided={provided}
eventIndex={eventIndex}
data={data}
next={props.next}
next={next}
delay={delay}
delayValue={delayValue}
previousEnd={previousEnd}
@@ -204,6 +200,7 @@ EventBlock.propTypes = {
delay: PropTypes.number,
index: PropTypes.number.isRequired,
eventIndex: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
next: PropTypes.bool,
};
@@ -142,7 +142,7 @@
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 4px;
border-radius: 8px;
width: 100%;
display: block;
@@ -150,11 +150,13 @@
overflow: hidden;
}
.collapsed .titleContasiner {
.collapsed .titleContainer {
height: fit-content;
}
.expanded .titleContainer {
height: 100%;
border-radius: 4px;
}
.oscLabel {
@@ -1,6 +1,9 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button';
import { FiClock, FiMinusCircle, FiPlus } from 'react-icons/fi';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { Tooltip } from '@chakra-ui/tooltip';
export default function ActionButtons(props) {
@@ -21,24 +24,16 @@ export default function ActionButtons(props) {
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'}
color={'orange.500'}
backgroundColor='orange.200'
color='orange.500'
/>
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem
icon={<FiPlus />}
onClick={() => actionHandler('event')}
isDisabled={!showAdd}
>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem
icon={<FiClock />}
onClick={() => actionHandler('delay')}
isDisabled={!showDelay}
>
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')} isDisabled={!showDelay}>
Add Delay after
</MenuItem>
<MenuItem
+43 -36
View File
@@ -1,21 +1,22 @@
import React, { createRef, useCallback, useContext, useEffect, useState } from 'react';
import style from './List.module.scss';
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import Empty from 'common/state/Empty';
import EventListItem from './EventListItem';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { useAtom } from 'jotai';
import { SelectSetting } from 'app/context/settingsAtom';
import EntryBlock from '../EntryBlock/EntryBlock';
import { CursorContext } from '../../../app/context/CursorContext';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
export default function EventList(props) {
const { events, eventsHandler } = props;
const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } =
useContext(CursorContext);
const socket = useSocket();
const [selectedId, setSelectedId] = useState(null);
const [nextId, setNextId] = useState(null);
const [cursor, setCursor] = useState(0);
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
const cursorRef = createRef();
const { showQuickEntry } = useContext(LocalEventSettingsContext);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
@@ -26,13 +27,11 @@ export default function EventList(props) {
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) {
// Arrow down
if (e.keyCode === 40) {
if (cursor == null) setCursor(0);
else if (cursor < events.length - 1) setCursor(cursor + 1);
if (cursor < events.length - 1) moveCursorDown();
}
// Arrow up
if (e.keyCode === 38) {
if (cursor == null) setCursor(0);
else if (cursor > 0) setCursor(cursor - 1);
if (cursor > 0) moveCursorUp();
}
// E
if (e.key === 'e' || e.key === 'E') {
@@ -54,7 +53,7 @@ export default function EventList(props) {
}
}
},
[cursor, events, eventsHandler]
[cursor, events.length, eventsHandler, moveCursorDown, moveCursorUp]
);
useEffect(() => {
@@ -67,7 +66,7 @@ export default function EventList(props) {
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleKeyPress, cursor, events]);
}, [handleKeyPress, cursor, events, setCursor]);
// handle incoming messages
useEffect(() => {
@@ -101,13 +100,14 @@ export default function EventList(props) {
block: 'nearest',
inline: 'start',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cursor]);
// if selected event
// or cursor settings changed
useEffect(() => {
// and if we are locked
if (cursorSettings !== 'locked' || selectedId == null) return;
if (!isCursorLocked || selectedId == null) return;
// move cursor
let gotoIndex = -1;
@@ -123,7 +123,8 @@ export default function EventList(props) {
// move cursor
setCursor(gotoIndex);
}
}, [selectedId, cursorSettings]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId, isCursorLocked]);
if (events.length < 1) {
return <Empty text='No Events' />;
@@ -155,11 +156,7 @@ export default function EventList(props) {
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId='eventlist'>
{(provided) => (
<div
className={style.list}
{...provided.droppableProps}
ref={provided.innerRef}
>
<div className={style.list} {...provided.droppableProps} ref={provided.innerRef}>
{events.map((e, index) => {
if (index === 0) {
cumulativeDelay = 0;
@@ -174,24 +171,34 @@ export default function EventList(props) {
previousEnd = thisEnd;
thisEnd = e.timeEnd;
}
return (
<div
ref={cursor === index ? cursorRef : undefined}
key={e.id}
className={cursor === index ? style.cursor : undefined}
>
<EventListItem
type={e.type}
index={index}
eventIndex={eventIndex}
data={e}
selected={selectedId === e.id}
next={nextId === e.id}
eventsHandler={eventsHandler}
delay={cumulativeDelay}
previousEnd={previousEnd}
/>
<div key={e.id}>
{index === 0 && showQuickEntry && (
<EntryBlock index={-1} eventsHandler={eventsHandler} />
)}
<div
ref={cursor === index ? cursorRef : undefined}
className={cursor === index ? style.cursor : undefined}
>
<EventListItem
type={e.type}
index={index}
eventIndex={eventIndex}
data={e}
selected={selectedId === e.id}
next={nextId === e.id}
eventsHandler={eventsHandler}
delay={cumulativeDelay}
previousEnd={previousEnd}
/>
</div>
{showQuickEntry && (
<EntryBlock
showKbd={index === cursor}
index={index}
eventsHandler={eventsHandler}
/>
)}
</div>
);
})}
@@ -1,8 +1,9 @@
import React, { memo, useCallback, useContext } from 'react';
import DelayBlock from '../DelayBlock/DelayBlock';
import BlockBlock from '../BlockBlock/BlockBlock';
import EventBlock from '../EventBlock/EventBlock';
import { memo, useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
const areEqual = (prevProps, nextProps) => {
return (
@@ -29,46 +30,79 @@ const EventListItem = (props) => {
...rest
} = props;
const { emitError } = useContext(LoggingContext);
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
/**
* @description calculates duration from given options
* @param {number} start
* @param {number} end
* @returns {number}
*/
const calculateDuration = useCallback(
(start, end) => (start > end ? end + 86400000 - start : end - start),
[]
);
// Create / delete new events
const actionHandler = (action, payload) => {
switch (action) {
case 'event':
eventsHandler('add', { type: 'event', order: index + 1 });
break;
case 'delay':
eventsHandler('add', { type: 'delay', order: index + 1 });
break;
case 'block':
eventsHandler('add', { type: 'block', order: index + 1 });
break;
case 'delete':
eventsHandler('delete', data.id);
break;
case 'update':
// Handles and filters update requests
const { field, value } = payload;
if (field === 'durationOverride') {
// duration defines timeEnd
let end = (data.timeStart += value);
const newData = { id: data.id, timeEnd: end };
const actionHandler = useCallback(
(action, payload) => {
switch (action) {
case 'event':
eventsHandler(
'add',
{
type: 'event',
order: index + 1,
isPublic: defaultPublic,
},
{ startIsLastEnd: starTimeIsLastEnd ? index : undefined }
);
break;
case 'delay':
eventsHandler('add', { type: 'delay', order: index + 1 });
break;
case 'block':
eventsHandler('add', { type: 'block', order: index + 1 });
break;
case 'delete':
eventsHandler('delete', data.id);
break;
case 'update':
// Handles and filters update requests
const { field, value } = payload;
const newData = { id: data.id };
// request update in parent
eventsHandler('patch', newData);
} else if (field in data) {
// create object with new field
const newData = { id: data.id, [field]: value };
if (field === 'durationOverride') {
// duration defines timeEnd
newData.timeEnd = data.timeStart += value;
// request update in parent
eventsHandler('patch', newData);
} else {
emitError(`Unknown field: ${field}`);
}
break;
default:
break;
}
};
// request update in parent
eventsHandler('patch', newData);
} else if (field === 'timeStart') {
newData.duration = calculateDuration(value, data.timeEnd);
newData.timeStart = value;
// request update in parent
eventsHandler('patch', newData);
} else if (field === 'timeEnd') {
newData.duration = calculateDuration(data.timeStart, value);
newData.timeEnd = value;
// request update in parent
eventsHandler('patch', newData);
} else if (field in data) {
// create object with new field
newData[field] = value;
// request update in parent
eventsHandler('patch', newData);
} else {
emitError(`Unknown field: ${field}`);
}
break;
default:
break;
}
},
[data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
);
switch (type) {
case 'event':
@@ -85,9 +119,7 @@ const EventListItem = (props) => {
/>
);
case 'block':
return (
<BlockBlock index={index} data={data} actionHandler={actionHandler} />
);
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
case 'delay':
return (
<DelayBlock
@@ -1,32 +1,28 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { useCallback, useContext, useEffect, useState } from 'react';
import { EVENTS_TABLE } from 'app/api/apiConstants';
import { LoggingContext } from '../../../app/context/LoggingContext';
import {
fetchAllEvents,
requestApplyDelay,
requestDelete,
requestDeleteAll,
requestPatch,
requestPost,
requestPut,
requestDelete,
requestDeleteAll,
requestReorder,
requestApplyDelay,
} from 'app/api/eventsApi.js';
import { useFetch } from 'app/hooks/useFetch.js';
import EventList from './EventList';
import EventListMenu from 'features/menu/EventListMenu.jsx';
import { useFetch } from 'app/hooks/useFetch.js';
import Empty from 'common/state/Empty';
import { EVENTS_TABLE } from 'app/api/apiConstants';
import { BatchOperation } from 'app/context/collapseAtom';
import { useAtom } from 'jotai';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { CollapseContext } from '../../../app/context/CollapseContext';
export default function EventListWrapper() {
const [, setCollapsed] = useAtom(BatchOperation);
const { expandAll, collapseMultiple } = useContext(CollapseContext);
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const { data, status, isError, refetch } = useFetch(
EVENTS_TABLE,
fetchAllEvents
);
const { data, status, isError, refetch } = useFetch(EVENTS_TABLE, fetchAllEvents);
const [events, setEvents] = useState(null);
const addEvent = useMutation(requestPost, {
@@ -72,10 +68,7 @@ export default function EventListWrapper() {
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([
EVENTS_TABLE,
newEvent.id,
]);
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
@@ -86,10 +79,7 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData(
[EVENTS_TABLE, context.newEvent.id],
context.previousEvent
);
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
@@ -105,10 +95,7 @@ export default function EventListWrapper() {
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([
EVENTS_TABLE,
newEvent.id,
]);
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
@@ -119,15 +106,16 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData(
[EVENTS_TABLE, context.newEvent.id],
context.previousEvent
);
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: (newEvent) => {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
if (newEvent) {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
} else {
queryClient.invalidateQueries(EVENTS_TABLE);
}
},
});
@@ -237,11 +225,20 @@ export default function EventListWrapper() {
// Events API
const eventsHandler = useCallback(
async (action, payload) => {
async (action, payload, options = undefined) => {
switch (action) {
case 'add':
try {
await addEvent.mutateAsync(payload);
let newEvent = { ...payload };
// there is an option to pass an index of an array to use as start time
if (options?.startIsLastEnd !== undefined) {
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
}
// hard coding duration value to be as expected for now
// this until timeOptions gets implemented
// Todo: implement duration options
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
await addEvent.mutateAsync(newEvent);
} catch (error) {
emitError(`Error fetching data: ${error.message}`);
}
@@ -307,11 +304,12 @@ export default function EventListWrapper() {
case 'collapseall':
if (data == null) return;
setCollapsed({ clear: true, items: data, isCollapsed: true });
collapseMultiple(data);
break;
case 'expandall':
if (data == null) return;
setCollapsed({ clear: true, items: data, isCollapsed: false });
expandAll();
break;
case 'deleteall':
@@ -326,6 +324,7 @@ export default function EventListWrapper() {
break;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
);
@@ -23,11 +23,11 @@
.cursor {
width: 100%;
background: linear-gradient(
180deg,
#ff7597 2%,
#0001 3%,
#0001 97%,
#ff7597 98%
180deg,
#ff7597 2%,
#0001 3%,
#0001 97%,
#ff7597 98%
);
border-radius: 14px;
+7 -7
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { useSocket } from 'app/context/socketContext';
import style from './Info.module.scss';
import InfoTitle from './InfoTitle';
@@ -45,10 +45,10 @@ export default function Info() {
if (data.total === 0 || data.total == null) {
setSelected('No events');
} else {
const formatedCurrent = `Event ${
const formattedCurrent = `Event ${
data.index != null ? data.index + 1 : '-'
}/${data.total != null ? data.total : '-'}`;
setSelected(formatedCurrent);
}/${data.total ? data.total : '-'}`;
setSelected(formattedCurrent);
}
});
@@ -78,12 +78,12 @@ export default function Info() {
return (
<>
<div className={style.main}>
<span>{`Running on port 4001`}</span>
<span>Running on port 4001</span>
<span>{selected}</span>
</div>
<InfoNif />
<InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} />
<InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} />
<InfoTitle title='Now' data={titlesNow} roll={playback === 'roll'} />
<InfoTitle title='Next' data={titlesNext} roll={playback === 'roll'} />
<InfoLogger />
</>
);
+2 -2
View File
@@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import style from './InfoLogger.module.scss';
import CollapseBar from "../../common/components/collapseBar/CollapseBar";
import { LoggingContext } from '../../app/context/LoggingContext';
@@ -54,7 +54,7 @@ export default function InfoLogger() {
return (
<div className={collapsed ? style.container : style.container__expanded}>
<CollapseBar title={'Log'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} />
{!collapsed && (
<>
<div className={style.toggleBar}>
+20 -21
View File
@@ -1,10 +1,10 @@
import { useState } from 'react';
import React, { useState } from 'react';
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.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import handleLink from '../../common/utils/handleLink';
import { openLink } from '../../common/utils/linkUtils';
import style from './Info.module.scss';
export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -14,26 +14,25 @@ export default function InfoNif() {
const baseURL = 'http://__IP__:4001';
return (
<div className={style.container}>
<CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
<div className={style.container}>
<CollapseBar
title='Network Info'
isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)}
/>
{!collapsed && (
<div>
{status === 'success' && (
<>
{data?.networkInterfaces.map((e) => {
return (
<a
key={e.address}
href='#!'
onClick={() =>
handleLink(baseURL.replace('__IP__', e.address))
}
className={style.if}
>{`${e.name} - ${e.address}`}</a>
);
})}
</>
)}
{status === 'success' &&
data?.networkInterfaces.map((e) => (
<a
key={e.address}
href='#!'
onClick={() => openLink(baseURL.replace('__IP__', e.address))}
className={style.if}
>
{`${e.name} - ${e.address}`}
</a>
))}
</div>
)}
</div>
+2 -2
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Icon } from '@chakra-ui/react';
import { useState } from 'react';
import { FiChevronUp } from 'react-icons/fi';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import style from './Info.module.scss';
export default function InfoTitle(props) {
+21 -19
View File
@@ -1,16 +1,17 @@
import { memo, useMemo } from 'react';
import React, { memo, useContext } from 'react';
import { Divider } from '@chakra-ui/react';
import style from './EventListMenu.module.css';
import { CursorContext } from '../../app/context/CursorContext';
import MenuActionButtons from './MenuActionButtons';
import CollapseBtn from 'common/components/buttons/CollapseBtn';
import ExpandBtn from 'common/components/buttons/ExpandBtn';
import { SelectSetting, HandleOptions } from 'app/context/settingsAtom';
import { useAtom } from 'jotai';
import LockIconBtn from 'common/components/buttons/LockIconBtn';
import CursorUpBtn from '../../common/components/buttons/CursorUpBtn';
import CursorDownBtn from '../../common/components/buttons/CursorDownBtn';
import CursorLockedBtn from 'common/components/buttons/CursorLockedBtn';
import style from './EventListMenu.module.css';
import ExpandBtn from '../../common/components/buttons/ExpandBtn';
const EventListMenu = ({ eventsHandler }) => {
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
const [, SetOption] = useAtom(HandleOptions);
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
useContext(CursorContext);
const actionHandler = (action) => {
switch (action) {
@@ -23,12 +24,14 @@ const EventListMenu = ({ eventsHandler }) => {
case 'block':
eventsHandler('add', { type: action, order: 0 });
break;
case 'cursorUp':
moveCursorUp();
break;
case 'cursorDown':
moveCursorDown();
break;
case 'togglelock':
let newSet = 'locked';
if (cursorSettings === 'locked') {
newSet = 'unlocked';
}
SetOption({ cursor: newSet });
toggleCursorLocked();
break;
case 'deleteall':
eventsHandler('deleteall');
@@ -40,16 +43,15 @@ const EventListMenu = ({ eventsHandler }) => {
return (
<div className={style.headerButtons}>
<CollapseBtn
size='sm'
clickhandler={() => eventsHandler('collapseall')}
/>
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
<Divider orientation='vertical' />
<LockIconBtn
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
<CursorLockedBtn
size='sm'
clickhandler={() => actionHandler('togglelock')}
active={cursorSettings === 'locked'}
active={isCursorLocked}
/>
<Divider orientation='vertical' />
<MenuActionButtons actionHandler={actionHandler} size='sm' />
+11 -14
View File
@@ -1,11 +1,15 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button';
import { FiClock, FiMinusCircle, FiPlus, FiTrash2 } from 'react-icons/fi';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import { Divider } from '@chakra-ui/layout';
import { Tooltip } from '@chakra-ui/tooltip';
export default function MenuActionButtons(props) {
const { actionHandler } = props;
const { actionHandler, size } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
@@ -17,12 +21,12 @@ export default function MenuActionButtons(props) {
<MenuButton
as={IconButton}
aria-label='Create Menu'
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'}
color={'orange.500'}
backgroundColor='orange.200'
color='orange.500'
/>
</Tooltip>
<MenuList style={menuStyle}>
@@ -32,18 +36,11 @@ export default function MenuActionButtons(props) {
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')}>
Add Delay first
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={() => actionHandler('block')}
>
<MenuItem icon={<FiMinusCircle />} onClick={() => actionHandler('block')}>
Add Block first
</MenuItem>
<Divider />
<MenuItem
icon={<FiTrash2 />}
onClick={() => actionHandler('deleteall')}
color='red.500'
>
<MenuItem icon={<FiTrash2 />} onClick={() => actionHandler('deleteall')} color='red.500'>
Delete All
</MenuItem>
</MenuList>
+2 -6
View File
@@ -1,3 +1,4 @@
import React, { useContext, useRef } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
import { EVENTS_TABLE } from 'app/api/apiConstants';
@@ -9,7 +10,6 @@ import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn';
import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
@@ -40,7 +40,6 @@ export default function MenuBar(props) {
const handleUpload = (event) => {
const fileUploaded = event.target.files[0];
console.log('1', fileUploaded)
if (fileUploaded == null) return;
// Limit file size to 1MB
@@ -49,9 +48,6 @@ export default function MenuBar(props) {
return;
}
console.log('2', ! fileUploaded.name.endsWith('.xlsx')
|| !fileUploaded.name.endsWith('.json'))
// Check file extension
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
try {
@@ -145,7 +141,7 @@ export default function MenuBar(props) {
}
MenuBar.propTypes = {
isOpen: PropTypes.bool.isRequired,
isOpen: PropTypes.bool,
onOpen: PropTypes.func.isRequired,
};
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiDownload } from 'react-icons/fi';
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
export default function DownloadIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Export event list'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiDownload />}
colorScheme='white'
onClick={clickhandler}

Some files were not shown because too many files have changed in this diff Show More