* chore: upgrade dependencies
* feat/mac: draft pipeline
* feat/mac: use public folder for transient files
* feat/mac: set app fullscreen
* feat/mac: cmd + , toggles menu
* feat/mac: update readme
* refact: easier login process
* refact prevent style issues with safari
* refact reduce css download
* style: cleanup paginator design
* style: studio clock is responsive
* style: fix spacing style issues with safari
This commit is contained in:
Carlos Valente
2022-04-17 20:30:39 +02:00
committed by GitHub
parent 20d3ddbc18
commit db0eee3b9c
140 changed files with 10160 additions and 2139 deletions
+6 -6
View File
@@ -5,8 +5,8 @@ import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../utils/url.js';
import getRandomName from '../utils/getRandomName.js';
import { stringFromMillis } from 'ontime-utils/time.js';
import { generateId } from 'ontime-utils/generate_id.js';
import { generateId } from '../utils/generate_id.js';
import { stringFromMillis } from '../utils/time.js';
/*
* Class EventTimer adds functions specific to APP
@@ -764,7 +764,7 @@ export class EventTimer extends Timer {
const numEvents = events.length;
// is this the first event
let first = this.numEvents === 0;
const first = this.numEvents === 0;
// set general
this._eventlist = events;
@@ -822,7 +822,7 @@ export class EventTimer extends Timer {
if (e.id === this.selectedEventId) {
// handle reload selected
// Reload data if running
let type = this.selectedEventId === id && this._startedAt != null ? 'reload' : 'load';
const type = this.selectedEventId === id && this._startedAt != null ? 'reload' : 'load';
this.loadEvent(this.selectedEventIndex, type);
} else if (e.id === this.nextEventId) {
// roll needs to recalculate
@@ -1178,7 +1178,7 @@ export class EventTimer extends Timer {
rollLoad() {
const now = this._getCurrentTime();
let prevLoaded = this.selectedEventId;
const prevLoaded = this.selectedEventId;
// maybe roll has already been loaded
if (this.secondaryTimer === null) {
@@ -1428,7 +1428,7 @@ export class EventTimer extends Timer {
* @param {string} message
* @param {any} [payload]
*/
async sendOsc(message, payload = undefined) {
async sendOsc(message, payload) {
// Todo: add disabled osc check
const reply = await this.osc.send(message, payload);
if (!reply.success) {
+4 -11
View File
@@ -1,11 +1,11 @@
import { stringFromMillis } from '../utils/time.js';
/*
* Timer implements simple countdown timer functions
* User needs to use setup function to be able to use
*
*/
import { stringFromMillis } from 'ontime-utils/time.js';
export class Timer {
constructor() {
this.clock = null;
@@ -62,11 +62,7 @@ export class Timer {
if (this._startedAt != null) {
// update current timer
this.current =
this._startedAt +
this.duration +
this._pausedTotal +
this._pausedInterval -
now;
this._startedAt + this.duration + this._pausedTotal + this._pausedInterval - now;
}
// enable flag
@@ -114,10 +110,7 @@ export class Timer {
if (this._finishedAt) return this._finishedAt;
return Math.max(
this._startedAt +
this.duration +
this._pausedInterval +
this._pausedTotal,
this._startedAt + this.duration + this._pausedInterval + this._pausedTotal,
this._startedAt
);
}
+6 -17
View File
@@ -10,8 +10,7 @@ export const DAY_TO_MS = 86400000;
* @param {number} end - When does the event end
* @returns {number} normalised time
*/
export const normaliseEndTime = (start, end) =>
end < start ? end + DAY_TO_MS : end;
export const normaliseEndTime = (start, end) => (end < start ? end + DAY_TO_MS : end);
/**
* @description Sorts an array of objects by given property
@@ -34,7 +33,7 @@ export const sortArrayByProperty = (arr, property) => {
*/
export const replacePlaceholder = (str, values) => {
for (let [k, v] of Object.entries(values)) {
for (const [k, v] of Object.entries(values)) {
str = str.replace(k, v);
}
return str;
@@ -71,10 +70,7 @@ export const getSelectionByRoll = (arr, now) => {
// exit early if we are past the events
const lastEvent = orderedEvents[orderedEvents.length - 1];
const lastNormalEnd = normaliseEndTime(
lastEvent.timeStart,
lastEvent.timeEnd
);
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd);
if (now > lastNormalEnd) {
return {
nowIndex,
@@ -163,14 +159,8 @@ export const getSelectionByRoll = (arr, now) => {
* @returns {object} object with selection variables
*/
export const updateRoll = (currentTimers) => {
const {
selectedEventId,
current,
_finishAt,
clock,
secondaryTimer,
_secondaryTarget,
} = currentTimers;
const { selectedEventId, current, _finishAt, clock, secondaryTimer, _secondaryTarget } =
currentTimers;
// timers
let updatedTimer = current;
@@ -200,8 +190,7 @@ export const updateRoll = (currentTimers) => {
// a) we just finished an event (finished was set to true)
// b) we need to look for events
// this could be caused by a secondary timer or event finished
const secondaryRunning =
updatedSecondaryTimer <= 0 && updatedSecondaryTimer != null;
const secondaryRunning = updatedSecondaryTimer <= 0 && updatedSecondaryTimer != null;
if (isFinished || secondaryRunning) {
// look for events
+3 -9
View File
@@ -68,10 +68,7 @@ export const initiateOSC = (config) => {
try {
const t = parseInt(args);
if (isNaN(t)) {
global.timer.error(
'RX',
`OSC IN: delay time not recognised ${args}`
);
global.timer.error('RX', `OSC IN: delay time not recognised ${args}`);
return;
}
global.timer.increment(t * 1000 * 60);
@@ -82,7 +79,7 @@ export const initiateOSC = (config) => {
case 'goto':
try {
const eventIndex = parseInt(args);
if (isNaN(eventIndex) || eventIndex <= 0 || eventIndex == null) {
if (isNaN(eventIndex) || eventIndex <= 0) {
global.timer.error(
'RX',
`OSC IN: event index not recognised or out of range ${eventIndex}`
@@ -96,10 +93,7 @@ export const initiateOSC = (config) => {
case 'gotoid':
console.log('calling gotoid with', args);
if (args == null) {
global.timer.error(
'RX',
`OSC IN: event id not recognised or out of range ${args}}`
);
global.timer.error('RX', `OSC IN: event id not recognised or out of range ${args}}`);
return;
}
try {
+8 -8
View File
@@ -2,20 +2,20 @@
import { data, db } from '../app.js';
// utils
import { generateId } from 'ontime-utils/generate_id.js';
import {
block as blockDef,
delay as delayDef,
event as eventDef,
} from '../models/eventsDefinition.js';
import { generateId } from '../utils/generate_id.js';
const MAX_EVENTS = 99;
async function _insertAt(entry, index) {
// get events
let events = data.events;
let count = events.length;
let order = entry.order;
const events = data.events;
const count = events.length;
const order = entry.order;
// Remove order field from object
delete entry.order;
@@ -146,7 +146,7 @@ export const eventsPut = async (req, res) => {
return;
}
let eventId = req.body.id;
const eventId = req.body.id;
if (!eventId) {
res.status(400).send(`Object malformed: id missing`);
return;
@@ -191,8 +191,8 @@ export const eventsReorder = async (req, res) => {
const { index, from, to } = req.body;
// get events
let events = data.events;
let idx = events.findIndex((e) => e.id === index, from);
const events = data.events;
const idx = events.findIndex((e) => e.id === index, from);
// Check if item is at given index
if (idx !== from) {
@@ -233,7 +233,7 @@ export const eventsApplyDelay = async (req, res) => {
try {
// get events
let events = data.events;
const events = data.events;
// AUX
let delayIndex = null;
+1 -1
View File
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
import { data, db } from '../app.js';
import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js';
import { generateId } from 'ontime-utils/generate_id.js';
import { generateId } from '../utils/generate_id.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
View File
-1
View File
@@ -11,7 +11,6 @@
"nanoid": "^3.2.0",
"node-osc": "6.1.12",
"node-xlsx": "^0.21.0",
"ontime-utils": "link: ../server/utils/",
"passport": "^0.5.2",
"passport-local": "~1.0.0",
"socket.io": "^4.4.1",
@@ -0,0 +1,28 @@
import { generateId } from '../generate_id.js';
test('generate a valid 5 digit id', () => {
const id = generateId();
expect(id.length).toBe(5);
});
test('generate 100 with less than 110 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 100) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(105);
});
describe('generate 1000 with less than 1020 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 1000) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(1020);
});
+4
View File
@@ -0,0 +1,4 @@
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('1234567890abcdef', 5);
export const generateId = () => nanoid();
+69 -69
View File
@@ -2,8 +2,6 @@ import fs from 'fs';
import xlsx from 'node-xlsx';
import { event as eventDef } from '../models/eventsDefinition.js';
import { dbModelv1 } from '../models/dataModel.js';
import { generateId } from 'ontime-utils/generate_id.js';
import { excelDateStringToMillis } from 'ontime-utils/time.js';
import { deleteFile, makeString, validateDuration } from './parserUtils.js';
import {
parseAliases_v1,
@@ -14,86 +12,25 @@ import {
parseSettings_v1,
parseUserFields_v1,
} from './parserUtils_v1.js';
import { excelDateStringToMillis } from './time.js';
import { generateId } from './generate_id.js';
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
export const JSON_MIME = 'application/json';
export const MAX_EVENTS = 99;
/**
* @description Middleware function that checks file type and calls relevant parser
* @param {string} file - reference to file
* @return {object} - parse result message
*/
export const fileHandler = async (file) => {
let res = {};
// check which file type are we dealing with
if (file.endsWith('.xlsx')) {
try {
const excelData = xlsx
.parse(file, { cellDates: true })
.find(
({ name }) => name.toLowerCase() === 'ontime' || name.toLowerCase() === 'event schedule'
);
// we only look at worksheets called ontime or event schedule
if (excelData?.data) {
const dataFromExcel = await parseExcel_v1(excelData.data);
res.data = await parseJson_v1(dataFromExcel);
res.message = 'success';
} else {
console.log('Error: No sheets found named ontime or event schedule');
res = {
error: true,
message: `No sheets found named ontime or event schedule`,
};
}
} catch (error) {
res = { error: true, message: `Error parsing file: ${error}` };
}
}
if (file.endsWith('.json')) {
// if json check version
const rawdata = fs.readFileSync(file);
let uploadedJson = null;
try {
uploadedJson = JSON.parse(rawdata);
} catch (error) {
return { error: true, message: 'Error parsing JSON file' };
}
if (uploadedJson.settings.version === 1) {
try {
res.data = await parseJson_v1(uploadedJson);
res.message = 'success';
} catch (error) {
res = { error: true, message: `Error parsing file: ${error}` };
}
} else {
res = { error: true, message: 'Error parsing file, version unknown' };
}
}
// delete file
await deleteFile(file);
return res;
};
/**
* @description Excel array parser
* @param {array} excelData - array with excel sheet
* @returns {object} - parsed object
*/
export const parseExcel_v1 = async (excelData) => {
let eventData = {
const eventData = {
title: '',
url: '',
};
let customUserFields = {};
let events = [];
const customUserFields = {};
const events = [];
let timeStartIndex = null;
let timeEndIndex = null;
let titleIndex = null;
@@ -291,7 +228,7 @@ export const parseJson_v1 = async (jsonData, enforce = false) => {
}
// object containing the parsed data
let returnData = {};
const returnData = {};
// parse Events
returnData.events = parseEvents_v1(jsonData);
@@ -366,3 +303,66 @@ export const validateEvent_v1 = (eventArgs) => {
return event;
};
/**
* @description Middleware function that checks file type and calls relevant parser
* @param {string} file - reference to file
* @return {object} - parse result message
*/
export const fileHandler = async (file) => {
let res = {};
// check which file type are we dealing with
if (file.endsWith('.xlsx')) {
try {
const excelData = xlsx
.parse(file, { cellDates: true })
.find(
({ name }) => name.toLowerCase() === 'ontime' || name.toLowerCase() === 'event schedule'
);
// we only look at worksheets called ontime or event schedule
if (excelData?.data) {
const dataFromExcel = await parseExcel_v1(excelData.data);
res.data = await parseJson_v1(dataFromExcel);
res.message = 'success';
} else {
console.log('Error: No sheets found named ontime or event schedule');
res = {
error: true,
message: `No sheets found named ontime or event schedule`,
};
}
} catch (error) {
res = { error: true, message: `Error parsing file: ${error}` };
}
}
if (file.endsWith('.json')) {
// if json check version
const rawdata = fs.readFileSync(file);
let uploadedJson = null;
try {
uploadedJson = JSON.parse(rawdata);
} catch (error) {
return { error: true, message: 'Error parsing JSON file' };
}
if (uploadedJson.settings.version === 1) {
try {
res.data = await parseJson_v1(uploadedJson);
res.message = 'success';
} catch (error) {
res = { error: true, message: `Error parsing file: ${error}` };
}
} else {
res = { error: true, message: 'Error parsing file, version unknown' };
}
}
// delete file
await deleteFile(file);
return res;
};
+1 -1
View File
@@ -9,7 +9,7 @@ import fs from 'fs';
export const makeString = (val, fallback = '') => {
if (typeof val === 'string') return val;
else if (val == null || val.constructor === Object) return fallback;
else return val.toString();
return val.toString();
};
/**
+10 -10
View File
@@ -1,7 +1,7 @@
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
import { generateId } from 'ontime-utils/generate_id.js';
import { dbModelv1 } from '../models/dataModel.js';
import { MAX_EVENTS, validateEvent_v1 } from './parser.js';
import { generateId } from './generate_id.js';
/**
* Parse events array of an entry
@@ -12,8 +12,8 @@ export const parseEvents_v1 = (data) => {
let newEvents = [];
if ('events' in data) {
console.log('Found events definition, importing...');
let events = [];
let ids = [];
const events = [];
const ids = [];
for (const e of data.events) {
// cap number of events
if (events.length >= MAX_EVENTS) {
@@ -28,7 +28,7 @@ export const parseEvents_v1 = (data) => {
}
if (e.type === 'event') {
let event = validateEvent_v1(e);
const event = validateEvent_v1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
@@ -94,7 +94,7 @@ export const parseSettings_v1 = (data, enforce) => {
if (s.app == null || s.version == null) {
console.log('ERROR: unknown app version, skipping');
} else {
let settings = {
const settings = {
lock: s.lock || null,
pinCode: s.pinCode || null,
};
@@ -123,12 +123,12 @@ export const parseOsc_v1 = (data, enforce) => {
if ('osc' in data) {
console.log('Found OSC definition, importing...');
const s = data.osc;
let osc = {};
const osc = {};
if (s.port) osc.port = s.port;
if (s.portOut) osc.portOut = s.portOut;
if (s.targetIP) osc.targetIP = s.targetIP;
if (s.enabled !== undefined) osc.enabled = s.enabled;
if (typeof s.enabled !== 'undefined') osc.enabled = s.enabled;
// write to db
newOsc = {
@@ -149,7 +149,7 @@ export const parseOsc_v1 = (data, enforce) => {
* @returns {object} - event object data
*/
export const parseHttp_v1 = (data, enforce) => {
let newHttp = {};
const newHttp = {};
if ('http' in data) {
console.log('Found HTTP definition, importing...');
const h = data.osc;
@@ -207,13 +207,13 @@ export const parseAliases_v1 = (data) => {
* @returns {object} - event object data
*/
export const parseUserFields_v1 = (data) => {
let newUserFields = dbModelv1.userFields;
const newUserFields = dbModelv1.userFields;
if ('userFields' in data) {
console.log('Found User Fields definition, importing...');
// we will only be importing the fields we know, so look for that
let fieldsFound = 0;
for (let n in newUserFields) {
for (const n in newUserFields) {
if (n in data.userFields) {
fieldsFound++;
newUserFields[n] = data.userFields[n];
+2 -7
View File
@@ -27,12 +27,7 @@ export const nowInMillis = () => {
* @returns {string} String representing time 00:12:02
*/
export const stringFromMillis = (
ms,
showSeconds = true,
delim = ':',
ifNull = '...'
) => {
export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
if (ms == null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : '';
const millis = Math.abs(ms);
@@ -57,7 +52,7 @@ export const stringFromMillis = (
export const excelDateStringToMillis = (excelDate) => {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date)) {
const h = date.getUTCHours();
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
+38 -12
View File
@@ -1,26 +1,52 @@
import multer from 'multer';
import { statSync, mkdirSync } from 'fs';
import { existsSync, mkdirSync } from 'fs';
import * as path from 'path';
import { EXCEL_MIME, JSON_MIME } from './parser.js';
/**
* @description Returns public path depending on os
* @return {string|*}
*/
function getAppDataPath() {
switch (process.platform) {
case 'darwin': {
return path.join(process.env.HOME, 'Library', 'Application Support', 'Ontime');
}
case 'win32': {
return path.join(process.env.APPDATA, 'Ontime');
}
case 'linux': {
return path.join(process.env.HOME, '.Ontime');
}
default: {
return '';
}
}
}
// Define multer storage object
const storage = multer.diskStorage({
destination: function (req, file, cb) {
let newDestination = 'uploads/';
let stat = null;
try {
stat = statSync(newDestination);
} catch (err) {
mkdirSync(newDestination);
// get platform path
const appDataPath = getAppDataPath();
if (appDataPath === '') {
throw new Error('Could not resolve public folder for platform');
}
if (stat && !stat.isDirectory()) {
throw new Error(
`Directory cannot be created because an inode of a different type exists at ${newDestination}`
);
// append uploads folder
const newDestination = path.join(appDataPath, 'uploads');
// Create directory if not exist
if (!existsSync(newDestination)) {
try {
mkdirSync(newDestination);
} catch (err) {
throw new Error('Could not create directory');
}
}
cb(null, newDestination);
},
filename: function (req, file, cb) {
cb(null, Date.now() + '--' + file.originalname);
cb(null, `${Date.now()}--${file.originalname}`);
},
});
+4 -6
View File
@@ -3,8 +3,7 @@
* @param {string} url - URL to be checked
* @returns {string} Sanitized url
*/
export const cleanURL = (url) => {
export const cleanURL = (url) => {
// trim whitespaces
let r = url.trim();
@@ -12,10 +11,9 @@ export const cleanURL = (url) => {
r = r.split(' ').join('%20');
// contain only allowed characters
r = r.replace(/([^\x00-\x7F]|[@\s<>\[\]{}|\\^])+/g, '')
r = r.replace(/([@\s<>[\]{}|\\^])+/g, '');
// starts with http://
if (!r.startsWith('http://')) r = `http://${r}`
if (!r.startsWith('http://')) r = `http://${r}`;
return r;
}
};
-4
View File
@@ -1103,10 +1103,6 @@ once@^1.3.0:
dependencies:
wrappy "1"
"ontime-utils@link: ../server/utils":
version "0.0.0"
uid ""
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"