Feature: quick start (#369)

* feat: quick start modal

* refactor: end message is part of view settings

* refactor: small tweaks and type improvements
This commit is contained in:
Carlos Valente
2023-04-29 21:20:25 +02:00
committed by GitHub
parent 33b496f98e
commit 38654f8981
41 changed files with 463 additions and 282 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
export const STATIC_PORT = 4001;
// REST stuff
export const EVENTDATA_TABLE = ['eventdata'];
export const EVENT_DATA = ['eventdata'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const RUNDOWN_TABLE_KEY = 'rundown';
+5 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { Alias, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { apiRepoLatest } from '../../externals';
import { InfoType } from '../models/Info';
@@ -169,3 +169,7 @@ export async function getLatestVersion(): Promise<HasUpdate> {
version: res.data.tag_name as string,
};
}
export async function postNew(initialData: Partial<EventData>) {
return axios.post(`${ontimeURL}/new`, initialData);
}
@@ -19,7 +19,6 @@ export const AutoTextArea = (props: AutoTextAreaProps) => {
};
}, []);
return (
<Textarea
overflow='hidden'
@@ -27,7 +26,7 @@ export const AutoTextArea = (props: AutoTextAreaProps) => {
resize='none'
ref={ref}
transition='height none'
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'}
variant={isDark ? 'ontime-filled' : 'ontime-filled-on-light'}
{...rest}
/>
);
@@ -1,13 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { EVENTDATA_TABLE } from '../api/apiConstants';
import { EVENT_DATA } from '../api/apiConstants';
import { fetchEventData } from '../api/eventDataApi';
import { eventDataPlaceholder } from '../models/EventData';
export default function useEventData() {
const { data, status, isError, refetch } = useQuery({
queryKey: EVENTDATA_TABLE,
queryKey: EVENT_DATA,
queryFn: fetchEventData,
placeholderData: eventDataPlaceholder,
retry: 5,
@@ -6,5 +6,4 @@ export const eventDataPlaceholder: EventData = {
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
endMessage: '',
};
@@ -2,4 +2,5 @@ import { ViewSettings } from 'ontime-types';
export const viewsSettingsPlaceholder: ViewSettings = {
overrideStyles: false,
endMessage: '',
};
@@ -7,6 +7,7 @@ import MenuBar from '../menu/MenuBar';
import AboutModal from '../modals/about-modal/AboutModal';
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
import ModalManager from '../modals/ModalManager';
import QuickStart from '../modals/quick-start/QuickStart';
import styles from './Editor.module.scss';
@@ -25,6 +26,7 @@ export default function Editor() {
onClose: onIntegrationModalClose,
} = useDisclosure();
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
// Set window title
useEffect(() => {
@@ -34,6 +36,7 @@ export default function Editor() {
return (
<>
<ErrorBoundary>
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
@@ -52,6 +55,8 @@ export default function Editor() {
onIntegrationOpen={onIntegrationModalOpen}
isAboutOpen={isAboutModalOpen}
onAboutOpen={onAboutModalOpen}
isQuickStartOpen={isQuickStartOpen}
onQuickStartOpen={onQuickStartOpen}
/>
</ErrorBoundary>
</Box>
+17 -4
View File
@@ -1,12 +1,13 @@
import { useCallback, useEffect } from 'react';
import { VStack } from '@chakra-ui/react';
import { FiSave } from '@react-icons/all-files/fi/FiSave';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoHelp } from '@react-icons/all-files/io5/IoHelp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi';
@@ -27,6 +28,8 @@ interface MenuBarProps {
onIntegrationOpen: () => void;
isAboutOpen: boolean;
onAboutOpen: () => void;
isQuickStartOpen: boolean;
onQuickStartOpen: () => void;
}
const buttonStyle = {
@@ -52,6 +55,8 @@ export default function MenuBar(props: MenuBarProps) {
onIntegrationOpen,
isAboutOpen,
onAboutOpen,
isQuickStartOpen,
onQuickStartOpen,
} = props;
const { isElectron, sendToElectron } = useElectronEvent();
@@ -102,7 +107,15 @@ export default function MenuBar(props: MenuBarProps) {
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiUpload />}
icon={<IoColorWand />}
className={isQuickStartOpen ? style.open : ''}
clickHandler={onQuickStartOpen}
tooltip='Quick start'
aria-label='Quick start'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoPushOutline />}
className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen}
tooltip='Upload showfile'
@@ -110,7 +123,7 @@ export default function MenuBar(props: MenuBarProps) {
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiSave />}
icon={<IoSaveOutline />}
clickHandler={downloadRundown}
tooltip='Export showfile'
aria-label='Export showfile'
@@ -32,7 +32,6 @@ export default function SettingsModal() {
publicInfo: data.publicInfo,
backstageUrl: data.backstageUrl,
backstageInfo: data.backstageInfo,
endMessage: data.endMessage,
});
}, [changed, data]);
@@ -168,23 +167,6 @@ export default function SettingsModal() {
onChange={(event) => handleChange('backstageInfo', event.target.value)}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
</div>
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
</form>
@@ -1,6 +1,8 @@
@use '../../theme/v2Styles' as *;
@use '../../theme/ontimeColours' as *;
$el-padding-with-compensation: 24px; // 16 + 8
@mixin modal-link {
color: $blue-500;
transition-property: color;
@@ -14,7 +16,7 @@
.headerNotes {
font-size: $text-body-size;
width: 100%;
padding: 0 $section-spacing;
padding: 0 $el-padding-with-compensation;
color: $modal-note-color;
margin-bottom: $section-spacing;
@@ -24,6 +26,12 @@
}
}
.footerNotes {
font-size: $inner-section-text-size;
padding: 0 $el-padding-with-compensation;
color: $modal-note-color;
}
.divider {
margin: $element-spacing 0;
border: 0;
@@ -31,6 +39,7 @@
}
.sectionContainer {
padding: 8px 16px;
display: flex;
flex-direction: column;
height: 100%
@@ -60,7 +69,7 @@
.sectionTitle {
font-size: $inner-section-text-size;
display: block;
width: 100%;
&.main {
font-weight: 600;
}
@@ -83,9 +92,6 @@
.buttonSection {
margin-top: $section-spacing;
padding-top: $section-spacing;
padding-left: -24px;
border-top: 1px solid $gray-100;
display: flex;
gap: $section-spacing;
}
@@ -96,7 +102,7 @@
.shiftRight {
align-self: flex-end;
margin-right: 8px;
margin-right: $element-spacing;
}
.showPointer {
@@ -109,7 +115,7 @@
}
.padBottom {
padding-bottom: 8px;
padding-bottom: $element-spacing;
}
.logo {
@@ -117,17 +123,22 @@
height: 48px;
display: inline-block;
vertical-align: text-bottom;
margin-right: 16px;
margin-right: $section-spacing;
}
.test {
padding-top: 24px;
.updateSection {
padding-top: $el-padding-with-compensation;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 4px;
gap: $element-inner-spacing;
.error {
font-size: $error-red;
}
}
.overflowContainer {
overflow-y: auto;
max-height: 40%;
}
@@ -1,15 +1,14 @@
import { useCallback, useEffect, useState } from 'react';
import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { useEmitLog } from '@/common/stores/logger';
import { postView } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn';
import useViewSettings from '../../common/hooks-query/useViewSettings';
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
import { useEmitLog } from '../../common/stores/logger';
import { openLink } from '../../common/utils/linkUtils';
import { inputProps } from '../../features/modals/modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -83,44 +82,37 @@ export default function ViewsSettingsModal() {
🔥 Changes take effect immediately 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.hSeparator}>Timer end message</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={50}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
<div className={style.hSeparator}>Style Options</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
CSS Style Overrides
</span>
This feature allows user defined CSS to override the application stylesheets as a way to customise viewers
appearance.
<br />
Currently the feature affects the following views
<br />
<ul className={style.featureList}>
<li>
<IoCheckmarkSharp /> Stage timer
</li>
<li>
<IoCheckmarkSharp /> Clock
</li>
<li>
<IoCheckmarkSharp /> Minimal timer
</li>
<li>
<IoCheckmarkSharp /> Backstage screen
</li>
<li>
<IoCheckmarkSharp /> Public screen
</li>
<li>
<IoCheckmarkSharp /> Countdown
</li>
</ul>
Read more about it in the documentation{' '}
This feature allows user defined CSS to customise viewers appearance. <br />
<a
href='#!'
onClick={() => openLink('https://cpvalente.gitbook.io/ontime/features/custom-styling')}
className={style.if}
>
over at Gitbook
For details on the styling and file location please refer to documentation
</a>
</div>
<div className={style.modalFields}>
@@ -50,7 +50,7 @@ export default function UpdateChecker(props: UpdateCheckerProps) {
const disableButton = Boolean(updateMessage && 'version' in updateMessage);
return (
<div className={styles.test}>
<div className={styles.updateSection}>
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}>
Check for updates
</Button>
@@ -1,4 +1,4 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import ModalWrapper from '../ModalWrapper';
@@ -19,26 +19,28 @@ export default function IntegrationModal(props: IntegrationModalProps) {
return (
<ModalWrapper title='Integration Settings' isOpen={isOpen} onClose={onClose}>
<div className={styles.headerNotes}>
Manage settings related to protocol integrations
<a href={oscDocsUrl} target='_blank' rel='noreferrer'>
Read the docs
</a>
</div>
<Tabs variant='ontime' size='sm' isLazy>
<TabList>
<Tab>OSC</Tab>
<Tab>OSC Integration</Tab>
</TabList>
<TabPanels>
<TabPanel>
<OscSettings />
</TabPanel>
<TabPanel>
<OscIntegration />
</TabPanel>
</TabPanels>
</Tabs>
<ModalBody>
<div className={styles.headerNotes}>
Manage settings related to protocol integrations
<a href={oscDocsUrl} target='_blank' rel='noreferrer'>
Read the docs
</a>
</div>
<Tabs variant='ontime' size='sm' isLazy>
<TabList>
<Tab>OSC</Tab>
<Tab>OSC Integration</Tab>
</TabList>
<TabPanels>
<TabPanel>
<OscSettings />
</TabPanel>
<TabPanel>
<OscIntegration />
</TabPanel>
</TabPanels>
</Tabs>
</ModalBody>
</ModalWrapper>
);
}
@@ -17,7 +17,7 @@ export default function OntimeModalFooter(props: OntimeModalFooterProps) {
const disableSubmit = isSubmitting || !isDirty || !isValid;
return (
<ModalFooter className={styles.buttonSection} paddingInlineStart={0} paddingInlineEnd={0} paddingBottom={0}>
<ModalFooter className={styles.buttonSection}>
<Button isDisabled={disableRevert} variant='ontime-ghost-on-light' size='sm' onClick={handleRevert}>
Revert to saved
</Button>
@@ -1,6 +1,5 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { ModalBody } from '@chakra-ui/react';
import type { OSCSettings, OscSubscription } from 'ontime-types';
import { TimerLifeCycle } from 'ontime-types';
import { generateId } from 'ontime-utils';
@@ -103,27 +102,25 @@ export default function OscIntegration() {
return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
<ModalBody>
{subscriptionKeys.map((cycle, idx) => {
return (
<>
<OscSubscriptionRow
key={cycle}
cycle={cycle as TimerLifeCycle}
title={sectionText[cycle as TimerLifeCycle].title}
subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
visible={showSection === cycle}
setShowSection={setShowSection}
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
handleDelete={deleteSubscriptionEntry}
handleAddNew={addNewSubscriptionEntry}
register={register}
/>
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
</>
);
})}
</ModalBody>
{subscriptionKeys.map((cycle, idx) => {
return (
<>
<OscSubscriptionRow
key={cycle}
cycle={cycle as TimerLifeCycle}
title={sectionText[cycle as TimerLifeCycle].title}
subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
visible={showSection === cycle}
setShowSection={setShowSection}
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
handleDelete={deleteSubscriptionEntry}
handleAddNew={addNewSubscriptionEntry}
register={register}
/>
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
</>
);
})}
<OntimeModalFooter
formId='oscSubscriptions'
handleRevert={resetForm}
@@ -1,5 +1,5 @@
import { useForm } from 'react-hook-form';
import { FormControl, Input, ModalBody, Switch } from '@chakra-ui/react';
import { FormControl, Input, Switch } from '@chakra-ui/react';
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
import { PlaceholderSettings } from '../../../common/models/OscSettings';
@@ -53,114 +53,108 @@ export default function OscSettings() {
};
return (
<>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
<ModalBody>
<div className={styles.splitSection}>
<div>
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
</div>
<Switch {...register('enabledIn')} variant='ontime-on-light' />
</div>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
<div className={styles.splitSection}>
<div>
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
</div>
<Switch {...register('enabledIn')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
<label htmlFor='portIn'>
<span className={styles.sectionTitle}>Listen on Port</span>
{errors.portIn ? (
<span className={styles.error}>{errors.portIn.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 8888</span>
)}
</label>
<Input
id='portIn'
placeholder='8888'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portIn', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
<label htmlFor='portIn'>
<span className={styles.sectionTitle}>Listen on Port</span>
{errors.portIn ? (
<span className={styles.error}>{errors.portIn.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 8888</span>
)}
</label>
<Input
id='portIn'
placeholder='8888'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portIn', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<hr className={styles.divider} />
<div className={styles.splitSection}>
<div>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
OSC Output
</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
</div>
<Switch {...register('enabledOut')} variant='ontime-on-light' />
</div>
<div className={styles.splitSection}>
<div>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
OSC Output
</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
</div>
<Switch {...register('enabledOut')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
<label htmlFor='targetIP'>
<span className={styles.sectionTitle}>OSC target IP</span>
{errors.targetIP ? (
<span className={styles.error}>{errors.targetIP.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
)}
</label>
<Input
id='targetIP'
placeholder='127.0.0.1'
width='140px'
size='sm'
textAlign='right'
variant='ontime-filled-on-light'
{...register('targetIP', {
required: { value: true, message: 'Required field' },
pattern: {
value: isIPAddress,
message: 'Invalid IP address',
},
})}
/>
</FormControl>
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
<label htmlFor='targetIP'>
<span className={styles.sectionTitle}>OSC target IP</span>
{errors.targetIP ? (
<span className={styles.error}>{errors.targetIP.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
)}
</label>
<Input
id='targetIP'
placeholder='127.0.0.1'
width='140px'
size='sm'
textAlign='right'
variant='ontime-filled-on-light'
{...register('targetIP', {
required: { value: true, message: 'Required field' },
pattern: {
value: isIPAddress,
message: 'Invalid IP address',
},
})}
/>
</FormControl>
<FormControl className={styles.splitSection}>
<label htmlFor='portOut'>
<span className={styles.sectionTitle}>OSC target Port</span>
{errors.portOut ? (
<span className={styles.error}>{errors.portOut.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 9999</span>
)}
</label>
<Input
id='portOut'
placeholder='9999'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portOut', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
</ModalBody>
</form>
<FormControl className={styles.splitSection}>
<label htmlFor='portOut'>
<span className={styles.sectionTitle}>OSC target Port</span>
{errors.portOut ? (
<span className={styles.error}>{errors.portOut.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 9999</span>
)}
</label>
<Input
id='portOut'
placeholder='9999'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portOut', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<OntimeModalFooter
formId='oscSettings'
handleRevert={resetForm}
@@ -168,6 +162,6 @@ export default function OscSettings() {
isValid={isValid}
isSubmitting={isSubmitting}
/>
</>
</form>
);
}
@@ -0,0 +1,150 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Textarea,
} from '@chakra-ui/react';
import type { EventData } from 'ontime-types';
import { EVENT_DATA, RUNDOWN_TABLE } from '../../../common/api/apiConstants';
import { postNew } from '../../../common/api/ontimeApi';
import useEventData from '../../../common/hooks-query/useEventData';
import { eventDataPlaceholder } from '../../../common/models/EventData';
import { ontimeQueryClient } from '../../../common/queryClient';
import styles from '../Modal.module.scss';
interface QuickStartProps {
onClose: () => void;
isOpen: boolean;
}
export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
const { data, status } = useEventData();
const {
handleSubmit,
register,
reset,
formState: { isSubmitting },
} = useForm({ defaultValues: data });
useEffect(() => {
reset(data);
}, [reset, data]);
const onSubmit = async (data: Partial<EventData>) => {
await postNew(data);
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
};
const onReset = () => reset(eventDataPlaceholder);
const disableButtons = status !== 'success' || isSubmitting;
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Ontime quick start</ModalHeader>
<ModalCloseButton />
<ModalBody className={styles.pad}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer}>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Event title
<Input
variant='ontime-filled-on-light'
size='sm'
maxLength={50}
placeholder='Eurovision song contest'
{...register('title')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Public Info
<Textarea
variant='ontime-filled-on-light'
size='sm'
maxLength={150}
placeholder='Shows always start ontime'
{...register('publicInfo')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Public QR Code Url
<Input
variant='ontime-filled-on-light'
size='sm'
placeholder='www.getontime.no'
{...register('publicUrl')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Backstage Info
<Textarea
variant='ontime-filled-on-light'
size='sm'
maxLength={150}
placeholder='Wi-Fi password: 1234'
{...register('backstageInfo')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Backstage QR Code Url
<Input
variant='ontime-filled-on-light'
size='sm'
placeholder='www.ontime.gitbook.io'
{...register('backstageUrl')}
/>
</label>
</div>
<div className={styles.footerNotes}>
Note: Application options will be kept but rundown and event data will be reset <br />
</div>
<ModalFooter className={styles.buttonSection}>
<Button onClick={onReset} isDisabled={disableButtons} variant='ontime-ghost-on-light' size='sm'>
Clear data
</Button>
<Button
type='submit'
isLoading={isSubmitting}
isDisabled={disableButtons}
variant='ontime-filled'
padding='0 2em'
size='sm'
>
New showfile
</Button>
</ModalFooter>
</form>
</ModalBody>
</ModalContent>
</Modal>
);
}
@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react';
import { ComponentType, useMemo } from 'react';
import { Playback, TitleBlock } from 'ontime-types';
import { useStore } from 'zustand';
@@ -10,22 +10,23 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions';
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
const withData = (Component: ReactNode) => {
return (props) => {
const withData = <P extends object>(Component: ComponentType<P>) => {
// eslint-disable-next-line react/display-name -- its ok
return (props: P) => {
// persisted app state
const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data
const { data: eventsData } = useRundown();
const { data: genData } = useEventData();
const { data: rundownData } = useRundown();
const { data: eventData } = useEventData();
const { data: viewSettings } = useViewSettings();
const publicEvents = useMemo(() => {
if (Array.isArray(eventsData)) {
return eventsData.filter((e) => e.type === 'event' && e.title && e.isPublic);
if (Array.isArray(rundownData)) {
return rundownData.filter((e) => e.type === 'event' && e.title && e.isPublic);
}
return [];
}, [eventsData]);
}, [rundownData]);
// websocket data
const data = useStore(runtime);
@@ -99,12 +100,12 @@ const withData = (Component: ReactNode) => {
publicTitle={publicTitleManager}
time={TimeManagerType}
events={publicEvents}
backstageEvents={eventsData}
backstageEvents={rundownData}
selectedId={selectedId}
publicSelectedId={publicSelectedId}
viewSettings={viewSettings}
nextId={nextId}
general={genData}
general={eventData}
onAir={onAir}
/>
);
@@ -21,7 +21,7 @@ interface MinimalTimerProps {
}
export default function MinimalTimer(props: MinimalTimerProps) {
const { isMirrored, pres, time, viewSettings, general } = props;
const { isMirrored, pres, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
@@ -129,7 +129,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const isPlaying = time.playback !== Playback.Pause;
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 0) < 0 && general.endMessage && !hideEndMessage;
const showEndMessage = (time.current ?? 0) < 0 && viewSettings.endMessage && !hideEndMessage;
const showFinished =
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
@@ -162,7 +162,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
</div>
)}
{showEndMessage ? (
<div className='end-message'>{general.endMessage}</div>
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
@@ -46,7 +46,7 @@ interface TimerProps {
}
export default function Timer(props: TimerProps) {
const { isMirrored, general, pres, title, time, viewSettings } = props;
const { isMirrored, pres, title, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
@@ -65,7 +65,7 @@ export default function Timer(props: TimerProps) {
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 1) < 0 && general.endMessage;
const showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
const showProgress = time.playback !== Playback.Stop;
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
const showClock = time.timerType !== TimerType.Clock;
@@ -95,7 +95,7 @@ export default function Timer(props: TimerProps) {
<div className='timer-container'>
{showEndMessage ? (
<div className='end-message'>{general.endMessage}</div>
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
+4 -1
View File
@@ -2,7 +2,7 @@ export const ontimeModal = {
header: {
fontWeight: 400,
letterSpacing: '0.3px',
padding: '8px 16px',
padding: '16px 24px',
fontSize: '20px',
color: '#202020', // $gray-50
},
@@ -19,6 +19,9 @@ export const ontimeModal = {
closeButton: {
color: '#202020', // $gray-50
},
footer: {
padding: '8px',
},
};
export const ontimeSmallModal = {
+3
View File
@@ -13,4 +13,7 @@ export const ontimeTab = {
tablist: {
borderBottom: '2px solid #ececec', // $gray-100
},
tabpanel: {
padding: 0,
},
};
+5 -8
View File
@@ -23,13 +23,11 @@ export const ontimeInputFilled = {
export const ontimeInputFilledOnLight = {
field: {
backgroundColor: 'white',
border: '2px solid transparent',
border: '2px solid #f6f6f6', // $gray-50
_hover: {
backgroundColor: 'white',
border: '2px solid #D2DDFF', // $blue-200
},
_focus: {
backgroundColor: 'white',
border: '2px solid #578AF4', // $blue-500
},
},
@@ -42,16 +40,15 @@ export const ontimeTextAreaFilled = {
export const ontimeTextAreaFilledOnLight = {
borderRadius: '3px',
fontWeight: '400',
backgroundColor: '#ececec', // $gray-100
backgroundColor: 'white',
color: '#202020', // $gray-1200
border: '1px solid transparent',
border: '2px solid #f6f6f6', // $gray-50
_hover: {
backgroundColor: '#cfcfcf', // $gray-300
border: '2px solid #D2DDFF', // $blue-200
},
_focus: {
backgroundColor: '#cfcfcf', // $gray-300
color: '#101010',
border: '1px solid #578AF4', // $blue-500
border: '2px solid #578AF4', // $blue-500
},
_placeholder: { color: '#9d9d9d' }, // $gray-500
};
+1 -1
View File
@@ -88,7 +88,7 @@ const theme = extendTheme({
},
variants: {
'ontime-filled': { ...ontimeTextAreaFilled },
'ontime-filled-onlight': { ...ontimeTextAreaFilledOnLight },
'ontime-filled-on-light': { ...ontimeTextAreaFilledOnLight },
},
},
Tooltip: {
@@ -12,7 +12,7 @@ export class DataProvider {
return data;
}
static async setEventData(newData: EventData) {
static async setEventData(newData: Partial<EventData>) {
data.eventData = { ...data.eventData, ...newData };
await this.persist();
return data.eventData;
@@ -8,7 +8,6 @@ describe('safeMerge', () => {
publicUrl: 'existing public URL',
backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
},
settings: {
app: 'ontime',
@@ -20,6 +19,7 @@ describe('safeMerge', () => {
},
viewSettings: {
overrideStyles: false,
endMessage: 'existing endMessage',
},
aliases: [],
userFields: {
@@ -75,7 +75,6 @@ describe('safeMerge', () => {
publicInfo: 'new public info',
backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
});
});
@@ -145,7 +144,6 @@ describe('safeMerge', () => {
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
endMessage: '',
},
settings: {
app: 'ontime',
@@ -157,6 +155,7 @@ describe('safeMerge', () => {
},
viewSettings: {
overrideStyles: false,
endMessage: '',
},
aliases: [],
userFields: {
@@ -1,14 +1,14 @@
import { removeUndefined } from '../utils/parserUtils.js';
import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to 'event'
export const getEvent = async (req, res) => {
export const getEventData = async (req, res) => {
res.json(DataProvider.getEventData());
};
// Create controller for POST request to 'event'
export const postEvent = async (req, res) => {
export const postEventData = async (req, res) => {
if (failEmptyObjects(req.body, res)) {
return;
}
@@ -1,6 +1,6 @@
import { body, validationResult } from 'express-validator';
export const eventSanitizer = [
export const eventDataSanitizer = [
body('title').optional().isString().trim(),
body('publicUrl').optional().isString().trim(),
body('publicInfo').optional().isString().trim(),
@@ -1,7 +1,8 @@
import fs from 'fs';
import type { EventData } from 'ontime-types';
import { networkInterfaces } from 'os';
import { generateId } from 'ontime-utils';
import { fileHandler } from '../utils/parser.ts';
import { fileHandler } from '../utils/parser.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { mergeObject } from '../utils/parserUtils.js';
@@ -10,6 +11,7 @@ import { eventStore } from '../stores/EventStore.js';
import { resolveDbPath } from '../setup.js';
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
import { logger } from '../classes/Logger.js';
import { deleteAllEvents } from '../services/RundownService.js';
// Create controller for GET request to '/ontime/poll'
// Returns data for current state
@@ -56,9 +58,9 @@ const uploadAndParse = async (file, req, res, options) => {
try {
const result = await fileHandler(file);
if (result?.error) {
if ('error' in result && result.error) {
res.status(400).send({ message: result.message });
} else if (result.message === 'success') {
} else if ('data' in result && result.message === 'success') {
PlaybackService.stop();
// explicitly write objects
if (typeof result !== 'undefined') {
@@ -241,7 +243,10 @@ export const postViewSettings = async (req, res) => {
}
try {
const newData = { overrideStyles: req.body.overrideStyles };
const newData = {
overrideStyles: req.body.overrideStyles,
endMessage: req.body?.endMessage || '',
};
await DataProvider.setViewSettings(newData);
res.status(200).send(newData);
} catch (error) {
@@ -288,3 +293,21 @@ export const dbUpload = async (req, res) => {
const file = req.file.path;
await uploadAndParse(file, req, res, options);
};
// Create controller for POST request to '/ontime/new'
export const postNew = async (req, res) => {
try {
const newEventData: Omit<EventData, 'endMessage'> = {
title: req.body?.title ?? '',
publicUrl: req.body?.publicUrl ?? '',
publicInfo: req.body?.publicInfo ?? '',
backstageUrl: req.body?.backstageUrl ?? '',
backstageInfo: req.body?.backstageInfo ?? '',
};
const newData = await DataProvider.setEventData(newEventData);
await deleteAllEvents();
res.status(201).send(newData);
} catch (error) {
res.status(400).send(error);
}
};
@@ -6,6 +6,7 @@ import { validateOscSubscription } from '../utils/parserFunctions.js';
*/
export const viewValidator = [
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
check('endMessage').isString().trim().withMessage('endMessage value must be string'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
+1 -1
View File
@@ -8,7 +8,6 @@ export const dbModel: DatabaseModel = {
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
endMessage: '',
},
settings: {
app: 'ontime',
@@ -20,6 +19,7 @@ export const dbModel: DatabaseModel = {
},
viewSettings: {
overrideStyles: false,
endMessage: '',
},
aliases: [],
userFields: {
+6 -6
View File
@@ -1,12 +1,12 @@
import express from 'express';
// import event controller
import { getEventData, postEventData } from '../controllers/eventDataController.ts';
import { eventDataSanitizer } from '../controllers/eventDataController.validate.ts';
export const router = express.Router();
// import event controller
import { getEvent, postEvent } from '../controllers/eventDataController.js';
import { eventSanitizer } from '../controllers/eventDataController.validate.js';
// create route between controller and 'GET /event' endpoint
router.get('/', getEvent);
router.get('/', getEventData);
// create route between controller and 'POST /event' endpoint
router.post('/', eventSanitizer, postEvent);
router.post('/', eventDataSanitizer, postEventData);
+5
View File
@@ -11,6 +11,7 @@ import {
getViewSettings,
poll,
postAliases,
postNew,
postOSC,
postSettings,
postUserFields,
@@ -24,6 +25,7 @@ import {
validateUserFields,
viewValidator,
} from '../controllers/ontimeController.validate.js';
import { eventDataSanitizer } from '../controllers/eventDataController.validate.js';
export const router = express.Router();
@@ -68,3 +70,6 @@ router.get('/osc', getOSC);
// create route between controller and '/ontime/osc' endpoint
router.post('/osc', validateOSC, postOSC);
// create route between controller and '/ontime/new' endpoint
router.post('/new', eventDataSanitizer, postNew);
+1 -1
View File
@@ -352,7 +352,7 @@ type ResponseError = { error: true; message: string };
* @param {string} file - reference to file
* @return {object} - parse result message
*/
export const fileHandler = async (file) => {
export const fileHandler = async (file): ResponseOK | ResponseError => {
let res: Partial<ResponseOK | ResponseError> = {};
// check which file type are we dealing with
+1 -1
View File
@@ -87,7 +87,6 @@ export const parseEventData = (data, enforce): EventData => {
publicInfo: e.publicInfo || dbModel.eventData.publicInfo,
backstageUrl: e.backstageUrl || dbModel.eventData.backstageUrl,
backstageInfo: e.backstageInfo || dbModel.eventData.backstageInfo,
endMessage: e.endMessage || dbModel.eventData.endMessage,
};
} else if (enforce) {
newEventData = { ...dbModel.eventData };
@@ -145,6 +144,7 @@ export const parseViewSettings = (data, enforce): ViewSettings => {
const viewSettings = {
overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
};
// write to db
+3 -3
View File
@@ -224,8 +224,7 @@
"publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject",
"endMessage": ""
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
},
"settings": {
"app": "ontime",
@@ -235,7 +234,8 @@
"pinCode": "1234"
},
"viewSettings": {
"overrideStyles": false
"overrideStyles": false,
"endMessage": ""
},
"osc": {
"port": 8888,
+3 -3
View File
@@ -98,8 +98,7 @@
"publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject",
"endMessage": ""
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
},
"settings": {
"app": "ontime",
@@ -110,7 +109,8 @@
"timeFormat": "24"
},
"viewSettings": {
"overrideStyles": false
"overrideStyles": false,
"endMessage": ""
},
"aliases": [
{