Compare commits

...

23 Commits

Author SHA1 Message Date
Carlos Valente 37abd3fe47 chore(ci): prevent publishing resolver on release 2025-10-22 06:26:35 +02:00
Carlos Valente b509ee5b1f bump to version 4.0.2 2025-10-22 06:26:35 +02:00
Shobhit-Nagpal dd2b59efe5 fix: move blue line to pseudo element 2025-10-21 16:29:59 +02:00
Shobhit-Nagpal 5788785b3e fix: minor ui shift on quick add inline 2025-10-21 16:29:59 +02:00
Carlos Valente e488ad19b3 fix: ensure well formed URLs for IP address destinations 2025-10-18 15:29:16 +02:00
Carlos Valente cbc03c898c fix: correct alignment to end message 2025-10-18 15:28:47 +02:00
jwetzell 155bfbdc42 Merge pull request #1833 from cpvalente/fix/npm-action-access-error 2025-10-18 07:13:05 -05:00
jwetzell 5d3714a4fc Merge pull request #1836 from cpvalente/fix/use-flat-rundown 2025-10-18 07:12:39 -05:00
jwetzell a8a1d210d8 use flat order when building flat rundown 2025-10-17 21:46:13 -05:00
jwetzell 49de8b63a7 add setup-node step back to resolve npm access issues 2025-10-17 08:13:11 -05:00
Carlos Valente 03c9f51b47 chore: update demo project 2025-10-17 12:20:18 +02:00
Carlos Valente 8d00fd3966 bump version to 4.0.1 2025-10-17 12:20:18 +02:00
Carlos Valente 80fd21354e fix: apply styling to overtime timer 2025-10-16 21:13:26 +02:00
Carlos Valente a87b15825a fix: end message scaling 2025-10-16 21:13:26 +02:00
Carlos Valente 6a8f98c469 fix: end message only shows if timer is in overtime 2025-10-16 21:13:26 +02:00
jwetzell 8685298430 Merge pull request #1830 from cpvalente/fix/linux-project-download 2025-10-15 07:25:57 -05:00
jwetzell d43af2db57 pass options to project download to allow folders beginning with a dot 2025-10-14 19:51:15 -05:00
jwetzell ef718c48e4 Merge pull request #1828 from cpvalente/npm-trusted-publisher
update cli and resolver workflows for new NPM auth
2025-10-14 16:18:53 -05:00
Alex Christoffer Rasmussen 45e7373279 Small clean-up (#1827)
* chore: avoid shadow variable name

* chore: map could just be foreach

* clean uneeded template literal

* lint
2025-10-13 20:24:25 +02:00
jwetzell 6af2d6e021 update cli and resolver workflows for new NPM auth 2025-10-12 14:11:52 -05:00
Alex Christoffer Rasmussen 9b36f36f73 fix: next cue (#1821)
* fix next cue

* Update apps/server/src/services/runtime-service/runtime.service.ts

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2025-10-12 14:42:55 +02:00
Carlos Valente 3a0dbf057e chore: update docs 2025-10-12 07:48:25 +02:00
Carlos Valente dae84ab48a chore: update screenshots 2025-10-12 07:48:25 +02:00
29 changed files with 198 additions and 90 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 149 KiB

+8 -4
View File
@@ -14,6 +14,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -45,10 +50,9 @@ jobs:
- name: Copy external - name: Copy external
run: cp -R apps/server/src/external apps/cli/external run: cp -R apps/server/src/external apps/cli/external
- name: Setup pnpm auth config # This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" - name: Install newer version of npm
env: run: npm install -g npm@11
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- name: Publish to NPM - name: Publish to NPM
run: pnpm publish --access public --no-git-checks run: pnpm publish --access public --no-git-checks
+8 -6
View File
@@ -1,8 +1,6 @@
name: Ontime Resolver build name: Ontime Resolver build
on: on:
release:
types: [published]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -14,6 +12,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -34,10 +37,9 @@ jobs:
- name: Build project packages - name: Build project packages
run: pnpm build:resolver run: pnpm build:resolver
- name: Setup pnpm auth config # This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" - name: Install newer version of npm
env: run: npm install -g npm@11
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- name: Publish to NPM - name: Publish to NPM
run: pnpm publish --access public --no-git-checks run: pnpm publish --access public --no-git-checks
-2
View File
@@ -26,8 +26,6 @@ We do our best to have most topics covered by the documentation. However, if you
Let us know! Let us know!
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback. Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
We would also like to include a testimonials section in our ✨new website✨. It would be great to showcase the diversity of users running Ontime.
# Ontime # Ontime
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing. Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "4.0.0", "version": "4.0.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "4.0.0", "version": "4.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -55,7 +55,7 @@ export function useFlatRundown() {
// update data whenever the revision changes // update data whenever the revision changes
useEffect(() => { useEffect(() => {
if (data.revision !== -1 || data.revision !== prevRevision) { if (data.revision !== -1 || data.revision !== prevRevision) {
const flatRundown = data.order.map((id) => data.entries[id]); const flatRundown = data.flatOrder.map((id) => data.entries[id]);
setFlatRundown(flatRundown); setFlatRundown(flatRundown);
setPrevRevision(data.revision); setPrevRevision(data.revision);
} }
@@ -175,7 +175,7 @@ describe('generateUrlPresetOptions', () => {
expect(() => generateUrlPresetOptions('test', 'invalid-url')).toThrow(); expect(() => generateUrlPresetOptions('test', 'invalid-url')).toThrow();
}); });
it('throws on on invalid route', () => { it('throws on invalid route', () => {
expect(() => generateUrlPresetOptions('test', 'www.getontime.no/somethingelse/')).toThrow(); expect(() => generateUrlPresetOptions('test', 'www.getontime.no/somethingelse/')).toThrow();
}); });
}); });
@@ -2,16 +2,21 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
height: 0;
position: relative; position: relative;
height: 1px;
background: $blue-500;
z-index: $zindex-floating; z-index: $zindex-floating;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: $blue-500;
}
} }
.addButton { .addButton {
position: absolute; position: relative;
top: 0;
left: 0;
transform: translateY(-50%);
} }
@@ -14,12 +14,18 @@ export default function GenerateLinkFormExport({ lockedPath }: GenerateLinkFormE
const { data: infoData } = useInfo(); const { data: infoData } = useInfo();
const { data: urlPresetData } = useUrlPresets({ skip: lockedPath === undefined }); const { data: urlPresetData } = useUrlPresets({ skip: lockedPath === undefined });
/**
* hostOptions are only used for local networks
* the NIF address is a local IP address: 192.168.x.x or 10.x.x.x
* We need to inject the port and protocol to create a valid URL
* eg: http://192.168.x.x:port
*/
const hostOptions = useMemo(() => { const hostOptions = useMemo(() => {
return infoData.networkInterfaces.map((nif) => ({ return infoData.networkInterfaces.map((nif) => ({
value: nif.address, value: `http://${nif.address}:${infoData.serverPort}`,
label: `${nif.name} - ${nif.address}`, label: `${nif.name} - ${nif.address}`,
})); }));
}, [infoData.networkInterfaces]); }, [infoData.networkInterfaces, infoData.serverPort]);
const pathOptions = useMemo(() => { const pathOptions = useMemo(() => {
if (lockedPath) { if (lockedPath) {
+2 -2
View File
@@ -92,10 +92,10 @@
.end-message { .end-message {
text-align: center; text-align: center;
font-size: 11.5vw; font-size: 11.5vw;
line-height: 0.9em;
font-weight: 600; font-weight: 600;
color: var(--timer-overtime-color-override, $timer-finished-color); color: var(--timer-overtime-color-override, $timer-finished-color);
padding: 0; padding: 0;
max-height: 100%;
} }
.timer { .timer {
@@ -117,7 +117,7 @@
// use a class instead of a phase, to allow suppressing overtime style // use a class instead of a phase, to allow suppressing overtime style
&--finished { &--finished {
color: var(--timer-colour, var(--timer-overtime-color-override, $timer-finished-color)); color: var(--timer-overtime-color-override, $timer-finished-color);
} }
&[data-phase="warning"] { &[data-phase="warning"] {
+1 -1
View File
@@ -105,7 +105,7 @@ export function getShowModifiers(
} }
return { return {
showEndMessage: freezeOvertime && freezeMessage !== '', showEndMessage: phase === TimerPhase.Overtime && freezeOvertime && freezeMessage !== '',
showFinished: phase === TimerPhase.Overtime, showFinished: phase === TimerPhase.Overtime,
showWarning: phase === TimerPhase.Warning, showWarning: phase === TimerPhase.Warning,
showDanger: phase === TimerPhase.Danger, showDanger: phase === TimerPhase.Danger,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-electron", "name": "ontime-electron",
"version": "4.0.0", "version": "4.0.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "4.0.0", "version": "4.0.2",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
+1 -1
View File
@@ -109,7 +109,7 @@ export async function projectDownload(req: Request, res: Response) {
return; return;
} }
res.download(pathToFile, filename, (error) => { res.download(pathToFile, filename, { dotfiles: 'allow' }, (error) => {
if (error) { if (error) {
const message = getErrorMessage(error); const message = getErrorMessage(error);
res.status(500).send({ message }); res.status(500).send({ message });
@@ -355,7 +355,7 @@ export function migrateRundown(
const newCustom: EntryCustomFields = {}; const newCustom: EntryCustomFields = {};
if (translationTable) { if (translationTable) {
Object.entries(custom).map(([key, value]) => { Object.entries(custom).forEach(([key, value]) => {
const newKey = translationTable.get(key); const newKey = translationTable.get(key);
if (newKey) { if (newKey) {
newCustom[newKey] = value; newCustom[newKey] = value;
@@ -19,8 +19,10 @@ exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`
"Warning Time", "Warning Time",
"Danger Time", "Danger Time",
"Skip", "Skip",
"Custom Field", "Video Notes",
"Images", "Audio Notes",
"PowerPoint Name",
"PowerPoint Slide",
], ],
[ [
"e2163f", "e2163f",
@@ -33,16 +35,21 @@ exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`
"", "",
"", "",
"", "",
"Go to Settings cog wheel top left of Editor, Project > Create", "Moderator - Emma Thompson
Speakers
- Liam Carter + Sophia Patel
- Ethan Brooks
- Lucas Bennett",
"milestone", "milestone",
"", "",
"", "",
"", "",
"", "",
"Create Custom Fields to store specific text information or images. "",
"",
Go to Settings > Project data > Custom fields", "",
"https://www.getontime.no/images/icons/ontime-logo.png", "",
], ],
[ [
"7eaf99", "7eaf99",
@@ -63,6 +70,8 @@ Go to Settings > Project data > Custom fields",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"9bf60f", "9bf60f",
@@ -81,8 +90,11 @@ Go to Settings > Project data > Custom fields",
"00:10:00", "00:10:00",
"00:05:00", "00:05:00",
"", "",
"Put additional info here", "Camera + PowerPoint on stream
"", PowerPoint on screens",
"2x Wireless Hand Helds",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
], ],
[ [
"bf71a2", "bf71a2",
@@ -103,6 +115,8 @@ Go to Settings > Project data > Custom fields",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"c2697f", "c2697f",
@@ -121,8 +135,11 @@ Go to Settings > Project data > Custom fields",
"00:02:00", "00:02:00",
"00:01:00", "00:01:00",
"", "",
"", "Cameras on stream
"", PowerPoint on screens",
"1x Wireless Hand Held",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
], ],
[ [
"fa593e", "fa593e",
@@ -141,8 +158,11 @@ Go to Settings > Project data > Custom fields",
"00:02:00", "00:02:00",
"00:01:00", "00:01:00",
"", "",
"", "Camera + PowerPoint on stream
"", PowerPoint on screens",
"2x Wireless Hand Helds",
"Session1.pptx",
"https://www.getontime.no/images/aux/demo-slide2.webp",
], ],
[ [
"a8b0b3", "a8b0b3",
@@ -163,6 +183,8 @@ Go to Settings > Project data > Custom fields",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"", "",
@@ -201,6 +223,8 @@ Go to Settings > Project data > Custom fields",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"0aaa7d", "0aaa7d",
@@ -213,15 +237,16 @@ Go to Settings > Project data > Custom fields",
"01:00:00", "01:00:00",
"TRUE", "TRUE",
"", "",
"Buffet in lobby "Buffet in lobby",
Music plays, holding slide on screens",
"count-down", "count-down",
"", "",
"00:02:00", "00:02:00",
"00:01:00", "00:01:00",
"", "",
"", "Holding slide on screens",
"", "House music",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
], ],
[ [
"", "",
@@ -260,6 +285,8 @@ Music plays, holding slide on screens",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"02afca", "02afca",
@@ -280,6 +307,8 @@ Music plays, holding slide on screens",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"75ce86", "75ce86",
@@ -298,8 +327,14 @@ Music plays, holding slide on screens",
"00:02:00", "00:02:00",
"00:01:00", "00:01:00",
"", "",
"", "Camera + PPT + Video on stream
"", PowerPoint + Video on screens
Video file: Session2.mp4",
"1x Wireless Hand Held
1x Video with audio",
"Session2.pptx",
"https://www.getontime.no/images/aux/demo-slide3.webp",
], ],
[ [
"e10ed9", "e10ed9",
@@ -318,8 +353,10 @@ Music plays, holding slide on screens",
"00:02:00", "00:02:00",
"00:01:00", "00:01:00",
"", "",
"", "Holding slide on screens",
"", "1x Wireless Hand Held",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
], ],
[ [
"07df89", "07df89",
@@ -340,6 +377,8 @@ Music plays, holding slide on screens",
"", "",
"", "",
"", "",
"",
"",
], ],
[ [
"", "",
@@ -52,7 +52,7 @@ router.get('/:rundownId/export', validateRundownExport, (req: Request, res: Resp
const customFields = getProjectCustomFields(); const customFields = getProjectCustomFields();
const buffer = generateExcelFile(rundown, customFields); const buffer = generateExcelFile(rundown, customFields);
res.setHeader('Content-Disposition', `attachment;`); res.setHeader('Content-Disposition', 'attachment;');
res.setHeader('Content-Type', EXCEL_MIME); res.setHeader('Content-Type', EXCEL_MIME);
res.setHeader('Content-Length', buffer.length.toString()); res.setHeader('Content-Length', buffer.length.toString());
res.status(200).send(buffer); res.status(200).send(buffer);
@@ -80,6 +80,18 @@ describe('generateAuthenticatedUrl()', () => {
}); });
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/?token=1234'); expect(withAuth.toString()).toBe('http://192.168.10.173:4001/?token=1234');
}); });
it('throws if provided an IP address without protocol', () => {
expect(() =>
generateShareUrl('192.168.10.173', '<<companion>>', {
lockConfig: false,
lockNav: false,
authenticate: true,
preset: undefined,
hash: '1234',
}),
).toThrowError('Invalid URL');
});
}); });
describe('for ontime-cloud URLs', () => { describe('for ontime-cloud URLs', () => {
@@ -62,6 +62,11 @@ export function generateShareUrl(
canonicalPath: string, canonicalPath: string,
{ authenticate, lockConfig, lockNav, preset, prefix = routerPrefix, hash = hashedPassword }: LinkOptions, { authenticate, lockConfig, lockNav, preset, prefix = routerPrefix, hash = hashedPassword }: LinkOptions,
): URL { ): URL {
/**
* URL constructor will throw if given an IP address without protocol
* for the case of IP addresses, we expect that the base URL provides protocol and port
* eg: http://192.168.10.1:4001
*/
const url = new URL(baseUrl); const url = new URL(baseUrl);
// companion links point to the root // companion links point to the root
+2 -2
View File
@@ -123,8 +123,8 @@ function formatTimer(number) {
* @param {number} number - The number to pad * @param {number} number - The number to pad
* @returns {string} The padded number string * @returns {string} The padded number string
*/ */
function leftPad(number) { function leftPad(val) {
return Math.floor(number).toString().padStart(2, '0'); return Math.floor(val).toString().padStart(2, '0');
} }
} }
+51 -17
View File
@@ -28,13 +28,9 @@ export const demoDb: DatabaseModel = {
type: SupportedEntry.Milestone, type: SupportedEntry.Milestone,
cue: 'Demo', cue: 'Demo',
title: 'Clear all, or Create New Project to start fresh', title: 'Clear all, or Create New Project to start fresh',
note: 'Go to Settings cog wheel top left of Editor, Project > Create', note: 'Moderator - Emma Thompson\n\nSpeakers\n- Liam Carter + Sophia Patel\n- Ethan Brooks\n- Lucas Bennett',
colour: '#9d9d9d', colour: '#9d9d9d',
custom: { custom: {},
Custom_Field:
'Create Custom Fields to store specific text information or images.\n\nGo to Settings > Project data > Custom fields',
Images: 'https://www.getontime.no/images/icons/ontime-logo.png',
},
parent: null, parent: null,
revision: 0, revision: 0,
}, },
@@ -78,7 +74,10 @@ export const demoDb: DatabaseModel = {
timeWarning: 600000, timeWarning: 600000,
timeDanger: 300000, timeDanger: 300000,
custom: { custom: {
Custom_Field: 'Put additional info here', PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Camera + PowerPoint on stream\nPowerPoint on screens',
Audio_Notes: '2x Wireless Hand Helds',
PowerPoint_Name: 'HoldingSlide.pptx',
}, },
triggers: [], triggers: [],
}, },
@@ -117,7 +116,12 @@ export const demoDb: DatabaseModel = {
revision: 0, revision: 0,
timeWarning: 120000, timeWarning: 120000,
timeDanger: 60000, timeDanger: 60000,
custom: {}, custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Cameras on stream\nPowerPoint on screens',
Audio_Notes: '1x Wireless Hand Held',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [], triggers: [],
}, },
fa593e: { fa593e: {
@@ -144,7 +148,12 @@ export const demoDb: DatabaseModel = {
revision: 0, revision: 0,
timeWarning: 120000, timeWarning: 120000,
timeDanger: 60000, timeDanger: 60000,
custom: {}, custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide2.webp',
Video_Notes: 'Camera + PowerPoint on stream\nPowerPoint on screens',
Audio_Notes: '2x Wireless Hand Helds',
PowerPoint_Name: 'Session1.pptx',
},
triggers: [], triggers: [],
}, },
a8b0b3: { a8b0b3: {
@@ -187,7 +196,7 @@ export const demoDb: DatabaseModel = {
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
countToEnd: false, countToEnd: false,
skip: false, skip: false,
note: 'Buffet in lobby\nMusic plays, holding slide on screens', note: 'Buffet in lobby',
colour: '#779BE7', colour: '#779BE7',
delay: 0, delay: 0,
dayOffset: 0, dayOffset: 0,
@@ -197,7 +206,12 @@ export const demoDb: DatabaseModel = {
revision: 0, revision: 0,
timeWarning: 120000, timeWarning: 120000,
timeDanger: 60000, timeDanger: 60000,
custom: {}, custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Holding slide on screens',
Audio_Notes: 'House music',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [], triggers: [],
}, },
'6b0edb': { '6b0edb': {
@@ -250,7 +264,12 @@ export const demoDb: DatabaseModel = {
revision: 0, revision: 0,
timeWarning: 120000, timeWarning: 120000,
timeDanger: 60000, timeDanger: 60000,
custom: {}, custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide3.webp',
Video_Notes: 'Camera + PPT + Video on stream\nPowerPoint + Video on screens\n\nVideo file: Session2.mp4',
Audio_Notes: '1x Wireless Hand Held\n1x Video with audio',
PowerPoint_Name: 'Session2.pptx',
},
triggers: [], triggers: [],
}, },
e10ed9: { e10ed9: {
@@ -277,7 +296,12 @@ export const demoDb: DatabaseModel = {
revision: 0, revision: 0,
timeWarning: 120000, timeWarning: 120000,
timeDanger: 60000, timeDanger: 60000,
custom: {}, custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Holding slide on screens',
Audio_Notes: '1x Wireless Hand Held',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [], triggers: [],
}, },
'07df89': { '07df89': {
@@ -341,15 +365,25 @@ export const demoDb: DatabaseModel = {
}, },
], ],
customFields: { customFields: {
Custom_Field: { Video_Notes: {
type: 'text',
colour: '#FFAB33',
label: 'Video Notes',
},
Audio_Notes: {
type: 'text',
colour: '#339E4E',
label: 'Audio Notes',
},
PowerPoint_Name: {
type: 'text', type: 'text',
colour: '#3E75E8', colour: '#3E75E8',
label: 'Custom Field', label: 'PowerPoint Name',
}, },
Images: { PowerPoint_Slide: {
type: 'image', type: 'image',
colour: '#ED3333', colour: '#ED3333',
label: 'Images', label: 'PowerPoint Slide',
}, },
}, },
automation: { automation: {
@@ -50,7 +50,7 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
return false; return false;
} }
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) { if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
return false; return false;
} }
@@ -273,13 +273,14 @@ class RuntimeService {
public startByCue(cue: string): boolean { public startByCue(cue: string): boolean {
const state = runtimeState.getState(); const state = runtimeState.getState();
const rundown = getCurrentRundown(); const rundown = getCurrentRundown();
const { playableEventOrder } = getRundownMetadata(); const { timedEventOrder } = getRundownMetadata();
const event = findNextPlayableWithCue( const event = findNextPlayableWithCue(
rundown, rundown,
playableEventOrder, timedEventOrder,
cue, cue,
state.rundown.selectedEventIndex ?? undefined, state.rundown.selectedEventIndex ?? undefined,
state.timer.playback === Playback.Armed, // If we are armed allow the armed event to be considered for playback
); );
if (!event) { if (!event) {
@@ -341,6 +342,7 @@ class RuntimeService {
playableEventOrder, playableEventOrder,
cue, cue,
state.rundown.selectedEventIndex ?? undefined, state.rundown.selectedEventIndex ?? undefined,
false,
); );
if (!event) { if (!event) {
@@ -118,24 +118,25 @@ export function findNextPlayableId(playableEventsOrder: EntryId[], currentEventI
*/ */
export function findNextPlayableWithCue( export function findNextPlayableWithCue(
rundown: Rundown, rundown: Rundown,
playableEventsOrder: EntryId[], timedEventsOrder: EntryId[],
targetCue: string, targetCue: string,
currentEventIndex = 0, currentEventIndex = 0,
allowCurrent = false,
): OntimeEvent | undefined { ): OntimeEvent | undefined {
const lowerCaseCue = targetCue.toLowerCase(); const startFromIndex = allowCurrent ? currentEventIndex : currentEventIndex + 1;
for (let i = currentEventIndex; i < playableEventsOrder.length; i++) { for (let i = startFromIndex; i < timedEventsOrder.length; i++) {
const eventId = playableEventsOrder[i]; const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId]; const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) { if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event; return event;
} }
} }
for (let i = 0; i < currentEventIndex; i++) { for (let i = 0; i < startFromIndex; i++) {
const eventId = playableEventsOrder[i]; const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId]; const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) { if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event; return event;
} }
} }
@@ -377,7 +377,7 @@ export async function upload(sheetId: string, options: ImportMap) {
const titleMetadata = Object.values(sheetMetadata)[0]; const titleMetadata = Object.values(sheetMetadata)[0];
if (titleMetadata === undefined) { if (titleMetadata === undefined) {
throw new Error(`Sheet read failed: failed to find title row`); throw new Error('Sheet read failed: failed to find title row');
} }
const titleRow = titleMetadata['row']; const titleRow = titleMetadata['row'];
const updateRundown = Array<sheets_v4.Schema$Request>(); const updateRundown = Array<sheets_v4.Schema$Request>();
+4 -4
View File
@@ -11,14 +11,14 @@ describe('parseExcelDate', () => {
['1899-12-30T07:00:00.000Z', 28800000], ['1899-12-30T07:00:00.000Z', 28800000],
['1899-12-30T08:00:10.000Z', 32410000], ['1899-12-30T08:00:10.000Z', 32410000],
['1899-12-30T08:30:00.000Z', 34200000], ['1899-12-30T08:30:00.000Z', 34200000],
])(`handles %s`, (fromExcel, expected) => { ])('handles %s', (fromExcel, expected) => {
expect(parseExcelDate(fromExcel)).toBe(expected); expect(parseExcelDate(fromExcel)).toBe(expected);
}); });
}); });
describe('parses a time string that passes validation', () => { describe('parses a time string that passes validation', () => {
test.each([['10:00:00'], ['10:00'], ['10:00AM'], ['10:00am'], ['10:00PM'], ['10:00pm']])( test.each([['10:00:00'], ['10:00'], ['10:00AM'], ['10:00am'], ['10:00PM'], ['10:00pm']])(
`handles %s`, 'handles %s',
(fromExcel) => { (fromExcel) => {
expect(parseExcelDate(fromExcel)).not.toBe(0); expect(parseExcelDate(fromExcel)).not.toBe(0);
}, },
@@ -26,13 +26,13 @@ describe('parseExcelDate', () => {
}); });
describe('uses numeric fields as minutes', () => { describe('uses numeric fields as minutes', () => {
test.each([[1], [10], [100]])(`handles numeric fields %s`, (fromExcel) => { test.each([[1], [10], [100]])('handles numeric fields %s', (fromExcel) => {
expect(parseExcelDate(fromExcel)).toBe(fromExcel * MILLIS_PER_MINUTE); expect(parseExcelDate(fromExcel)).toBe(fromExcel * MILLIS_PER_MINUTE);
}); });
}); });
describe('returns 0 on other strings', () => { describe('returns 0 on other strings', () => {
test.each([['test'], [''], ['x']])(`handles invalid fields %s`, (fromExcel) => { test.each([['test'], [''], ['x']])('handles invalid fields %s', (fromExcel) => {
expect(parseExcelDate(fromExcel)).toBe(0); expect(parseExcelDate(fromExcel)).toBe(0);
}); });
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "4.0.0", "version": "4.0.2",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"ontime", "ontime",