mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 22:24:11 +00:00
refactor: small tweaks and fixes
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
[](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml)
|
[](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://ontime.gitbook.io)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
|
||||||
## Download the latest releases here
|
## Download the latest releases here
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@ outputs.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
[Read the docs to learn more](https://docs.getontime.no)
|
||||||
|
|
||||||
## Using Ontime
|
## Using Ontime
|
||||||
|
|
||||||
Once installed and running, Ontime starts a background server that is the heart of all processes.
|
Once installed and running, Ontime starts a background server that is the heart of all processes.
|
||||||
@@ -59,7 +61,7 @@ IP.ADDRESS:4001/editor > the control interface, same as the app
|
|||||||
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
||||||
```
|
```
|
||||||
|
|
||||||
More documentation is available [in our docs](https://ontime.gitbook.io)
|
More documentation is available [in our docs](https://docs.getontime.no)
|
||||||
|
|
||||||
## Feature List (in no specific order)
|
## Feature List (in no specific order)
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
|
|||||||
- WebSockets
|
- WebSockets
|
||||||
- [x] Roll mode: run standalone using the system clock
|
- [x] Roll mode: run standalone using the system clock
|
||||||
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
|
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
|
||||||
- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have
|
- [x] [Countdown to anything!](https://docs.getontime.no/features/count-to-anything/): have
|
||||||
a countdown to any scheduled event
|
a countdown to any scheduled event
|
||||||
- [x] Multi-platform (available on Windows, MacOS and Linux)
|
- [x] Multi-platform (available on Windows, MacOS and Linux)
|
||||||
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
|
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
|
||||||
@@ -120,9 +122,14 @@ Ontime broadcasts its data over WebSockets. This allows you to consume its data
|
|||||||
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language
|
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language
|
||||||
that can run in the browser).
|
that can run in the browser).
|
||||||
<br />
|
<br />
|
||||||
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
|
We have prepared a few resources to help here:
|
||||||
how to get you started and read the docs about
|
- Shipped with Ontime there is a small clock to get you started, it is available at `http://localhost:4001/external/demo` and the [code can be found here](https://github.com/cpvalente/ontime/tree/master/apps/server/src/external/demo)
|
||||||
the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-apis#osc-and-websocket-api)
|
- See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a template on
|
||||||
|
how to get you started
|
||||||
|
- See information about the [Websocket API](https://docs.getontime.no/api/osc-and-ws/)
|
||||||
|
<br />
|
||||||
|
More information [in the docs](https://docs.getontime.no/features/custom-views/)
|
||||||
|
|
||||||
|
|
||||||
### Headless run️
|
### Headless run️
|
||||||
|
|
||||||
@@ -130,7 +137,7 @@ You can self-host and run Ontime in a docker image.
|
|||||||
|
|
||||||
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
||||||
|
|
||||||
If you want to run this image in a Raspberry Pi, please see [the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
|
If you want to run this image in a Raspberry Pi, please see [the docs](https://docs.getontime.no/additional-notes/use-with-rpi/)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -192,7 +199,7 @@ Information about the project setup can be found in the [development documentati
|
|||||||
|
|
||||||
# Help
|
# Help
|
||||||
|
|
||||||
Help is underway! ... and can be found [here](https://ontime.gitbook.io)
|
Help is underway! ... and can be found [here](https://docs.getontime.no)
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|||||||
@@ -91,9 +91,6 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
|||||||
(event: KeyboardEvent<HTMLInputElement>) => {
|
(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
|
||||||
} else if (event.key === 'Tab') {
|
|
||||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
ignoreChange.current = true;
|
ignoreChange.current = true;
|
||||||
@@ -101,7 +98,7 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
|||||||
resetValue();
|
resetValue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resetValue, validateAndSubmit],
|
[resetValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onBlurHandler = useCallback(
|
const onBlurHandler = useCallback(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.emptyContainer {
|
.emptyContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $gray-1350;
|
color: $white-10;
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import EmptyImage from '../../../assets/images/empty.svg?react';
|
|||||||
import style from './Empty.module.scss';
|
import style from './Empty.module.scss';
|
||||||
|
|
||||||
interface EmptyProps {
|
interface EmptyProps {
|
||||||
text: string;
|
text?: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export default function Empty(props: EmptyProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={style.emptyContainer} {...rest}>
|
<div className={style.emptyContainer} {...rest}>
|
||||||
<EmptyImage className={style.empty} />
|
<EmptyImage className={style.empty} />
|
||||||
<span className={style.text}>{text}</span>
|
{text && <span className={style.text}>{text}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
{ value: '1h0m0s', expect: 1000 * 60 * 60 },
|
{ value: '1h0m0s', expect: 1000 * 60 * 60 },
|
||||||
{ value: '23h0m0s', expect: 1000 * 60 * 60 * 23 },
|
{ value: '23h0m0s', expect: 1000 * 60 * 60 * 23 },
|
||||||
{ value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
{ value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||||
|
{ value: '12H12M12S', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||||
{ value: '2m', expect: 2 * 60 * 1000 },
|
{ value: '2m', expect: 2 * 60 * 1000 },
|
||||||
{ value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 },
|
{ value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 },
|
||||||
{ value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 },
|
{ value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 },
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ function checkAmPm(value: string) {
|
|||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
function checkMatchers(value: string) {
|
function checkMatchers(value: string) {
|
||||||
const hoursMatch = /(\d+)h/.exec(value);
|
const hoursMatch = /(\d+)h/i.exec(value);
|
||||||
const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0;
|
const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0;
|
||||||
|
|
||||||
const minutesMatch = /(\d+)m/.exec(value);
|
const minutesMatch = /(\d+)m/i.exec(value);
|
||||||
const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0;
|
const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0;
|
||||||
|
|
||||||
const secondsMatch = /(\d+)s/.exec(value);
|
const secondsMatch = /(\d+)s/i.exec(value);
|
||||||
const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0;
|
const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0;
|
||||||
|
|
||||||
if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) {
|
if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) {
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ export const githubUrl = 'https://www.github.com/cpvalente/ontime';
|
|||||||
export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
|
export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
|
||||||
export const websiteUrl = 'https://www.getontime.no';
|
export const websiteUrl = 'https://www.getontime.no';
|
||||||
|
|
||||||
export const gitbookUrl = 'https://ontime.gitbook.io';
|
export const documentationUrl = 'https://docs.getontime.no';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { version } from '../../../../../package.json';
|
import { version } from '../../../../../package.json';
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import { gitbookUrl, githubUrl, websiteUrl } from '../../../../externals';
|
import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import CheckUpdatesButton from './CheckUpdatesButton';
|
import CheckUpdatesButton from './CheckUpdatesButton';
|
||||||
@@ -18,7 +18,7 @@ export default function AboutPanel() {
|
|||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.SubHeader>Links</Panel.SubHeader>
|
<Panel.SubHeader>Links</Panel.SubHeader>
|
||||||
<ExternalLink href={gitbookUrl}>Read the docs over at GitBook</ExternalLink>
|
<ExternalLink href={documentationUrl}>Read the docs</ExternalLink>
|
||||||
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
|
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import * as Panel from '../PanelUtils';
|
|||||||
|
|
||||||
import style from './GeneralPanel.module.scss';
|
import style from './GeneralPanel.module.scss';
|
||||||
|
|
||||||
const cssOverrideDocsUrl = 'https://ontime.gitbook.io/v2/features/custom-styling';
|
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
||||||
|
|
||||||
export default function ViewSettingsForm() {
|
export default function ViewSettingsForm() {
|
||||||
const { data, status, refetch, isFetching } = useViewSettings();
|
const { data, status, refetch, isFetching } = useViewSettings();
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import * as Panel from '../PanelUtils';
|
|||||||
import HttpIntegrations from './HttpIntegrations';
|
import HttpIntegrations from './HttpIntegrations';
|
||||||
import OscIntegrations from './OscIntegrations';
|
import OscIntegrations from './OscIntegrations';
|
||||||
|
|
||||||
const integrationDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/integrations';
|
const integrationDocsUrl = 'https://docs.getontime.no/api/integrations/';
|
||||||
|
|
||||||
export default function IntegrationsPanel() {
|
export default function IntegrationsPanel() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
<Input
|
<Input
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
size='sm'
|
size='sm'
|
||||||
placeholder='www.ontime.gitbook.io'
|
placeholder='http://docs.getontime.no'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default function ProjectData() {
|
|||||||
<Input
|
<Input
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
size='sm'
|
size='sm'
|
||||||
placeholder='www.ontime.gitbook.io'
|
placeholder='http://docs.getontime.no'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import * as Panel from '../PanelUtils';
|
|||||||
import CustomFieldEntry from './CustomFieldEntry';
|
import CustomFieldEntry from './CustomFieldEntry';
|
||||||
import CustomFieldForm from './CustomFieldForm';
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|
||||||
const customFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
|
const customFieldsDocsUrl = 'https://docs.getontime.no/features/custom-fields/';
|
||||||
|
|
||||||
export default function ProjectSettingsPanel() {
|
export default function ProjectSettingsPanel() {
|
||||||
const { data, refetch } = useCustomFields();
|
const { data, refetch } = useCustomFields();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||||
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
const googleSheetDocsUrl = 'https://ontime.gitbook.io/v2/features/google-sheet';
|
const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/';
|
||||||
|
|
||||||
export default function GSheetInfo() {
|
export default function GSheetInfo() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
grid-area: clk;
|
grid-area: clk;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 18.75rem;
|
max-width: 18.75rem;
|
||||||
|
min-width: 5em;
|
||||||
|
|
||||||
font-family: var(--font-family-override, $viewer-font-family);
|
font-family: var(--font-family-override, $viewer-font-family);
|
||||||
color: var(--timer-color-override, $timer-color);
|
color: var(--timer-color-override, $timer-color);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface TimerDisplayProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays time in ms in formatted timetag
|
* Displays time in ms in formatted timetag
|
||||||
* Typically used in production views
|
* Used in editor
|
||||||
*/
|
*/
|
||||||
export default function TimerDisplay(props: TimerDisplayProps) {
|
export default function TimerDisplay(props: TimerDisplayProps) {
|
||||||
const { time } = props;
|
const { time } = props;
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ $panel-gap: 0.5rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
flex-grow: 1;
|
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: $min-playback-width;
|
min-width: $min-playback-width;
|
||||||
max-width: $max-playback-width;
|
max-width: $max-playback-width;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import OntimeModalFooter from '../OntimeModalFooter';
|
|||||||
|
|
||||||
import style from './SettingsModal.module.scss';
|
import style from './SettingsModal.module.scss';
|
||||||
|
|
||||||
const aliasesDocsUrl = 'https://ontime.gitbook.io/v2/features/url-aliases';
|
const aliasesDocsUrl = 'https://docs.getontime.no/features/url-presets/';
|
||||||
|
|
||||||
// we wrap the array in an object to be simplify react-hook-form
|
// we wrap the array in an object to be simplify react-hook-form
|
||||||
type Aliases = {
|
type Aliases = {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function ProjectDataForm() {
|
|||||||
{...inputProps}
|
{...inputProps}
|
||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled-on-light'
|
||||||
size='sm'
|
size='sm'
|
||||||
placeholder='www.ontime.gitbook.io'
|
placeholder='https://docs.getontime.no'
|
||||||
isDisabled={disableInputs}
|
isDisabled={disableInputs}
|
||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import style from './Overview.module.scss';
|
|||||||
* @param time
|
* @param time
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function formattedTime(time: MaybeNumber) {
|
function formatedTime(time: MaybeNumber) {
|
||||||
return millisToString(time, { fallback: timerPlaceholder });
|
return millisToString(time, { fallback: timerPlaceholder });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@ export default function Overview() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TitlesOverview />
|
<TitlesOverview />
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<TimeRow label='Planned start' value={formattedTime(plannedStart)} className={style.start} />
|
<TimeRow label='Planned start' value={formatedTime(plannedStart)} className={style.start} />
|
||||||
<TimeRow label='Actual start' value={formattedTime(actualStart)} className={style.start} />
|
<TimeRow label='Actual start' value={formatedTime(actualStart)} className={style.start} />
|
||||||
</div>
|
</div>
|
||||||
<RuntimeOverview />
|
<RuntimeOverview />
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<TimeRow label='Planned end' value={formattedTime(plannedEnd)} className={style.end} />
|
<TimeRow label='Planned end' value={formatedTime(plannedEnd)} className={style.end} />
|
||||||
<TimeRow label='Expected end' value={formattedTime(expectedEnd)} className={style.end} />
|
<TimeRow label='Expected end' value={formatedTime(expectedEnd)} className={style.end} />
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,24 +51,32 @@ function TitlesOverview() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOffsetText(offset: MaybeNumber): string {
|
||||||
|
if (offset === null) {
|
||||||
|
return enDash;
|
||||||
|
}
|
||||||
|
const isAhead = offset <= 0;
|
||||||
|
let offsetText = millisToString(Math.abs(offset), { fallback: enDash });
|
||||||
|
if (offsetText !== enDash) {
|
||||||
|
offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`;
|
||||||
|
}
|
||||||
|
return offsetText;
|
||||||
|
}
|
||||||
|
|
||||||
function RuntimeOverview() {
|
function RuntimeOverview() {
|
||||||
const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview();
|
const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview();
|
||||||
|
|
||||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||||
const ofTotal = numEvents || enDash;
|
const ofTotal = numEvents || enDash;
|
||||||
const progressText = numEvents ? `${current} of ${ofTotal}` : '-';
|
const progressText = numEvents ? `${current} of ${ofTotal}` : '-';
|
||||||
|
const offsetText = getOffsetText(offset);
|
||||||
const isAhead = offset <= 0;
|
const offsetClasses = offset === null ? undefined : offset > 0 ? style.behind : style.ahead;
|
||||||
let offsetText = millisToString(Math.abs(offset), { fallback: enDash });
|
|
||||||
if (offsetText !== enDash) {
|
|
||||||
offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimeColumn label='Progress' value={progressText} />
|
<TimeColumn label='Progress' value={progressText} />
|
||||||
<TimeColumn label='Offset' value={offsetText} className={isAhead ? style.ahead : style.behind} />
|
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} />
|
||||||
<TimeColumn label='Time now' value={formattedTime(clock)} />
|
<TimeColumn label='Time now' value={formatedTime(clock)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
data={event}
|
data={event}
|
||||||
loaded={isLoaded}
|
loaded={isLoaded}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
next={isNext}
|
isNext={isNext}
|
||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
previousEventId={previousEventId}
|
previousEventId={previousEventId}
|
||||||
playback={isLoaded ? featureData.playback : undefined}
|
playback={isLoaded ? featureData.playback : undefined}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function RundownEmpty(props: RundownEmptyProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.alignCenter}>
|
<div className={style.alignCenter}>
|
||||||
<Empty text='No data yet' style={{ marginTop: '7vh' }} />
|
<Empty style={{ marginTop: '7vh' }} />
|
||||||
<Button onClick={handleAddNew} variant='ontime-filled' className={style.spaceTop} leftIcon={<IoAdd />}>
|
<Button onClick={handleAddNew} variant='ontime-filled' className={style.spaceTop} leftIcon={<IoAdd />}>
|
||||||
Create Event
|
Create Event
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface RundownEntryProps {
|
|||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
next: boolean;
|
isNext: boolean;
|
||||||
previousEnd: MaybeNumber;
|
previousEnd: MaybeNumber;
|
||||||
previousEventId?: string;
|
previousEventId?: string;
|
||||||
playback?: Playback; // we only care about this if this event is playing
|
playback?: Playback; // we only care about this if this event is playing
|
||||||
@@ -30,7 +30,7 @@ interface RundownEntryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownEntry(props: RundownEntryProps) {
|
export default function RundownEntry(props: RundownEntryProps) {
|
||||||
const { isPast, data, loaded, hasCursor, next, previousEnd, previousEventId, playback, isRolling, eventIndex } =
|
const { isPast, data, loaded, hasCursor, isNext, previousEnd, previousEventId, playback, isRolling, eventIndex } =
|
||||||
props;
|
props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
||||||
@@ -138,7 +138,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
previousEnd={previousEnd}
|
previousEnd={previousEnd}
|
||||||
colour={data.colour}
|
colour={data.colour}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
next={next}
|
isNext={isNext}
|
||||||
skip={data.skip}
|
skip={data.skip}
|
||||||
loaded={loaded}
|
loaded={loaded}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@use '../editors//EditorMixin' as editor;
|
@use '../editors/EditorMixin' as editor;
|
||||||
|
|
||||||
.rundownExport {
|
.rundownExport {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
|
||||||
|
|
||||||
&.extracted {
|
&.extracted {
|
||||||
.list {
|
.list {
|
||||||
@@ -34,8 +34,7 @@
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
box-shadow: $box-shadow-right;
|
box-shadow: $box-shadow-right;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 38rem;
|
min-width: 38rem;
|
||||||
max-width: 45rem;
|
max-width: 45rem;
|
||||||
}
|
}
|
||||||
@@ -49,8 +48,7 @@
|
|||||||
background-color: $gray-1325;
|
background-color: $gray-1325;
|
||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 30rem;
|
min-width: 30rem;
|
||||||
max-width: 45rem;
|
max-width: 45rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
@@ -15,39 +16,40 @@ interface TitleEditorProps {
|
|||||||
|
|
||||||
export default function EditableBlockTitle(props: TitleEditorProps) {
|
export default function EditableBlockTitle(props: TitleEditorProps) {
|
||||||
const { title, eventId, placeholder, className } = props;
|
const { title, eventId, placeholder, className } = props;
|
||||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
|
|
||||||
useEffect(() => {
|
const submitCallback = useCallback(
|
||||||
setBlockTitle(title);
|
|
||||||
}, [title]);
|
|
||||||
|
|
||||||
const handleTitle = useCallback(
|
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
if (text === title) {
|
if (text === title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanVal = text.trim();
|
const cleanVal = text.trim();
|
||||||
setBlockTitle(cleanVal);
|
|
||||||
|
|
||||||
updateEvent({ id: eventId, title: cleanVal });
|
updateEvent({ id: eventId, title: cleanVal });
|
||||||
},
|
},
|
||||||
[title, updateEvent, eventId],
|
[title, updateEvent, eventId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const classes = cx([className, style.eventTitle, !blockTitle ? style.noTitle : null]);
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(title, submitCallback, {
|
||||||
|
submitOnEnter: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = cx([className, style.eventTitle, !value ? style.noTitle : null]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editable
|
<Input
|
||||||
variant='ontime'
|
data-testid='block__title'
|
||||||
value={blockTitle}
|
variant='ontime-ghosted'
|
||||||
|
value={value}
|
||||||
className={classes}
|
className={classes}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(value) => setBlockTitle(value)}
|
onChange={onChange}
|
||||||
onSubmit={(value) => handleTitle(value)}
|
onBlur={onBlur}
|
||||||
>
|
onKeyDown={onKeyDown}
|
||||||
<EditablePreview className={style.preview} />
|
autoComplete='off'
|
||||||
<EditableInput />
|
fontWeight='600'
|
||||||
</Editable>
|
letterSpacing='0.25px'
|
||||||
|
paddingLeft='0'
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $skip-opacity: 0.1;
|
|||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'binder ... ... ...'
|
'binder ... ... ...'
|
||||||
'binder pb-actions times actions'
|
'binder pb-actions times actions'
|
||||||
'binder pb-actions title next'
|
'binder pb-actions title title'
|
||||||
'binder pb-actions estatus estatus'
|
'binder pb-actions estatus estatus'
|
||||||
'binder ... ... ...';
|
'binder ... ... ...';
|
||||||
|
|
||||||
@@ -127,15 +127,30 @@ $skip-opacity: 0.1;
|
|||||||
.eventTimers {
|
.eventTimers {
|
||||||
grid-area: times;
|
grid-area: times;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: $block-clearance;
|
gap: $block-clearance;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventTitle {
|
.titleSection {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
max-height: calc(2.5em + 2px);
|
align-items: center;
|
||||||
line-height: 1.25em;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.nextTag {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $orange-500;
|
||||||
|
letter-spacing: 0.03px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventTitle {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: calc(2.5em + 2px);
|
||||||
|
line-height: 1.25em;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventActions {
|
.eventActions {
|
||||||
@@ -146,10 +161,11 @@ $skip-opacity: 0.1;
|
|||||||
|
|
||||||
.progressBg {
|
.progressBg {
|
||||||
grid-area: progb;
|
grid-area: progb;
|
||||||
border-radius: 2px;
|
border-radius: 1px;
|
||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden; /* clip foreground border radius*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBg.hidden {
|
.progressBg.hidden {
|
||||||
@@ -184,15 +200,6 @@ $skip-opacity: 0.1;
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nextTag {
|
|
||||||
grid-area: next;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $orange-500;
|
|
||||||
letter-spacing: 0.03px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventStatus {
|
.eventStatus {
|
||||||
grid-area: status;
|
grid-area: status;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ interface EventBlockProps {
|
|||||||
previousEnd: MaybeNumber;
|
previousEnd: MaybeNumber;
|
||||||
colour: string;
|
colour: string;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
next: boolean;
|
isNext: boolean;
|
||||||
skip: boolean;
|
skip: boolean;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
@@ -76,7 +76,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
previousEnd,
|
previousEnd,
|
||||||
colour,
|
colour,
|
||||||
isPast,
|
isPast,
|
||||||
next,
|
isNext,
|
||||||
skip = false,
|
skip = false,
|
||||||
loaded,
|
loaded,
|
||||||
hasCursor,
|
hasCursor,
|
||||||
@@ -240,7 +240,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
className={blockClasses}
|
className={blockClasses}
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={dragStyle}
|
style={dragStyle}
|
||||||
onMouseDown={handleFocusClick}
|
onClick={handleFocusClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
id='event-block'
|
id='event-block'
|
||||||
>
|
>
|
||||||
@@ -268,7 +268,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
title={title}
|
title={title}
|
||||||
note={note}
|
note={note}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
next={next}
|
isNext={isNext}
|
||||||
skip={skip}
|
skip={skip}
|
||||||
loaded={loaded}
|
loaded={loaded}
|
||||||
playback={playback}
|
playback={playback}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ interface EventBlockInnerProps {
|
|||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
next: boolean;
|
isNext: boolean;
|
||||||
skip: boolean;
|
skip: boolean;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
playback?: Playback;
|
playback?: Playback;
|
||||||
@@ -63,7 +63,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
title,
|
title,
|
||||||
note,
|
note,
|
||||||
delay,
|
delay,
|
||||||
next,
|
isNext,
|
||||||
skip = false,
|
skip = false,
|
||||||
loaded,
|
loaded,
|
||||||
playback,
|
playback,
|
||||||
@@ -100,12 +100,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
linkStart={linkStart}
|
linkStart={linkStart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
<div className={style.titleSection}>
|
||||||
{next && (
|
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||||
<Tooltip label='Next event' {...tooltipProps}>
|
{isNext && (
|
||||||
<span className={style.nextTag}>UP NEXT</span>
|
<Tooltip label='Next event' {...tooltipProps}>
|
||||||
</Tooltip>
|
<span className={style.nextTag}>UP NEXT</span>
|
||||||
)}
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<EventBlockPlayback
|
<EventBlockPlayback
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
skip={skip}
|
skip={skip}
|
||||||
@@ -117,7 +119,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
|||||||
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
||||||
<span className={style.eventNote}>{note}</span>
|
<span className={style.eventNote}>{note}</span>
|
||||||
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
||||||
{loaded && <EventBlockProgressBar playback={playback} />}
|
{loaded && <EventBlockProgressBar />}
|
||||||
</div>
|
</div>
|
||||||
<div className={style.eventStatus} tabIndex={-1}>
|
<div className={style.eventStatus} tabIndex={-1}>
|
||||||
<Tooltip label={`Time type: ${timerType}`} {...tooltipProps}>
|
<Tooltip label={`Time type: ${timerType}`} {...tooltipProps}>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ $gap-left: calc(2rem + 0.25rem + 3rem);
|
|||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin indicator($bg-colour) {
|
@mixin indicator($bg-colour) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo } from 'react';
|
import { memo, MouseEvent } from 'react';
|
||||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
||||||
@@ -40,11 +40,13 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
|
|||||||
const { eventId, skip, isPlaying, isPaused, loaded, disablePlayback } = props;
|
const { eventId, skip, isPlaying, isPaused, loaded, disablePlayback } = props;
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
|
|
||||||
const toggleSkip = () => {
|
const toggleSkip = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
updateEvent({ id: eventId, skip: !skip });
|
updateEvent({ id: eventId, skip: !skip });
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionHandler = () => {
|
const actionHandler = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
// is playing -> pause
|
// is playing -> pause
|
||||||
// is paused -> continue
|
// is paused -> continue
|
||||||
// otherwise -> start
|
// otherwise -> start
|
||||||
@@ -57,6 +59,11 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const load = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setEventPlayback.loadEvent(eventId);
|
||||||
|
};
|
||||||
|
|
||||||
const buttonVariant: Partial<StyleVariant> = {};
|
const buttonVariant: Partial<StyleVariant> = {};
|
||||||
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
@@ -103,7 +110,7 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
|
|||||||
isDisabled={disablePlayback}
|
isDisabled={disablePlayback}
|
||||||
{...tooltipProps}
|
{...tooltipProps}
|
||||||
{...blockBtnStyle}
|
{...blockBtnStyle}
|
||||||
clickHandler={() => setEventPlayback.loadEvent(eventId)}
|
clickHandler={load}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
|
|||||||
+1
-16
@@ -5,20 +5,5 @@
|
|||||||
|
|
||||||
transition: 1s linear;
|
transition: 1s linear;
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
|
background-color: $gray-200;
|
||||||
&.play {
|
|
||||||
background-color: $playback-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.overtime {
|
|
||||||
background-color: $playback-negative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pause {
|
|
||||||
background-color: $ontime-paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.roll {
|
|
||||||
background-color: $ontime-roll;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { MaybeNumber, Playback } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
import { useTimer } from '../../../../common/hooks/useSocket';
|
import { useTimer } from '../../../../common/hooks/useSocket';
|
||||||
import { clamp } from '../../../../common/utils/math';
|
import { clamp } from '../../../../common/utils/math';
|
||||||
|
|
||||||
import style from './EventBlockProgressBar.module.scss';
|
import style from './EventBlockProgressBar.module.scss';
|
||||||
|
|
||||||
interface EventBlockProgressBarProps {
|
|
||||||
playback?: Playback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber): number {
|
export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber): number {
|
||||||
if (remaining === null || total === null) {
|
if (remaining === null || total === null) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -25,10 +21,9 @@ export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber):
|
|||||||
return clamp(100 - (remaining * 100) / total, 0, 100);
|
return clamp(100 - (remaining * 100) / total, 0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
|
export default function EventBlockProgressBar() {
|
||||||
const { playback } = props;
|
|
||||||
const timer = useTimer();
|
const timer = useTimer();
|
||||||
|
|
||||||
const progress = `${getPercentComplete(timer.current, timer.duration)}%`;
|
const progress = `${getPercentComplete(timer.current, timer.duration)}%`;
|
||||||
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
|
return <div className={style.progressBar} style={{ width: progress }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
max-width: max-content;
|
max-width: max-content;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<span className={style.inputLabel}>Event Visibility</span>
|
<span className={style.inputLabel}>Event Visibility</span>
|
||||||
<label className={style.switchLabel}>
|
<label className={style.switchLabel}>
|
||||||
<Switch size='sm' isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
|
<Switch size='md' isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
|
||||||
{isPublic ? 'Public' : 'Private'}
|
{isPublic ? 'Public' : 'Private'}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,15 +22,16 @@ export default function RundownHeader() {
|
|||||||
<div className={style.header}>
|
<div className={style.header}>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
variant={appMode === AppMode.Freeze ? 'ontime-filled' : 'ontime-ghosted'}
|
variant={appMode === AppMode.Freeze ? 'ontime-filled' : 'ontime-outlined'}
|
||||||
size='sm'
|
size='sm'
|
||||||
icon={<IoSnowOutline />}
|
icon={<IoSnowOutline />}
|
||||||
clickHandler={setFreezeMode}
|
clickHandler={setFreezeMode}
|
||||||
tooltip='Freeze rundown'
|
tooltip='Freeze rundown'
|
||||||
aria-label='Freeze rundown'
|
aria-label='Freeze rundown'
|
||||||
|
isDisabled
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
variant={appMode === AppMode.Run ? 'ontime-filled' : 'ontime-ghosted'}
|
variant={appMode === AppMode.Run ? 'ontime-filled' : 'ontime-outlined'}
|
||||||
size='sm'
|
size='sm'
|
||||||
icon={<IoPlay />}
|
icon={<IoPlay />}
|
||||||
clickHandler={setRunMode}
|
clickHandler={setRunMode}
|
||||||
@@ -38,7 +39,7 @@ export default function RundownHeader() {
|
|||||||
aria-label='Run mode'
|
aria-label='Run mode'
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
variant={appMode === AppMode.Edit ? 'ontime-filled' : 'ontime-ghosted'}
|
variant={appMode === AppMode.Edit ? 'ontime-filled' : 'ontime-outlined'}
|
||||||
size='sm'
|
size='sm'
|
||||||
icon={<IoOptions />}
|
icon={<IoOptions />}
|
||||||
clickHandler={setEditMode}
|
clickHandler={setEditMode}
|
||||||
@@ -47,7 +48,7 @@ export default function RundownHeader() {
|
|||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<RundownMenu>
|
<RundownMenu>
|
||||||
<MenuButton size='sm' as={Button} rightIcon={<IoAdd />} aria-label='Rundown menu' variant='ontime-ghosted'>
|
<MenuButton size='sm' as={Button} rightIcon={<IoAdd />} aria-label='Rundown menu' variant='ontime-outlined'>
|
||||||
Rundown
|
Rundown
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</RundownMenu>
|
</RundownMenu>
|
||||||
|
|||||||
@@ -26,4 +26,5 @@
|
|||||||
color: $blue-500;
|
color: $blue-500;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,19 @@
|
|||||||
transition: $viewer-transition-time;
|
transition: $viewer-transition-time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blackout {
|
||||||
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #000;
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity $viewer-transition-time;
|
||||||
|
&--active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* =================== CLOCK ===================*/
|
/* =================== CLOCK ===================*/
|
||||||
|
|
||||||
.clock-container {
|
.clock-container {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
}
|
}
|
||||||
const stageTimerCharacters = display.replace('/:/g', '').length;
|
const stageTimerCharacters = display.replace('/:/g', '').length;
|
||||||
|
|
||||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`;
|
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
||||||
let timerFontSize = 89 / (stageTimerCharacters - 1);
|
let timerFontSize = 89 / (stageTimerCharacters - 1);
|
||||||
// we need to shrink the timer if the external is going to be there
|
// we need to shrink the timer if the external is going to be there
|
||||||
if (showExternal) {
|
if (showExternal) {
|
||||||
@@ -162,6 +162,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={timerOptions} />
|
<ViewParamsEditor paramFields={timerOptions} />
|
||||||
|
<div className={showBlackout ? 'blackout blackout--active' : 'blackout'} />
|
||||||
{!userOptions.hideMessage && (
|
{!userOptions.hideMessage && (
|
||||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||||
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
||||||
|
|||||||
@@ -33,8 +33,19 @@ html,
|
|||||||
background-color: $bg-container-l1;
|
background-color: $bg-container-l1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// workaround for chakra
|
/* smaller root size for MacOS laptops */
|
||||||
// // https://github.com/chakra-ui/chakra-ui/issues/417
|
@media (max-width: 1600px) {
|
||||||
|
body,
|
||||||
|
html,
|
||||||
|
.App {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* workaround for chakra
|
||||||
|
* https://github.com/chakra-ui/chakra-ui/issues/417
|
||||||
|
*/
|
||||||
option {
|
option {
|
||||||
color: initial;
|
color: initial;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
.mirror {
|
.mirror {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blackout {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ const commonStyles = {
|
|||||||
backgroundColor: '#262626', // $gray-1200
|
backgroundColor: '#262626', // $gray-1200
|
||||||
color: '#e2e2e2', // $gray-200
|
color: '#e2e2e2', // $gray-200
|
||||||
border: '1px solid transparent',
|
border: '1px solid transparent',
|
||||||
|
borderRadius: '3px',
|
||||||
_hover: {
|
_hover: {
|
||||||
backgroundColor: '#2d2d2d', // $gray-1100
|
backgroundColor: '#2d2d2d', // $gray-1100
|
||||||
},
|
},
|
||||||
@@ -25,6 +26,18 @@ export const ontimeInputFilled = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ontimeInputGhosted = {
|
||||||
|
field: {
|
||||||
|
...commonStyles,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: '#f6f6f6', // $gray-50
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'transparent', // $gray-1100
|
||||||
|
border: '1px solid #2B5ABC', // $blue-500
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const ontimeInputFilledOnLight = {
|
export const ontimeInputFilledOnLight = {
|
||||||
field: {
|
field: {
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { ontimeTab } from './ontimeTab';
|
|||||||
import {
|
import {
|
||||||
ontimeInputFilled,
|
ontimeInputFilled,
|
||||||
ontimeInputFilledOnLight,
|
ontimeInputFilledOnLight,
|
||||||
|
ontimeInputGhosted,
|
||||||
ontimeTextAreaFilled,
|
ontimeTextAreaFilled,
|
||||||
ontimeTextAreaFilledOnLight,
|
ontimeTextAreaFilledOnLight,
|
||||||
ontimeTextAreaTransparent,
|
ontimeTextAreaTransparent,
|
||||||
@@ -71,6 +72,7 @@ const theme = extendTheme({
|
|||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
'ontime-filled': { ...ontimeInputFilled },
|
'ontime-filled': { ...ontimeInputFilled },
|
||||||
|
'ontime-ghosted': { ...ontimeInputGhosted },
|
||||||
'ontime-filled-on-light': { ...ontimeInputFilledOnLight },
|
'ontime-filled-on-light': { ...ontimeInputFilledOnLight },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime-prerelease",
|
||||||
"version": "3.0.0-alpha",
|
"version": "3.0.0-alpha",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"productName": "ontime",
|
"productName": "ontime-prerelease",
|
||||||
"appId": "no.lightdev.ontime",
|
"appId": "no.lightdev.ontime.prerelease",
|
||||||
"asar": true,
|
"asar": true,
|
||||||
"dmg": {
|
"dmg": {
|
||||||
"artifactName": "ontime-macOS-${arch}.dmg",
|
"artifactName": "ontime-macOS-${arch}.dmg",
|
||||||
|
|||||||
@@ -121,6 +121,12 @@ function getApplicationMenu(isMac, askToQuit) {
|
|||||||
await shell.openExternal('http://localhost:4001/cuesheet');
|
await shell.openExternal('http://localhost:4001/cuesheet');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Operator',
|
||||||
|
click: async () => {
|
||||||
|
await shell.openExternal('http://localhost:4001/operator');
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -153,7 +159,7 @@ function getApplicationMenu(isMac, askToQuit) {
|
|||||||
{
|
{
|
||||||
label: 'Online documentation',
|
label: 'Online documentation',
|
||||||
click: async () => {
|
click: async () => {
|
||||||
await shell.openExternal('https://ontime.gitbook.io/');
|
await shell.openExternal('https://docs.getontime.no/');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -85,13 +85,13 @@ const actionHandlers: Record<string, ActionHandler> = {
|
|||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
}
|
}
|
||||||
if ('id' in payload) {
|
if ('id' in payload) {
|
||||||
assert.isString(payload);
|
assert.isString(payload.id);
|
||||||
runtimeService.startById(payload);
|
runtimeService.startById(payload.id);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
}
|
}
|
||||||
if ('cue' in payload) {
|
if ('cue' in payload) {
|
||||||
assert.isString(payload);
|
assert.isString(payload.cue);
|
||||||
runtimeService.startByCue(payload);
|
runtimeService.startByCue(payload.cue);
|
||||||
return { payload: 'success' };
|
return { payload: 'success' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -48,7 +48,7 @@ const connectSocket = () => {
|
|||||||
// we only need to read message type of ontime
|
// we only need to read message type of ontime
|
||||||
if (type === 'ontime') {
|
if (type === 'ontime') {
|
||||||
// destructure known data from ontime
|
// destructure known data from ontime
|
||||||
// see https://cpvalente.gitbook.io/ontime/control-and-feedback/websocket-api
|
// see https://docs.getontime.no/api/osc-and-ws/
|
||||||
const { timer, playback } = payload;
|
const { timer, playback } = payload;
|
||||||
const timerElement = document.getElementById('timer');
|
const timerElement = document.getElementById('timer');
|
||||||
if (playback == 'stop') {
|
if (playback == 'stop') {
|
||||||
|
|||||||
+1
-1
@@ -10,5 +10,5 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="timer"></div>
|
<div id="timer"></div>
|
||||||
<script src="./app.js" type="module"></script>
|
<script src="./app.js" type="text/javascript"></script>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
import { initAssets, startIntegrations, startOSCServer, startServer } from './app.js';
|
import { initAssets, startIntegrations, startOSCServer, startServer } from './app.js';
|
||||||
|
|
||||||
async function startOntime() {
|
async function startOntime() {
|
||||||
|
|||||||
@@ -142,17 +142,18 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
const isTimeToUpdate = state.clock - TimerService.previousUpdate >= TimerService._updateInterval;
|
const isTimeToUpdate = state.clock - TimerService.previousUpdate >= TimerService._updateInterval;
|
||||||
|
|
||||||
// some changes need an immediate update
|
// some changes need an immediate update
|
||||||
|
const hasNewLoaded = state.eventNow?.id !== TimerService.previousState?.eventNow?.id;
|
||||||
const hasSkippedBack = state.clock < TimerService.previousUpdate;
|
const hasSkippedBack = state.clock < TimerService.previousUpdate;
|
||||||
const justStarted = !TimerService.previousState?.timer;
|
const justStarted = !TimerService.previousState?.timer;
|
||||||
const hasChangedPlayback = TimerService.previousState.timer?.playback !== state.timer.playback;
|
const hasChangedPlayback = TimerService.previousState.timer?.playback !== state.timer.playback;
|
||||||
const hasImmediateChanges = hasSkippedBack || justStarted || hasChangedPlayback;
|
const hasImmediateChanges = hasNewLoaded || hasSkippedBack || justStarted || hasChangedPlayback;
|
||||||
|
|
||||||
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) {
|
if (hasImmediateChanges || (isTimeToUpdate && !deepEqual(TimerService.previousState?.timer, state.timer))) {
|
||||||
eventStore.set('timer', state.timer);
|
eventStore.set('timer', state.timer);
|
||||||
TimerService.previousState.timer = { ...state.timer };
|
TimerService.previousState.timer = { ...state.timer };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTimeToUpdate && !deepEqual(TimerService.previousState?.runtime, state.runtime)) {
|
if (hasChangedPlayback || (isTimeToUpdate && !deepEqual(TimerService.previousState?.runtime, state.runtime))) {
|
||||||
eventStore.set('runtime', state.runtime);
|
eventStore.set('runtime', state.runtime);
|
||||||
TimerService.previousState.runtime = { ...state.runtime };
|
TimerService.previousState.runtime = { ...state.runtime };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1387,6 +1387,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
|
runtime: {
|
||||||
|
actualStart: 150,
|
||||||
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
@@ -1408,6 +1411,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
|
runtime: {
|
||||||
|
actualStart: 100,
|
||||||
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
@@ -1430,9 +1436,116 @@ describe('getRuntimeOffset()', () => {
|
|||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: 125,
|
pausedAt: 125,
|
||||||
},
|
},
|
||||||
|
runtime: {
|
||||||
|
actualStart: 100,
|
||||||
|
},
|
||||||
} as RuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const offset = getRuntimeOffset(state);
|
const offset = getRuntimeOffset(state);
|
||||||
expect(offset).toBe(25);
|
expect(offset).toBe(25);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can only count once started', () => {
|
||||||
|
const state = {
|
||||||
|
clock: 78480789,
|
||||||
|
eventNow: {
|
||||||
|
id: 'd6a2ce',
|
||||||
|
type: 'event',
|
||||||
|
title: '',
|
||||||
|
timeStart: 77400000,
|
||||||
|
timeEnd: 81000000,
|
||||||
|
duration: 3600000,
|
||||||
|
timeStrategy: 'lock-duration',
|
||||||
|
linkStart: null,
|
||||||
|
endAction: 'none',
|
||||||
|
timerType: 'count-down',
|
||||||
|
isPublic: true,
|
||||||
|
skip: false,
|
||||||
|
note: '',
|
||||||
|
colour: '',
|
||||||
|
cue: '1',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 120000,
|
||||||
|
timeDanger: 60000,
|
||||||
|
custom: {},
|
||||||
|
delay: 0,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
selectedEventIndex: 0,
|
||||||
|
numEvents: 2,
|
||||||
|
offset: -77400000,
|
||||||
|
plannedStart: 77400000,
|
||||||
|
plannedEnd: 84600000,
|
||||||
|
actualStart: null,
|
||||||
|
expectedEnd: null,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
addedTime: 0,
|
||||||
|
current: 3600000,
|
||||||
|
duration: 3600000,
|
||||||
|
elapsed: null,
|
||||||
|
expectedFinish: null,
|
||||||
|
finishedAt: null,
|
||||||
|
playback: 'armed',
|
||||||
|
secondaryTimer: null,
|
||||||
|
startedAt: null,
|
||||||
|
},
|
||||||
|
_timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
|
||||||
|
} as RuntimeState;
|
||||||
|
|
||||||
|
const offset = getRuntimeOffset(state);
|
||||||
|
expect(offset).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles loaded event', () => {
|
||||||
|
const state = {
|
||||||
|
clock: 79521653,
|
||||||
|
eventNow: {
|
||||||
|
id: '835242',
|
||||||
|
type: 'event',
|
||||||
|
title: '',
|
||||||
|
timeStart: 81000000,
|
||||||
|
timeEnd: 84600000,
|
||||||
|
duration: 3600000,
|
||||||
|
timeStrategy: 'lock-duration',
|
||||||
|
linkStart: null,
|
||||||
|
endAction: 'none',
|
||||||
|
timerType: 'count-down',
|
||||||
|
isPublic: true,
|
||||||
|
skip: false,
|
||||||
|
note: '',
|
||||||
|
colour: '',
|
||||||
|
cue: '2',
|
||||||
|
revision: 0,
|
||||||
|
timeWarning: 120000,
|
||||||
|
timeDanger: 60000,
|
||||||
|
custom: {},
|
||||||
|
delay: 0,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
selectedEventIndex: 1,
|
||||||
|
numEvents: 2,
|
||||||
|
offset: -81000000,
|
||||||
|
plannedStart: 77400000,
|
||||||
|
plannedEnd: 84600000,
|
||||||
|
actualStart: 79443403,
|
||||||
|
expectedEnd: null,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
addedTime: 0,
|
||||||
|
current: 3600000,
|
||||||
|
duration: 3600000,
|
||||||
|
elapsed: null,
|
||||||
|
expectedFinish: null,
|
||||||
|
finishedAt: null,
|
||||||
|
playback: 'armed',
|
||||||
|
secondaryTimer: null,
|
||||||
|
startedAt: null,
|
||||||
|
},
|
||||||
|
_timer: { pausedAt: null, secondaryTarget: null, finishedNow: false },
|
||||||
|
} as RuntimeState;
|
||||||
|
|
||||||
|
const offset = getRuntimeOffset(state);
|
||||||
|
expect(offset).toBe(79521653 - 81000000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { appStateService } from '../app-state-service/AppStateService.js';
|
|||||||
import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
|
import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
|
||||||
import { dbModel } from '../../models/dataModel.js';
|
import { dbModel } from '../../models/dataModel.js';
|
||||||
import { deleteFile } from '../../utils/parserUtils.js';
|
import { deleteFile } from '../../utils/parserUtils.js';
|
||||||
|
import { switchDb } from '../../setup/loadDb.js';
|
||||||
|
|
||||||
// init dependencies
|
// init dependencies
|
||||||
init();
|
init();
|
||||||
@@ -40,6 +41,9 @@ export async function applyProjectFile(filePath: string, options?: Options) {
|
|||||||
const newFilePath = join(resolveProjectsDirectory, filename);
|
const newFilePath = join(resolveProjectsDirectory, filename);
|
||||||
await rename(filePath, newFilePath);
|
await rename(filePath, newFilePath);
|
||||||
|
|
||||||
|
// change LowDB to point to new file
|
||||||
|
await switchDb(filename);
|
||||||
|
|
||||||
// apply data model
|
// apply data model
|
||||||
await applyDataModel(data, options);
|
await applyDataModel(data, options);
|
||||||
|
|
||||||
@@ -135,6 +139,9 @@ export async function createProjectFile(filename: string, projectData: ProjectDa
|
|||||||
const newFile = join(resolveProjectsDirectory, filename);
|
const newFile = join(resolveProjectsDirectory, filename);
|
||||||
await writeFile(newFile, JSON.stringify(data));
|
await writeFile(newFile, JSON.stringify(data));
|
||||||
|
|
||||||
|
// change LowDB to point to new file
|
||||||
|
await switchDb(filename);
|
||||||
|
|
||||||
// apply its data
|
// apply its data
|
||||||
await applyDataModel(data);
|
await applyDataModel(data);
|
||||||
|
|
||||||
|
|||||||
@@ -191,10 +191,7 @@ class RuntimeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timedEvents = getPlayableEvents();
|
const timedEvents = getPlayableEvents();
|
||||||
const state = runtimeState.getState();
|
const success = runtimeState.load(event, timedEvents);
|
||||||
// TODO: return success boolean from runtimeState, when we work with optimising integrations
|
|
||||||
runtimeState.load(event, timedEvents);
|
|
||||||
const success = event.id === state.eventNow?.id;
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
integrationService.dispatch(TimerLifeCycle.onLoad);
|
integrationService.dispatch(TimerLifeCycle.onLoad);
|
||||||
|
|||||||
@@ -292,20 +292,27 @@ export const updateRoll = (state: RuntimeState) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates difference between the runtime and the schedule of an event
|
* Calculates difference between the runtime and the schedule of an event
|
||||||
|
* Positive offset is a delay
|
||||||
|
* Negative offset is time ahead
|
||||||
* @param state
|
* @param state
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getRuntimeOffset(state: RuntimeState): number {
|
export function getRuntimeOffset(state: RuntimeState): MaybeNumber {
|
||||||
if (state.eventNow === null) {
|
if (state.runtime.actualStart === null) {
|
||||||
return 0;
|
return null;
|
||||||
}
|
}
|
||||||
|
const { clock } = state;
|
||||||
const { timeStart } = state.eventNow;
|
const { timeStart } = state.eventNow;
|
||||||
const { addedTime, current, startedAt } = state.timer;
|
const { addedTime, current, startedAt } = state.timer;
|
||||||
|
|
||||||
|
// if we havent started, the offset is the difference to the schedule
|
||||||
|
if (startedAt === null) {
|
||||||
|
return clock - timeStart;
|
||||||
|
}
|
||||||
|
|
||||||
const overtime = Math.min(current, 0);
|
const overtime = Math.min(current, 0);
|
||||||
const startOffset = startedAt - timeStart;
|
const startOffset = startedAt - timeStart;
|
||||||
const pausedTime = state._timer.pausedAt === null ? 0 : state.clock - state._timer.pausedAt;
|
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
||||||
|
|
||||||
return startOffset + addedTime + pausedTime + Math.abs(overtime);
|
return startOffset + addedTime + pausedTime + Math.abs(overtime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ const lastLoadedProject = isTest ? 'db.json' : getLastLoadedProject();
|
|||||||
|
|
||||||
// path to public db
|
// path to public db
|
||||||
export const resolveDbDirectory = join(testDbStartDirectory, isTest ? `../${config.database.testdb}` : config.projects);
|
export const resolveDbDirectory = join(testDbStartDirectory, isTest ? `../${config.database.testdb}` : config.projects);
|
||||||
export const resolveDbPath = join(resolveDbDirectory, lastLoadedProject ? lastLoadedProject : config.database.filename);
|
export const resolveDbName = lastLoadedProject ? lastLoadedProject : config.database.filename;
|
||||||
|
export const resolveDbPath = join(resolveDbDirectory, resolveDbName);
|
||||||
|
|
||||||
export const pathToStartDb = isTest
|
export const pathToStartDb = isTest
|
||||||
? join(srcDirectory, '..', config.database.testdb, config.database.filename)
|
? join(srcDirectory, '..', config.database.testdb, config.database.filename)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { join } from 'path';
|
|||||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||||
import { dbModel } from '../models/dataModel.js';
|
import { dbModel } from '../models/dataModel.js';
|
||||||
|
|
||||||
import { pathToStartDb, resolveDbDirectory, resolveDbPath } from './index.js';
|
import { pathToStartDb, resolveDbDirectory, resolveDbName } from './index.js';
|
||||||
import { parseProjectFile } from '../services/project-service/projectFileUtils.js';
|
import { parseProjectFile } from '../services/project-service/projectFileUtils.js';
|
||||||
import { parseJson } from '../utils/parser.js';
|
import { parseJson } from '../utils/parser.js';
|
||||||
|
|
||||||
@@ -16,25 +16,25 @@ import { parseJson } from '../utils/parser.js';
|
|||||||
* @description ensures directories exist and populates database
|
* @description ensures directories exist and populates database
|
||||||
* @return {string} - path to db file
|
* @return {string} - path to db file
|
||||||
*/
|
*/
|
||||||
const populateDb = (): string => {
|
const populateDb = (directory: string, filename: string): string => {
|
||||||
// if everything goes well, the DB in disk is the one loaded
|
ensureDirectory(directory);
|
||||||
let dbInDisk = resolveDbPath;
|
let dbPath = join(directory, filename);
|
||||||
ensureDirectory(resolveDbDirectory);
|
|
||||||
|
|
||||||
|
// if everything goes well, the DB in disk is the one loaded
|
||||||
// if dbInDisk doesn't exist we want to use startup db
|
// if dbInDisk doesn't exist we want to use startup db
|
||||||
if (!existsSync(dbInDisk)) {
|
if (!existsSync(dbPath)) {
|
||||||
try {
|
try {
|
||||||
const dbDirectory = resolveDbDirectory;
|
const dbDirectory = resolveDbDirectory;
|
||||||
const newFileDirectory = join(dbDirectory, pathToStartDb.split('/').pop());
|
const newFileDirectory = join(dbDirectory, pathToStartDb.split('/').pop());
|
||||||
|
|
||||||
copyFileSync(pathToStartDb, newFileDirectory);
|
copyFileSync(pathToStartDb, newFileDirectory);
|
||||||
dbInDisk = newFileDirectory;
|
dbPath = newFileDirectory;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
/* we do not handle this */
|
/* we do not handle this */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbInDisk;
|
return dbPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,10 +55,9 @@ const parseDatabase = async (fileToRead: string, adapterToUse: Low<DatabaseModel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description loads ontime db
|
* @description loads ontime db
|
||||||
* @return {Promise<{data: (*), db: Low<unknown>}>}
|
|
||||||
*/
|
*/
|
||||||
async function loadDb() {
|
async function loadDb(directory: string, filename: string) {
|
||||||
const dbInDisk = populateDb();
|
const dbInDisk = populateDb(directory, filename);
|
||||||
|
|
||||||
const adapter = new JSONFile<DatabaseModel>(dbInDisk);
|
const adapter = new JSONFile<DatabaseModel>(dbInDisk);
|
||||||
const db = new Low(adapter, dbModel);
|
const db = new Low(adapter, dbModel);
|
||||||
@@ -72,12 +71,24 @@ async function loadDb() {
|
|||||||
|
|
||||||
export let db = {} as Low<DatabaseModel>;
|
export let db = {} as Low<DatabaseModel>;
|
||||||
export let data = {} as DatabaseModel;
|
export let data = {} as DatabaseModel;
|
||||||
export const dbLoadingProcess = loadDb();
|
export const dbLoadingProcess = loadDb(resolveDbDirectory, resolveDbName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises database at known location
|
||||||
|
*/
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const dbProvider = await dbLoadingProcess;
|
const dbProvider = await dbLoadingProcess;
|
||||||
db = dbProvider.db;
|
db = dbProvider.db;
|
||||||
data = dbProvider.data;
|
data = dbProvider.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to switch the database to a new file
|
||||||
|
*/
|
||||||
|
export const switchDb = async (newFileName: string) => {
|
||||||
|
const { db: newDb, data: newData } = await loadDb(resolveDbDirectory, newFileName);
|
||||||
|
db = newDb;
|
||||||
|
data = newData;
|
||||||
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ describe('mutation on runtimeState', () => {
|
|||||||
stop();
|
stop();
|
||||||
newState = getState();
|
newState = getState();
|
||||||
expect(newState.runtime.actualStart).toBeNull();
|
expect(newState.runtime.actualStart).toBeNull();
|
||||||
expect(newState.runtime.offset).toBe(0);
|
expect(newState.runtime.offset).toBeNull();
|
||||||
expect(newState.runtime.expectedEnd).toBeNull();
|
expect(newState.runtime.expectedEnd).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { timerConfig } from '../config/config.js';
|
|||||||
const initialRuntime: Runtime = {
|
const initialRuntime: Runtime = {
|
||||||
selectedEventIndex: null,
|
selectedEventIndex: null,
|
||||||
numEvents: 0,
|
numEvents: 0,
|
||||||
offset: 0,
|
offset: null,
|
||||||
plannedStart: 0,
|
plannedStart: 0,
|
||||||
plannedEnd: 0,
|
plannedEnd: 0,
|
||||||
actualStart: null,
|
actualStart: null,
|
||||||
@@ -128,7 +128,11 @@ export function updateRundownData(playableRundown: OntimeEvent[]) {
|
|||||||
* @param rundown
|
* @param rundown
|
||||||
* @param initialData
|
* @param initialData
|
||||||
*/
|
*/
|
||||||
export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: Partial<TimerState & RestorePoint>) {
|
export function load(
|
||||||
|
event: OntimeEvent,
|
||||||
|
rundown: OntimeEvent[],
|
||||||
|
initialData?: Partial<TimerState & RestorePoint>,
|
||||||
|
): boolean {
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
updateRundownData(rundown);
|
updateRundownData(rundown);
|
||||||
@@ -153,9 +157,11 @@ export function load(event: OntimeEvent, rundown: OntimeEvent[], initialData?: P
|
|||||||
if (firstStart === null || typeof firstStart === 'number') {
|
if (firstStart === null || typeof firstStart === 'number') {
|
||||||
runtimeState.runtime.actualStart = firstStart;
|
runtimeState.runtime.actualStart = firstStart;
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset;
|
runtimeState.runtime.expectedEnd = (runtimeState.runtime.plannedEnd + runtimeState.runtime.offset) % dayInMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return event.id === runtimeState.eventNow?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) {
|
export function loadNow(event: OntimeEvent, playableEvents: OntimeEvent[]) {
|
||||||
@@ -329,7 +335,9 @@ export function addTime(amount: number) {
|
|||||||
|
|
||||||
// update runtime delays: over - under
|
// update runtime delays: over - under
|
||||||
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
runtimeState.runtime.offset = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.runtime.expectedEnd = runtimeState.runtime.plannedEnd + runtimeState.runtime.offset;
|
if (runtimeState.runtime.offset !== null) {
|
||||||
|
runtimeState.runtime.expectedEnd = (runtimeState.runtime.plannedEnd + runtimeState.runtime.offset) % dayInMs;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import {
|
|||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
EndAction,
|
|
||||||
TimerType,
|
|
||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
EventCustomFields,
|
EventCustomFields,
|
||||||
@@ -325,8 +323,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
|||||||
duration,
|
duration,
|
||||||
timeStrategy,
|
timeStrategy,
|
||||||
linkStart: validateLinkStart(maybeLinkStart),
|
linkStart: validateLinkStart(maybeLinkStart),
|
||||||
endAction: validateEndAction(patchEvent.endAction, EndAction.None),
|
endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction),
|
||||||
timerType: validateTimerType(patchEvent.timerType, TimerType.CountDown),
|
timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType),
|
||||||
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
||||||
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
|
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
|
||||||
note: makeString(patchEvent.note, originalEvent.note),
|
note: makeString(patchEvent.note, originalEvent.note),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { test } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
const fileToUpload = 'e2e/tests/fixtures/test-db.json';
|
const fileToUpload = 'e2e/tests/fixtures/test-db.json';
|
||||||
|
|
||||||
@@ -20,7 +20,12 @@ test('test project file upload', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'close' }).click();
|
await page.getByRole('button', { name: 'close' }).click();
|
||||||
|
|
||||||
// asset test events
|
// asset test events
|
||||||
await page.getByText('Albania').click();
|
const firstTitle = page.getByTestId('entry-1').getByTestId('block__title')
|
||||||
await page.getByText('Latvia').click();
|
await expect(firstTitle).toHaveValue('Albania');
|
||||||
await page.getByText('Lithuania').click();
|
|
||||||
|
const secondTitle = page.getByTestId('entry-2').getByTestId('block__title')
|
||||||
|
await expect(secondTitle).toHaveValue('Latvia');
|
||||||
|
|
||||||
|
const thirdTitle = page.getByTestId('entry-3').getByTestId('block__title')
|
||||||
|
await expect(thirdTitle).toHaveValue('Lithuania');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ test('delays are show correctly', async ({ page }) => {
|
|||||||
await page.getByTestId('rundown').getByTestId('time-input-duration').click();
|
await page.getByTestId('rundown').getByTestId('time-input-duration').click();
|
||||||
await page.getByTestId('rundown').getByTestId('time-input-duration').fill('10');
|
await page.getByTestId('rundown').getByTestId('time-input-duration').fill('10');
|
||||||
await page.getByTestId('rundown').getByTestId('time-input-duration').press('Enter');
|
await page.getByTestId('rundown').getByTestId('time-input-duration').press('Enter');
|
||||||
await page.getByText('Event title').click();
|
await page.getByTestId('block__title').click();
|
||||||
await page.getByPlaceholder('Event title').fill('test');
|
await page.getByTestId('block__title').fill('test');
|
||||||
await page.getByPlaceholder('Event title').press('Enter');
|
await page.getByTestId('block__title').press('Enter');
|
||||||
|
|
||||||
await page.getByTestId('entry-1').getByText('test').click({ button: 'right' });
|
await page.locator('#event-block').getByText('1').click({ button: 'right' });
|
||||||
await page.getByRole('menuitem', { name: 'Toggle public' }).click();
|
await page.getByRole('menuitem', { name: 'Toggle public' }).click();
|
||||||
|
|
||||||
// add a delay
|
// add a delay
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { MaybeNumber } from '../../utils/utils.type.js';
|
|||||||
export type Runtime = {
|
export type Runtime = {
|
||||||
numEvents: number;
|
numEvents: number;
|
||||||
selectedEventIndex: MaybeNumber;
|
selectedEventIndex: MaybeNumber;
|
||||||
offset: number;
|
offset: MaybeNumber;
|
||||||
plannedStart: MaybeNumber;
|
plannedStart: MaybeNumber;
|
||||||
actualStart: MaybeNumber;
|
actualStart: MaybeNumber;
|
||||||
plannedEnd: MaybeNumber;
|
plannedEnd: MaybeNumber;
|
||||||
|
|||||||
Reference in New Issue
Block a user