mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a63fa68d39 | |||
| 20503de6fd | |||
| 0335da9786 | |||
| 9f1a64f53c | |||
| 975356f4f4 | |||
| 1b448835cd | |||
| cbf3d566b9 | |||
| 6c20cb5e99 | |||
| cfc3aab893 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
.title-card {
|
.title-card {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline {
|
.title-card__title,
|
||||||
display: flex;
|
.title-card__secondary {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-card__title {
|
.title-card__title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: clamp(32px, 3.5vw, 50px);
|
font-size: clamp(1.5rem, 3vw, 3rem);
|
||||||
color: var(--color-override, $viewer-color);
|
color: var(--color-override, $viewer-color);
|
||||||
line-height: 1.1em;
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-card__secondary {
|
||||||
|
font-size: clamp(1rem, 2vw, 2.25rem);
|
||||||
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
|
line-height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-card__label {
|
.title-card__label {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 0.5rem;
|
||||||
font-size: clamp(1rem, 1.5vw, 1.5rem);
|
font-size: clamp(1rem, 1.5vw, 1.5rem);
|
||||||
font-weight: 400;
|
|
||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -28,13 +40,3 @@
|
|||||||
color: var(--accent-color-override, $accent-color);
|
color: var(--accent-color-override, $accent-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-card__secondary {
|
|
||||||
font-size: clamp(1.5rem, 2vw, 2.25rem);
|
|
||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
|
||||||
line-height: 1.1em;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '\200b';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
|
import { ForwardedRef, forwardRef } from 'react';
|
||||||
|
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
|
||||||
import './TitleCard.scss';
|
import './TitleCard.scss';
|
||||||
|
|
||||||
interface TitleCardProps {
|
interface TitleCardProps {
|
||||||
label: 'now' | 'next';
|
|
||||||
title: string;
|
title: string;
|
||||||
|
label?: 'now' | 'next';
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleCard(props: TitleCardProps) {
|
const TitleCard = forwardRef((props: TitleCardProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||||
const { label, title, secondary } = props;
|
const { label, title, secondary, className = '' } = props;
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
const accent = label === 'now';
|
const accent = label === 'now';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='title-card'>
|
<div className={`title-card ${className}`} ref={ref}>
|
||||||
<div className='inline'>
|
<span className='title-card__title'>{title}</span>
|
||||||
<span className='title-card__title'>{title}</span>
|
<span className={accent ? 'title-card__label title-card__label--accent' : 'title-card__label'}>
|
||||||
<span className={accent ? 'title-card__label title-card__label--accent' : 'title-card__label'}>
|
{label && getLocalizedString(`common.${label}`)}
|
||||||
{getLocalizedString(`common.${label}`)}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='title-card__secondary'>{secondary}</div>
|
<div className='title-card__secondary'>{secondary}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
TitleCard.displayName = 'TitleCard';
|
||||||
|
export default TitleCard;
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { useEffect } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button, Input } from '@chakra-ui/react';
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
import * as Panel from '../PanelUtils';
|
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
export type ProjectFormValues = {
|
export type ProjectFormValues = {
|
||||||
@@ -15,10 +13,9 @@ interface ProjectFormProps {
|
|||||||
filename: string;
|
filename: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
||||||
submitError: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectForm({ action, filename, onSubmit, onCancel, submitError }: ProjectFormProps) {
|
export default function ProjectForm({ action, filename, onSubmit, onCancel }: ProjectFormProps) {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
@@ -37,34 +34,31 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel, subm
|
|||||||
}, [setFocus]);
|
}, [setFocus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<form onSubmit={handleSubmit(onSubmit)} className={style.form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={style.form}>
|
<Input
|
||||||
<Input
|
className={style.formInput}
|
||||||
className={style.formInput}
|
id='filename'
|
||||||
id='filename'
|
size='sm'
|
||||||
|
type='text'
|
||||||
|
variant='ontime-filled'
|
||||||
|
placeholder='Enter new name'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('filename', { required: true })}
|
||||||
|
/>
|
||||||
|
<div className={style.actionButtons}>
|
||||||
|
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
type='text'
|
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
placeholder='Enter new name'
|
isDisabled={!isDirty || !isValid || isSubmitting}
|
||||||
autoComplete='off'
|
type='submit'
|
||||||
{...register('filename')}
|
className={style.saveButton}
|
||||||
/>
|
>
|
||||||
<div className={style.actionButtons}>
|
{action}
|
||||||
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
|
</Button>
|
||||||
Cancel
|
</div>
|
||||||
</Button>
|
</form>
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
isDisabled={!isDirty || !isValid || isSubmitting}
|
|
||||||
type='submit'
|
|
||||||
className={style.saveButton}
|
|
||||||
>
|
|
||||||
{action}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{submitError && <Panel.Error>{submitError}</Panel.Error>}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
renameProject,
|
renameProject,
|
||||||
} from '../../../../common/api/db';
|
} from '../../../../common/api/db';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||||
|
|
||||||
@@ -40,36 +41,53 @@ export default function ProjectListItem({
|
|||||||
onToggleEditMode,
|
onToggleEditMode,
|
||||||
}: ProjectListItemProps) {
|
}: ProjectListItemProps) {
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmitRename = async (values: ProjectFormValues) => {
|
const handleSubmitAction = (actionType: 'rename' | 'duplicate') => {
|
||||||
try {
|
return async (values: ProjectFormValues) => {
|
||||||
|
setLoading(true);
|
||||||
setSubmitError(null);
|
setSubmitError(null);
|
||||||
|
try {
|
||||||
if (!values.filename) {
|
if (!values.filename) {
|
||||||
setSubmitError('Filename cannot be blank');
|
setSubmitError('Filename cannot be blank');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const action = actionType === 'rename' ? renameProject : duplicateProject;
|
||||||
|
await action(filename, values.filename);
|
||||||
|
await onRefetch();
|
||||||
|
onSubmit();
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError(maybeAxiosError(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
await renameProject(filename, values.filename);
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoad = async (filename: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setSubmitError(null);
|
||||||
|
try {
|
||||||
|
await loadProject(filename);
|
||||||
await onRefetch();
|
await onRefetch();
|
||||||
onSubmit();
|
await invalidateAllCaches();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSubmitError(maybeAxiosError(error));
|
setSubmitError(maybeAxiosError(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitDuplicate = async (values: ProjectFormValues) => {
|
const handleDelete = async (filename: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setSubmitError(null);
|
||||||
try {
|
try {
|
||||||
setSubmitError(null);
|
await deleteProject(filename);
|
||||||
|
|
||||||
if (!values.filename) {
|
|
||||||
setSubmitError('Filename cannot be blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await duplicateProject(filename, values.filename);
|
|
||||||
await onRefetch();
|
await onRefetch();
|
||||||
onSubmit();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSubmitError(maybeAxiosError(error));
|
setSubmitError(maybeAxiosError(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,50 +104,55 @@ export default function ProjectListItem({
|
|||||||
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
|
const classes = current && !isCurrentlyBeingEdited ? style.current : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={filename} className={classes}>
|
<>
|
||||||
{isCurrentlyBeingEdited ? (
|
{submitError && (
|
||||||
<td colSpan={99}>
|
<tr key='filename-error'>
|
||||||
<ProjectForm
|
<td colSpan={99}>
|
||||||
action={editingMode}
|
<Panel.Error>{submitError}</Panel.Error>
|
||||||
filename={filename}
|
</td>
|
||||||
onSubmit={editingMode === 'duplicate' ? handleSubmitDuplicate : handleSubmitRename}
|
</tr>
|
||||||
onCancel={handleCancel}
|
)}
|
||||||
submitError={submitError}
|
<tr key={filename} className={classes}>
|
||||||
/>
|
{isCurrentlyBeingEdited ? (
|
||||||
</td>
|
<td colSpan={99}>
|
||||||
) : (
|
<ProjectForm
|
||||||
<>
|
action={editingMode}
|
||||||
<td className={style.containCell}>{filename}</td>
|
|
||||||
<td>{new Date(updatedAt).toLocaleString()}</td>
|
|
||||||
<td className={style.actionButton}>
|
|
||||||
<ActionMenu
|
|
||||||
current={current}
|
|
||||||
filename={filename}
|
filename={filename}
|
||||||
onChangeEditMode={handleToggleEditMode}
|
onSubmit={editingMode === 'duplicate' ? handleSubmitAction('duplicate') : handleSubmitAction('rename')}
|
||||||
onRefetch={onRefetch}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</>
|
) : (
|
||||||
)}
|
<>
|
||||||
</tr>
|
<td className={style.containCell}>{filename}</td>
|
||||||
|
<td>{new Date(updatedAt).toLocaleString()}</td>
|
||||||
|
<td className={style.actionButton}>
|
||||||
|
<ActionMenu
|
||||||
|
current={current}
|
||||||
|
filename={filename}
|
||||||
|
onChangeEditMode={handleToggleEditMode}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
isDisabled={loading}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionMenu({
|
interface ActionMenuProps {
|
||||||
current,
|
|
||||||
filename,
|
|
||||||
onChangeEditMode,
|
|
||||||
onRefetch,
|
|
||||||
}: {
|
|
||||||
current?: boolean;
|
current?: boolean;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
isDisabled: boolean;
|
||||||
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
onChangeEditMode: (editMode: EditMode, filename: string) => void;
|
||||||
onRefetch: () => Promise<void>;
|
onDelete: (filename: string) => void;
|
||||||
}) {
|
onLoad: (filename: string) => void;
|
||||||
const handleLoad = async () => {
|
}
|
||||||
await loadProject(filename);
|
function ActionMenu(props: ActionMenuProps) {
|
||||||
await invalidateAllCaches();
|
const { current, filename, isDisabled, onChangeEditMode, onDelete, onLoad } = props;
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = () => {
|
const handleRename = () => {
|
||||||
onChangeEditMode('rename', filename);
|
onChangeEditMode('rename', filename);
|
||||||
@@ -139,11 +162,6 @@ function ActionMenu({
|
|||||||
onChangeEditMode('duplicate', filename);
|
onChangeEditMode('duplicate', filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
await deleteProject(filename);
|
|
||||||
await onRefetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
await downloadProject(filename);
|
await downloadProject(filename);
|
||||||
};
|
};
|
||||||
@@ -161,16 +179,17 @@ function ActionMenu({
|
|||||||
color='#e2e2e2' // $gray-200
|
color='#e2e2e2' // $gray-200
|
||||||
variant='ontime-ghosted'
|
variant='ontime-ghosted'
|
||||||
size='sm'
|
size='sm'
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem onClick={handleLoad} isDisabled={current}>
|
<MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
|
||||||
Load
|
Load
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
||||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
||||||
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
||||||
<MenuItem isDisabled={current} onClick={handleDelete}>
|
<MenuItem isDisabled={current} onClick={() => onDelete(filename)}>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
|||||||
@@ -65,12 +65,15 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
|
|
||||||
|
@include ellipsis-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondaryField {
|
.secondaryField {
|
||||||
grid-area: secondary;
|
grid-area: secondary;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
@include ellipsis-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule {
|
.schedule {
|
||||||
@@ -96,10 +99,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0 0.25rem;
|
padding-inline: 0.25rem;
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.noColour {
|
.noColour {
|
||||||
@@ -110,6 +113,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
|
padding-inline: 0.25rem;
|
||||||
|
background-color: $gray-1250;
|
||||||
color: var(--operator-highlight-override, $ui-white);
|
color: var(--operator-highlight-override, $ui-white);
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { EditorUpdateFields } from '../EventEditor';
|
|||||||
|
|
||||||
import style from '../EventEditor.module.scss';
|
import style from '../EventEditor.module.scss';
|
||||||
|
|
||||||
interface CountedTextInputProps extends InputProps {
|
interface EventTextInputProps extends InputProps {
|
||||||
field: EditorUpdateFields;
|
field: EditorUpdateFields;
|
||||||
label: string;
|
label: string;
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventTextInput(props: CountedTextInputProps) {
|
export default function EventTextInput(props: EventTextInputProps) {
|
||||||
const { field, label, initialValue, submitHandler, maxLength } = props;
|
const { field, label, initialValue, submitHandler, maxLength } = props;
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
@@ -34,7 +34,7 @@ export default function EventTextInput(props: CountedTextInputProps) {
|
|||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
data-testid='input-textfield'
|
data-testid='input-textfield'
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={maxLength || 50}
|
maxLength={maxLength || 100}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { getPropertyValue } from '../common/viewUtils';
|
|||||||
|
|
||||||
import './Backstage.scss';
|
import './Backstage.scss';
|
||||||
|
|
||||||
|
export const MotionTitleCard = motion(TitleCard);
|
||||||
|
|
||||||
interface BackstageProps {
|
interface BackstageProps {
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
@@ -124,7 +126,7 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
>
|
||||||
<TitleCard label='now' title={eventNow.title} secondary={secondaryTextNow} />
|
<TitleCard title={eventNow.title} secondary={secondaryTextNow} />
|
||||||
<div className='timer-group'>
|
<div className='timer-group'>
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
||||||
@@ -151,16 +153,17 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{eventNext && (
|
{eventNext && (
|
||||||
<motion.div
|
<MotionTitleCard
|
||||||
className='event next'
|
className='event next'
|
||||||
key='next'
|
key='next'
|
||||||
variants={titleVariants}
|
variants={titleVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
label='next'
|
||||||
<TitleCard label='next' title={eventNext.title} secondary={secondaryTextNext} />
|
title={eventNext.title}
|
||||||
</motion.div>
|
secondary={secondaryTextNext}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
@@ -12,6 +11,7 @@ import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
|||||||
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
import { getFormattedTimer, isStringBoolean } from '../common/viewUtils';
|
||||||
|
|
||||||
import { fetchTimerData, TimerMessage } from './countdown.helpers';
|
import { fetchTimerData, TimerMessage } from './countdown.helpers';
|
||||||
import CountdownSelect from './CountdownSelect';
|
import CountdownSelect from './CountdownSelect';
|
||||||
@@ -84,24 +84,17 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id;
|
const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id;
|
||||||
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
|
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
|
||||||
const isRunningFinished = finished && runningMessage === TimerMessage.running;
|
const isRunningFinished = finished && runningMessage === TimerMessage.running;
|
||||||
const isSelected = runningMessage === TimerMessage.running;
|
|
||||||
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
||||||
|
|
||||||
const clock = formatTime(time.clock);
|
const clock = formatTime(time.clock);
|
||||||
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay);
|
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay);
|
||||||
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay);
|
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay);
|
||||||
|
|
||||||
const formatTimer = (): string => {
|
const hideSeconds = searchParams.get('hideTimerSeconds');
|
||||||
if (runningMessage === TimerMessage.ended) {
|
const formattedTimer = getFormattedTimer(runningTimer, time.timerType, getLocalizedString('common.minutes'), {
|
||||||
return formatTime(runningTimer, { format12: 'hh:mm a', format24: 'HH:mm' });
|
removeSeconds: isStringBoolean(hideSeconds),
|
||||||
}
|
removeLeadingZero: false,
|
||||||
let formattedTime = millisToString(isSelected ? runningTimer : runningTimer + delay);
|
});
|
||||||
if (isSelected || runningMessage === TimerMessage.waiting) {
|
|
||||||
formattedTime = removeLeadingZero(formattedTime);
|
|
||||||
}
|
|
||||||
return formattedTime;
|
|
||||||
};
|
|
||||||
const formattedTimer = formatTimer();
|
|
||||||
|
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const timeOption = getCountdownOptions(defaultFormat);
|
const timeOption = getCountdownOptions(defaultFormat);
|
||||||
|
|||||||
@@ -72,6 +72,8 @@
|
|||||||
font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif;
|
font-family: var(--lowerThird-font-family-override), Lato, Arial, sans-serif;
|
||||||
text-align: var(--lowerThird-text-align-override, left);
|
text-align: var(--lowerThird-text-align-override, left);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@include ellipsis-text();
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '\200b';
|
content: '\200b';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { getPropertyValue } from '../common/viewUtils';
|
|||||||
|
|
||||||
import './Public.scss';
|
import './Public.scss';
|
||||||
|
|
||||||
|
export const MotionTitleCard = motion(TitleCard);
|
||||||
|
|
||||||
interface BackstageProps {
|
interface BackstageProps {
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
@@ -83,31 +85,33 @@ export default function Public(props: BackstageProps) {
|
|||||||
<div className='now-container'>
|
<div className='now-container'>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{publicEventNow && (
|
{publicEventNow && (
|
||||||
<motion.div
|
<MotionTitleCard
|
||||||
className='event now'
|
className='event now'
|
||||||
key='now'
|
key='now'
|
||||||
variants={titleVariants}
|
variants={titleVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
label='now'
|
||||||
<TitleCard label='now' title={publicEventNow.title} secondary={secondaryTextNow} />
|
title={publicEventNow.title}
|
||||||
</motion.div>
|
secondary={secondaryTextNow}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{publicEventNext && (
|
{publicEventNext && (
|
||||||
<motion.div
|
<MotionTitleCard
|
||||||
className='event next'
|
className='event next'
|
||||||
key='next'
|
key='next'
|
||||||
variants={titleVariants}
|
variants={titleVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
label='next'
|
||||||
<TitleCard label='next' title={publicEventNext.title} secondary={secondaryTextNext} />
|
title={publicEventNext.title}
|
||||||
</motion.div>
|
secondary={secondaryTextNext}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ $orange-active: #f60;
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&--now {
|
&--now {
|
||||||
color: var(--studio-active-label, $red-active);
|
color: var(--studio-active-label, $red-active);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
// TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts?
|
// TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts?
|
||||||
// deferring rendering seems to affect styling (font and useFitText)
|
// deferring rendering seems to affect styling (font and useFitText)
|
||||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ minFontSize: 150, maxFontSize: 500 });
|
||||||
|
|
||||||
const activeIndicators = [...Array(12).keys()];
|
const activeIndicators = [...Array(12).keys()];
|
||||||
const secondsIndicators = [...Array(60).keys()];
|
const secondsIndicators = [...Array(60).keys()];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
|
padding: min(2vh, 16px) clamp(16px, 10vw, 64px);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto;
|
grid-template-columns: 100%;
|
||||||
grid-template-rows: auto 1fr auto auto auto;
|
grid-template-rows: auto 1fr auto auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'clock'
|
'clock'
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const titleVariants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MotionTitleCard = motion(TitleCard);
|
||||||
|
|
||||||
interface TimerProps {
|
interface TimerProps {
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
eventNext: OntimeEvent | null;
|
eventNext: OntimeEvent | null;
|
||||||
@@ -211,31 +213,33 @@ export default function Timer(props: TimerProps) {
|
|||||||
<>
|
<>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{eventNow?.title && (
|
{eventNow?.title && (
|
||||||
<motion.div
|
<MotionTitleCard
|
||||||
className='event now'
|
className='event now'
|
||||||
key='now'
|
key='now'
|
||||||
variants={titleVariants}
|
variants={titleVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
label='now'
|
||||||
<TitleCard label='now' title={mainFieldNow} secondary={secondaryTextNow} />
|
title={mainFieldNow}
|
||||||
</motion.div>
|
secondary={secondaryTextNow}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{eventNext?.title && (
|
{eventNext?.title && (
|
||||||
<motion.div
|
<MotionTitleCard
|
||||||
className='event next'
|
|
||||||
key='next'
|
key='next'
|
||||||
variants={titleVariants}
|
variants={titleVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
label='next'
|
||||||
<TitleCard label='next' title={mainFieldNext} secondary={secondaryTextNext} />
|
title={mainFieldNext}
|
||||||
</motion.div>
|
secondary={secondaryTextNext}
|
||||||
|
className='event next'
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use "ontimeStyles" as *;
|
@use 'ontimeStyles' as *;
|
||||||
|
|
||||||
//////////////////////////////////// general app elements
|
//////////////////////////////////// general app elements
|
||||||
|
|
||||||
@@ -15,3 +15,9 @@
|
|||||||
color: $ontime-color;
|
color: $ontime-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin ellipsis-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default defineConfig({
|
|||||||
additionalData: `
|
additionalData: `
|
||||||
@use './src/theme/ontimeColours' as *;
|
@use './src/theme/ontimeColours' as *;
|
||||||
@use './src/theme/ontimeStyles' as *;
|
@use './src/theme/ontimeStyles' as *;
|
||||||
|
@use './src/theme/mixins' as *;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"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",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
"node-xlsx": "^0.23.0",
|
"node-xlsx": "^0.23.0",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"steno": "^3.1.0",
|
"steno": "^4.0.2",
|
||||||
"ts-essentials": "^9.4.1",
|
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.3",
|
||||||
|
"ts-essentials": "^9.4.1",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
validatePatchProject,
|
validatePatchProject,
|
||||||
validateFilenameBody,
|
validateFilenameBody,
|
||||||
validateFilenameParam,
|
validateFilenameParam,
|
||||||
|
validateNewFilenameBody,
|
||||||
} from './db.validation.js';
|
} from './db.validation.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
@@ -31,7 +32,7 @@ router.post('/new', validateFilenameBody, validateNewProject, createProjectFile)
|
|||||||
router.get('/all', listProjects);
|
router.get('/all', listProjects);
|
||||||
|
|
||||||
router.post('/load', validateFilenameBody, loadProject);
|
router.post('/load', validateFilenameBody, loadProject);
|
||||||
router.post('/:filename/duplicate', validateFilenameParam, validateFilenameBody, duplicateProjectFile);
|
router.post('/:filename/duplicate', validateFilenameParam, validateNewFilenameBody, duplicateProjectFile);
|
||||||
router.put('/:filename/rename', validateFilenameParam, validateFilenameBody, renameProjectFile);
|
router.put('/:filename/rename', validateFilenameParam, validateFilenameBody, renameProjectFile);
|
||||||
router.delete('/:filename', validateFilenameParam, deleteProjectFile);
|
router.delete('/:filename', validateFilenameParam, deleteProjectFile);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,30 @@ export const validatePatchProject = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates request with newFilename in the body.
|
||||||
|
*/
|
||||||
|
export const validateNewFilenameBody = [
|
||||||
|
body('newFilename')
|
||||||
|
.exists()
|
||||||
|
.isString()
|
||||||
|
.trim()
|
||||||
|
.customSanitizer((input: string) => sanitize(input))
|
||||||
|
.withMessage('Failed to sanitize the filename')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('Filename was empty or contained only invalid characters')
|
||||||
|
.customSanitizer((input: string) => ensureJsonExtension(input)),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Validates request with filename in the body.
|
* @description Validates request with filename in the body.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
resolveExternalsDirectory,
|
resolveExternalsDirectory,
|
||||||
resolveStylesDirectory,
|
resolveStylesDirectory,
|
||||||
resolvedPath,
|
resolvedPath,
|
||||||
clearUploadfolder,
|
|
||||||
} from './setup/index.js';
|
} from './setup/index.js';
|
||||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||||
import { consoleSuccess, consoleHighlight } from './utils/console.js';
|
import { consoleSuccess, consoleHighlight } from './utils/console.js';
|
||||||
@@ -44,6 +43,9 @@ import { messageService } from './services/message-service/MessageService.js';
|
|||||||
import { populateDemo } from './setup/loadDemo.js';
|
import { populateDemo } from './setup/loadDemo.js';
|
||||||
import { getState } from './stores/runtimeState.js';
|
import { getState } from './stores/runtimeState.js';
|
||||||
import { initRundown } from './services/rundown-service/RundownService.js';
|
import { initRundown } from './services/rundown-service/RundownService.js';
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
import { clearUploadfolder } from './utils/upload.js';
|
||||||
import { generateCrashReport } from './utils/generateCrashReport.js';
|
import { generateCrashReport } from './utils/generateCrashReport.js';
|
||||||
import { getNetworkInterfaces } from './utils/networkInterfaces.js';
|
import { getNetworkInterfaces } from './utils/networkInterfaces.js';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { generateId, millisToString } from 'ontime-utils';
|
|||||||
import { clock } from '../services/Clock.js';
|
import { clock } from '../services/Clock.js';
|
||||||
import { isProduction } from '../setup/index.js';
|
import { isProduction } from '../setup/index.js';
|
||||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
import { consoleSubdued, consoleRed } from '../utils/console.js';
|
import { consoleSubdued, consoleError } from '../utils/console.js';
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
private queue: Log[];
|
private queue: Log[];
|
||||||
@@ -47,7 +47,7 @@ class Logger {
|
|||||||
private _push(log: Log) {
|
private _push(log: Log) {
|
||||||
if (this.canLog || log.level === LogLevel.Severe) {
|
if (this.canLog || log.level === LogLevel.Severe) {
|
||||||
if (log.level === LogLevel.Severe) {
|
if (log.level === LogLevel.Severe) {
|
||||||
consoleRed(`[${log.level}] \t ${log.origin} \t ${log.text}`);
|
consoleError(`[${log.level}] \t ${log.origin} \t ${log.text}`);
|
||||||
} else {
|
} else {
|
||||||
consoleSubdued(`[${log.level}] \t ${log.origin} \t ${log.text}`);
|
consoleSubdued(`[${log.level}] \t ${log.origin} \t ${log.text}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { consoleHighlight, consoleRed } from './utils/console.js';
|
import { consoleHighlight, consoleError } from './utils/console.js';
|
||||||
import { initAssets, startIntegrations, startServer } from './app.js';
|
import { initAssets, startIntegrations, startServer } from './app.js';
|
||||||
|
|
||||||
async function startOntime() {
|
async function startOntime() {
|
||||||
@@ -16,7 +16,7 @@ async function startOntime() {
|
|||||||
consoleHighlight('Request: Start integrations...');
|
consoleHighlight('Request: Start integrations...');
|
||||||
await startIntegrations();
|
await startIntegrations();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consoleRed(`Request failed: ${error}`);
|
consoleError(`Request failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,21 +14,24 @@ interface Config {
|
|||||||
class AppStateService {
|
class AppStateService {
|
||||||
private config: Low<Config>;
|
private config: Low<Config>;
|
||||||
private pathToFile: string;
|
private pathToFile: string;
|
||||||
|
private didInit = false;
|
||||||
|
|
||||||
constructor(appStatePath: string) {
|
constructor(appStatePath: string) {
|
||||||
this.pathToFile = appStatePath;
|
this.pathToFile = appStatePath;
|
||||||
const adapter = new JSONFile<Config>(this.pathToFile);
|
const adapter = new JSONFile<Config>(this.pathToFile);
|
||||||
this.config = new Low<Config>(adapter, null);
|
this.config = new Low<Config>(adapter, null);
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
await this.config.read();
|
await this.config.read();
|
||||||
await this.config.write();
|
await this.config.write();
|
||||||
|
this.didInit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(): Promise<Config> {
|
async get(): Promise<Config> {
|
||||||
|
if (!this.didInit) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
await this.config.read();
|
await this.config.read();
|
||||||
return this.config.data;
|
return this.config.data;
|
||||||
}
|
}
|
||||||
@@ -36,6 +39,10 @@ class AppStateService {
|
|||||||
async updateDatabaseConfig(filename: string): Promise<void> {
|
async updateDatabaseConfig(filename: string): Promise<void> {
|
||||||
if (isTest) return;
|
if (isTest) return;
|
||||||
|
|
||||||
|
if (!this.didInit) {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
this.config.data.lastLoadedProject = filename;
|
this.config.data.lastLoadedProject = filename;
|
||||||
await this.config.write();
|
await this.config.write();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ describe('generate()', () => {
|
|||||||
expect(initResult.totalDuration).toBe(500 - 100);
|
expect(initResult.totalDuration).toBe(500 - 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calculates total duration with 0 duration events without causing a next day', () => {
|
||||||
|
const testRundown: OntimeRundown = [
|
||||||
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
|
||||||
|
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300 } as OntimeEvent,
|
||||||
|
{ type: SupportedEvent.Event, id: 'skipped', skip: true, timeStart: 300, timeEnd: 400 } as OntimeEvent,
|
||||||
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
|
||||||
|
const initResult = generate(testRundown);
|
||||||
|
expect(initResult.order.length).toBe(4);
|
||||||
|
expect(initResult.totalDuration).toBe(500 - 100);
|
||||||
|
});
|
||||||
|
|
||||||
it('calculates total duration across days with gap', () => {
|
it('calculates total duration across days with gap', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const testRundown: OntimeRundown = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export function generate(
|
|||||||
let daySpan = 0;
|
let daySpan = 0;
|
||||||
let previousStart: MaybeNumber = null;
|
let previousStart: MaybeNumber = null;
|
||||||
let previousEnd: MaybeNumber = null;
|
let previousEnd: MaybeNumber = null;
|
||||||
|
let previousDuration: MaybeNumber = null;
|
||||||
|
|
||||||
for (let i = 0; i < initialRundown.length; i++) {
|
for (let i = 0; i < initialRundown.length; i++) {
|
||||||
const currentEvent = initialRundown[i];
|
const currentEvent = initialRundown[i];
|
||||||
@@ -111,7 +112,8 @@ export function generate(
|
|||||||
lastEnd = updatedEvent.timeEnd;
|
lastEnd = updatedEvent.timeEnd;
|
||||||
|
|
||||||
// check if we go over midnight, account for eventual gaps
|
// check if we go over midnight, account for eventual gaps
|
||||||
const gapOverMidnight = previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart);
|
const gapOverMidnight =
|
||||||
|
previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart, previousDuration);
|
||||||
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
|
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
|
||||||
if (gapOverMidnight || durationOverMidnight) {
|
if (gapOverMidnight || durationOverMidnight) {
|
||||||
daySpan++;
|
daySpan++;
|
||||||
@@ -134,6 +136,7 @@ export function generate(
|
|||||||
updatedEvent.delay = accumulatedDelay;
|
updatedEvent.delay = accumulatedDelay;
|
||||||
previousStart = updatedEvent.timeStart;
|
previousStart = updatedEvent.timeStart;
|
||||||
previousEnd = updatedEvent.timeEnd;
|
previousEnd = updatedEvent.timeEnd;
|
||||||
|
previousDuration = updatedEvent.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
order.push(updatedEvent.id);
|
order.push(updatedEvent.id);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import path, { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import fs from 'fs';
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
import { rm } from 'fs/promises';
|
|
||||||
|
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||||
@@ -17,18 +15,18 @@ import { ensureDirectory } from '../utils/fileManagement.js';
|
|||||||
export function getAppDataPath(): string {
|
export function getAppDataPath(): string {
|
||||||
// handle docker
|
// handle docker
|
||||||
if (process.env.ONTIME_DATA) {
|
if (process.env.ONTIME_DATA) {
|
||||||
return path.join(process.env.ONTIME_DATA);
|
return join(process.env.ONTIME_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'darwin': {
|
case 'darwin': {
|
||||||
return path.join(process.env.HOME!, 'Library', 'Application Support', 'Ontime');
|
return join(process.env.HOME!, 'Library', 'Application Support', 'Ontime');
|
||||||
}
|
}
|
||||||
case 'win32': {
|
case 'win32': {
|
||||||
return path.join(process.env.APPDATA!, 'Ontime');
|
return join(process.env.APPDATA!, 'Ontime');
|
||||||
}
|
}
|
||||||
case 'linux': {
|
case 'linux': {
|
||||||
return path.join(process.env.HOME!, '.Ontime');
|
return join(process.env.HOME!, '.Ontime');
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Could not resolve public folder for platform');
|
throw new Error('Could not resolve public folder for platform');
|
||||||
@@ -56,11 +54,11 @@ if (import.meta.url) {
|
|||||||
// path to server src folder
|
// path to server src folder
|
||||||
const currentDir = dirname(__dirname);
|
const currentDir = dirname(__dirname);
|
||||||
// locally we are in src/setup, in the production build, this is a single file at src
|
// locally we are in src/setup, in the production build, this is a single file at src
|
||||||
export const srcDirectory = isProduction ? currentDir : path.join(currentDir, '../');
|
export const srcDirectory = isProduction ? currentDir : join(currentDir, '../');
|
||||||
|
|
||||||
// resolve path to external
|
// resolve path to external
|
||||||
const productionPath = path.join(srcDirectory, 'client/');
|
const productionPath = join(srcDirectory, 'client/');
|
||||||
const devPath = path.join(srcDirectory, '../../client/build/');
|
const devPath = join(srcDirectory, '../../client/build/');
|
||||||
|
|
||||||
export const resolvedPath = (): string => {
|
export const resolvedPath = (): string => {
|
||||||
if (isTest) {
|
if (isTest) {
|
||||||
@@ -83,12 +81,12 @@ export const uploadsFolderPath = join(getAppDataPath(), config.uploads);
|
|||||||
|
|
||||||
const ensureAppState = () => {
|
const ensureAppState = () => {
|
||||||
ensureDirectory(getAppDataPath());
|
ensureDirectory(getAppDataPath());
|
||||||
fs.writeFileSync(appStatePath, JSON.stringify({ lastLoadedProject: 'db.json' }));
|
writeFileSync(appStatePath, JSON.stringify({ lastLoadedProject: 'db.json' }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLastLoadedProject = () => {
|
const getLastLoadedProject = () => {
|
||||||
try {
|
try {
|
||||||
const appState = JSON.parse(fs.readFileSync(appStatePath, 'utf8'));
|
const appState = JSON.parse(readFileSync(appStatePath, 'utf8'));
|
||||||
if (!appState.lastLoadedProject) {
|
if (!appState.lastLoadedProject) {
|
||||||
ensureAppState();
|
ensureAppState();
|
||||||
}
|
}
|
||||||
@@ -142,11 +140,3 @@ export const resolveCrashReportDirectory = getAppDataPath();
|
|||||||
|
|
||||||
// path to projects
|
// path to projects
|
||||||
export const resolveProjectsDirectory = join(getAppDataPath(), config.projects);
|
export const resolveProjectsDirectory = join(getAppDataPath(), config.projects);
|
||||||
|
|
||||||
export async function clearUploadfolder() {
|
|
||||||
try {
|
|
||||||
await rm(uploadsFolderPath, { recursive: true });
|
|
||||||
} catch (_) {
|
|
||||||
//we dont care that there was no folder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function consoleSuccess(message: string) {
|
|||||||
/**
|
/**
|
||||||
* Utility function to log messages in red
|
* Utility function to log messages in red
|
||||||
*/
|
*/
|
||||||
export function consoleRed(message: string) {
|
export function consoleError(message: string) {
|
||||||
console.error(inRed(message));
|
console.error(inRed(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { get } from '../services/rundown-service/rundownCache.js';
|
|||||||
import { getState } from '../stores/runtimeState.js';
|
import { getState } from '../stores/runtimeState.js';
|
||||||
import { resolveCrashReportDirectory } from '../setup/index.js';
|
import { resolveCrashReportDirectory } from '../setup/index.js';
|
||||||
|
|
||||||
|
import { ensureDirectory } from './fileManagement.js';
|
||||||
/**
|
/**
|
||||||
* Writes a file to the crash report location
|
* Writes a file to the crash report location
|
||||||
* @param fileName
|
* @param fileName
|
||||||
@@ -13,10 +14,12 @@ import { resolveCrashReportDirectory } from '../setup/index.js';
|
|||||||
*/
|
*/
|
||||||
function writeToFile(fileName: string, content: object) {
|
function writeToFile(fileName: string, content: object) {
|
||||||
const path = join(resolveCrashReportDirectory, fileName);
|
const path = join(resolveCrashReportDirectory, fileName);
|
||||||
|
ensureDirectory(resolveCrashReportDirectory);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const textContent = JSON.stringify(content, null, 2);
|
const textContent = JSON.stringify(content, null, 2);
|
||||||
writeFileSync(path, textContent);
|
writeFileSync(path, textContent);
|
||||||
} catch (e_rror) {
|
} catch (_) {
|
||||||
/** We do not handle the error here */
|
/** We do not handle the error here */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { rm } from 'fs/promises';
|
||||||
|
|
||||||
import { ensureDirectory } from './fileManagement.js';
|
import { ensureDirectory } from './fileManagement.js';
|
||||||
import { getAppDataPath, uploadsFolderPath } from '../setup/index.js';
|
import { getAppDataPath, uploadsFolderPath } from '../setup/index.js';
|
||||||
@@ -56,3 +57,11 @@ export const storage = multer.diskStorage({
|
|||||||
cb(null, file.originalname);
|
cb(null, file.originalname);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export async function clearUploadfolder() {
|
||||||
|
try {
|
||||||
|
await rm(uploadsFolderPath, { recursive: true });
|
||||||
|
} catch (_) {
|
||||||
|
//we dont care that there was no folder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules"
|
"cleanup": "rm -rf .turbo && rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deepmerge-ts": "^5.1.0",
|
"deepmerge-ts": "^7.0.3",
|
||||||
"nanoid": "^5.0.4"
|
"nanoid": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^v7.12.0",
|
"@typescript-eslint/eslint-plugin": "^v7.12.0",
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
* 09:00 - 10:00
|
* 09:00 - 10:00
|
||||||
* 09:30 - 10:30
|
* 09:30 - 10:30
|
||||||
*/
|
*/
|
||||||
export function checkIsNextDay(previousStart: number, timeStart: number): boolean {
|
export function checkIsNextDay(previousStart: number, timeStart: number, previousDuration?: number): boolean {
|
||||||
return timeStart <= previousStart;
|
return previousDuration === 0 ? false : timeStart <= previousStart;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+15
-19
@@ -304,11 +304,8 @@ importers:
|
|||||||
specifier: ^1.6.3
|
specifier: ^1.6.3
|
||||||
version: 1.6.3
|
version: 1.6.3
|
||||||
steno:
|
steno:
|
||||||
specifier: ^3.1.0
|
specifier: ^4.0.2
|
||||||
version: 3.2.0
|
version: 4.0.2
|
||||||
ts-essentials:
|
|
||||||
specifier: ^9.4.1
|
|
||||||
version: 9.4.1(typescript@5.4.3)
|
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.13.0
|
specifier: ^8.13.0
|
||||||
version: 8.13.0
|
version: 8.13.0
|
||||||
@@ -364,6 +361,9 @@ importers:
|
|||||||
shx:
|
shx:
|
||||||
specifier: ^0.3.4
|
specifier: ^0.3.4
|
||||||
version: 0.3.4
|
version: 0.3.4
|
||||||
|
ts-essentials:
|
||||||
|
specifier: ^9.4.1
|
||||||
|
version: 9.4.1(typescript@5.4.3)
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.1
|
specifier: ^10.9.1
|
||||||
version: 10.9.1(@types/node@18.11.18)(typescript@5.4.3)
|
version: 10.9.1(@types/node@18.11.18)(typescript@5.4.3)
|
||||||
@@ -392,11 +392,11 @@ importers:
|
|||||||
packages/utils:
|
packages/utils:
|
||||||
dependencies:
|
dependencies:
|
||||||
deepmerge-ts:
|
deepmerge-ts:
|
||||||
specifier: ^5.1.0
|
specifier: ^7.0.3
|
||||||
version: 5.1.0
|
version: 7.0.3
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.7
|
||||||
version: 5.0.4
|
version: 5.0.7
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^v7.12.0
|
specifier: ^v7.12.0
|
||||||
@@ -5106,8 +5106,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/deepmerge-ts@5.1.0:
|
/deepmerge-ts@7.0.3:
|
||||||
resolution: {integrity: sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==}
|
resolution: {integrity: sha512-dxFbFO2RSIhPNBPL/j8Nvdt6/vrkW9+uGf1NLah/QxBGAVbK9fj2fGTO+HwdHpPAyFAsyT9iEn/1SI9SUvespw==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -7547,8 +7547,8 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/nanoid@5.0.4:
|
/nanoid@5.0.7:
|
||||||
resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==}
|
resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
@@ -8796,11 +8796,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==}
|
resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/steno@3.2.0:
|
|
||||||
resolution: {integrity: sha512-zPKkv+LqoYffxrtD0GIVA08DvF6v1dW02qpP5XnERoobq9g3MKcTSBTi08gbGNFMNRo3TQV/6kBw811T1LUhKg==}
|
|
||||||
engines: {node: '>=16'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/steno@4.0.2:
|
/steno@4.0.2:
|
||||||
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
|
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -9108,7 +9103,7 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.4.3
|
typescript: 5.4.3
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/ts-node@10.9.1(@types/node@18.11.18)(typescript@5.4.3):
|
/ts-node@10.9.1(@types/node@18.11.18)(typescript@5.4.3):
|
||||||
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
|
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
|
||||||
@@ -9305,6 +9300,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==}
|
resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ufo@1.3.2:
|
/ufo@1.3.2:
|
||||||
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
|
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
|
||||||
|
|||||||
Reference in New Issue
Block a user