mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
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:
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -13,4 +13,7 @@ export const ontimeTab = {
|
||||
tablist: {
|
||||
borderBottom: '2px solid #ececec', // $gray-100
|
||||
},
|
||||
tabpanel: {
|
||||
padding: 0,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const theme = extendTheme({
|
||||
},
|
||||
variants: {
|
||||
'ontime-filled': { ...ontimeTextAreaFilled },
|
||||
'ontime-filled-onlight': { ...ontimeTextAreaFilledOnLight },
|
||||
'ontime-filled-on-light': { ...ontimeTextAreaFilledOnLight },
|
||||
},
|
||||
},
|
||||
Tooltip: {
|
||||
|
||||
Reference in New Issue
Block a user