mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-12 07:23:23 -05:00
Major refactoring of UI code, small UI changes. (#48)
* Major refactoring of UI code, small UI changes. * Single file index.js split up into separate modules * Modules for handling UI view components * Modules for handling JSON/Model data * Modules for support tasks * Module to encapsulate Moonfire API * Main application module * index.js simplified to just activating main app * Settings file functionality expanded * UI adds "Time Format" popup to allow changing time representation * CSS changes/additions to streamline looks * Recordings loading indicator only appears after 500ms delay, if at all * Address first set of PR change requests from Scott. * Add copyright headers to all files (except JSON files) * Fix bug with entering time values in range pickers * Fixed an erroneous comment and/or spelling error here and there * Fixed JSDoc comments where [description] was not filled in * Removed a TODO from NVRApplication as it no longer applies * Fixed bug handling "infinite" case of video segment lengths * Fixed bug in "trim" handler and trim execution * Retrofit video continues loading from separate PR Signed-off-by: Dolf Starreveld <dolf@starreveld.com> * Address PR comments Signed-off-by: Dolf Starreveld <dolf@starreveld.com> * Address PR comments Signed-off-by: Dolf Starreveld <dolf@starreveld.com>
This commit is contained in:
parent
caac324bd5
commit
58152e8d94
9
.eslintrc.json
Normal file
9
.eslintrc.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"extends": "google",
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@ cameras.sql
|
|||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
*.swp
|
*.swp
|
||||||
|
*.sublime-workspace
|
||||||
node_modules
|
node_modules
|
||||||
prep.config
|
prep.config
|
||||||
settings-nvr-local.js
|
settings-nvr-local.js
|
||||||
|
43
.jsbeautifyrc
Normal file
43
.jsbeautifyrc
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"indent_size": 2,
|
||||||
|
"indent_char": " ",
|
||||||
|
"indent_with_tabs": false,
|
||||||
|
"eol": "\n",
|
||||||
|
"end_with_newline": true,
|
||||||
|
"indent_level": 0,
|
||||||
|
"preserve_newlines": true,
|
||||||
|
"max_preserve_newlines": 4,
|
||||||
|
"space_in_paren": false,
|
||||||
|
"space_in_empty_paren": false,
|
||||||
|
"jslint_happy": false,
|
||||||
|
"space_after_anon_function": false,
|
||||||
|
"brace_style": "collapse,preserve-inline",
|
||||||
|
"unindent_chained_methods": false,
|
||||||
|
"break_chained_methods": false,
|
||||||
|
"keep_array_indentation": false,
|
||||||
|
"unescape_strings": false,
|
||||||
|
"wrap_line_length": 0,
|
||||||
|
"e4x": false,
|
||||||
|
"comma_first": false,
|
||||||
|
"operator_position": "before-newline",
|
||||||
|
"js": {
|
||||||
|
"indent_size": 2,
|
||||||
|
"indent_char": " ",
|
||||||
|
"indent_with_tabs": false
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"brace_style": "expand",
|
||||||
|
"keep_array_indentation": false,
|
||||||
|
"unescape_strings": false
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"package?(-lock).json": {
|
||||||
|
"indent_size": 2,
|
||||||
|
"brace_style": "collapse"
|
||||||
|
},
|
||||||
|
"*.sublime-@(settings|keymap|commands|menu)": {
|
||||||
|
"indent_size": 4,
|
||||||
|
"brace_style": "expand"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
.prettierrc.json
Normal file
37
.prettierrc.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"parser": "babylon",
|
||||||
|
"semi": true,
|
||||||
|
"requirePragma": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.js"],
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semi": true,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.json", "moonfire.sublime-project"],
|
||||||
|
"options": {
|
||||||
|
"parser": "json",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
42
moonfire.sublime-project
Normal file
42
moonfire.sublime-project
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".",
|
||||||
|
"folder_exclude_patterns": [
|
||||||
|
"design",
|
||||||
|
"ffmpeg",
|
||||||
|
"node_modules",
|
||||||
|
"ui-dist"
|
||||||
|
],
|
||||||
|
"file_exclude_patterns": ["*.png", "*.jpg", "*.gif"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"js_prettier": {
|
||||||
|
"additional_cli_args": {
|
||||||
|
"--config-precedence": "prefer-file"
|
||||||
|
},
|
||||||
|
"debug": true,
|
||||||
|
"prettier_cli_path": "",
|
||||||
|
"node_path": "",
|
||||||
|
"auto_format_on_save": false,
|
||||||
|
"auto_format_on_save_excludes": [],
|
||||||
|
"auto_format_on_save_requires_prettier_config": false,
|
||||||
|
"allow_inline_formatting": false,
|
||||||
|
"custom_file_extensions": [],
|
||||||
|
"max_file_size_limit": -1,
|
||||||
|
"prettier_options": {
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"parser": "babylon",
|
||||||
|
"semi": true,
|
||||||
|
"requirePragma": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,10 +23,16 @@
|
|||||||
"babel-minify-webpack-plugin": "^0.3.0",
|
"babel-minify-webpack-plugin": "^0.3.0",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.6.1",
|
||||||
"clean-webpack-plugin": "^0.1.18",
|
"clean-webpack-plugin": "^0.1.18",
|
||||||
|
"compression-webpack-plugin": "^1.1.10",
|
||||||
"css-loader": "^0.28.10",
|
"css-loader": "^0.28.10",
|
||||||
|
"eslint": "^4.18.2",
|
||||||
|
"eslint-config-google": "^0.9.1",
|
||||||
"file-loader": "^1.1.11",
|
"file-loader": "^1.1.11",
|
||||||
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.0.6",
|
"html-webpack-plugin": "^3.0.6",
|
||||||
|
"prettier": "1.11.1",
|
||||||
"style-loader": "^0.19.0",
|
"style-loader": "^0.19.0",
|
||||||
|
"uglifyjs-webpack-plugin": "^1.2.3",
|
||||||
"webpack": "^4.0.1",
|
"webpack": "^4.0.1",
|
||||||
"webpack-cli": "^2.0.10",
|
"webpack-cli": "^2.0.10",
|
||||||
"webpack-dev-server": "^3.1.0",
|
"webpack-dev-server": "^3.1.0",
|
||||||
|
@ -1,5 +1,34 @@
|
|||||||
// vim: set et ts=2 sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
//
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module must export a map, but can use a function with no arguments
|
* This module must export a map, but can use a function with no arguments
|
||||||
|
306
ui-src/NVRApplication.js
Normal file
306
ui-src/NVRApplication.js
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// TODO: test abort.
|
||||||
|
// TODO: add error bar on fetch failure.
|
||||||
|
// TODO: live updating.
|
||||||
|
|
||||||
|
import 'jquery-ui/themes/base/button.css';
|
||||||
|
import 'jquery-ui/themes/base/core.css';
|
||||||
|
import 'jquery-ui/themes/base/datepicker.css';
|
||||||
|
import 'jquery-ui/themes/base/dialog.css';
|
||||||
|
import 'jquery-ui/themes/base/resizable.css';
|
||||||
|
import 'jquery-ui/themes/base/theme.css';
|
||||||
|
import 'jquery-ui/themes/base/tooltip.css';
|
||||||
|
|
||||||
|
// This causes our custom css to be loaded after the above!
|
||||||
|
require('./assets/index.css');
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
import 'jquery-ui/ui/widgets/datepicker';
|
||||||
|
import 'jquery-ui/ui/widgets/dialog';
|
||||||
|
import 'jquery-ui/ui/widgets/tooltip';
|
||||||
|
|
||||||
|
import Camera from './lib/models/Camera';
|
||||||
|
import CameraView from './lib/views/CameraView';
|
||||||
|
import CalendarView from './lib/views/CalendarView';
|
||||||
|
import NVRSettingsView from './lib/views/NVRSettingsView';
|
||||||
|
import CheckboxGroupView from './lib/views/CheckboxGroupView';
|
||||||
|
import RecordingFormatter from './lib/support/RecordingFormatter';
|
||||||
|
import TimeFormatter, {
|
||||||
|
TimeStamp90kFormatter,
|
||||||
|
} from './lib/support/TimeFormatter';
|
||||||
|
import MoonfireAPI from './lib/MoonfireAPI';
|
||||||
|
|
||||||
|
const api = new MoonfireAPI();
|
||||||
|
let cameraViews = null; // CameraView objects
|
||||||
|
let calendarView = null; // CalendarView object
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently selected time format specification.
|
||||||
|
*
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
let timeFmt = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently active time formatter.
|
||||||
|
* This is lazy initialized at the point we receive the timezone information
|
||||||
|
* and never changes afterwards, except possibly for changing the timezone.
|
||||||
|
*
|
||||||
|
* @type {[type]}
|
||||||
|
*/
|
||||||
|
let timeFormatter = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently active time formatter for internal time format.
|
||||||
|
* This is lazy initialized at the point we receive the timezone information
|
||||||
|
* and never changes afterwards, except possibly for changing the timezone.
|
||||||
|
*
|
||||||
|
* @type {[type]}
|
||||||
|
*/
|
||||||
|
let timeFormatter90k = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globally set a new timezone for the app.
|
||||||
|
*
|
||||||
|
* @param {String} timeZone Timezone name
|
||||||
|
*/
|
||||||
|
function newTimeZone(timeZone) {
|
||||||
|
timeFormatter = new TimeFormatter(timeFmt, timeZone);
|
||||||
|
timeFormatter90k = new TimeStamp90kFormatter(timeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globally set a new time format for the app.
|
||||||
|
*
|
||||||
|
* @param {String} format Time format specification
|
||||||
|
*/
|
||||||
|
function newTimeFormat(format) {
|
||||||
|
timeFormatter = new TimeFormatter(format, timeFormatter.tz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for clicking on a video.
|
||||||
|
*
|
||||||
|
* A 'dialog' object is attached to the body of the dom and it
|
||||||
|
* properly initialized with the corrcet src url.
|
||||||
|
*
|
||||||
|
* @param {NVRSettings} nvrSettingsView NVRSettingsView in effect
|
||||||
|
* @param {object} camera Object for the camera
|
||||||
|
* @param {object} range Range Object
|
||||||
|
* @param {object} recording Recording object
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
function onSelectVideo(nvrSettingsView, camera, range, recording) {
|
||||||
|
console.log('Recording clicked: ', recording);
|
||||||
|
const trimmedRange = recording.range90k(nvrSettingsView.trim ? range : null);
|
||||||
|
const url = api.videoPlayUrl(
|
||||||
|
camera.uuid,
|
||||||
|
recording,
|
||||||
|
trimmedRange,
|
||||||
|
nvrSettingsView.timeStampTrack
|
||||||
|
);
|
||||||
|
const video = $('<video controls preload="auto" autoplay="true" />');
|
||||||
|
const dialog = $('<div class="playdialog" />').append(video);
|
||||||
|
$('body').append(dialog);
|
||||||
|
|
||||||
|
let [formattedStart, formattedEnd] = timeFormatter90k.formatSameDayShortened(
|
||||||
|
trimmedRange.startTime90k,
|
||||||
|
trimmedRange.endTime90k
|
||||||
|
);
|
||||||
|
dialog.dialog({
|
||||||
|
title: camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd,
|
||||||
|
width: recording.videoSampleEntryWidth / 4,
|
||||||
|
close: () => {
|
||||||
|
const videoDOMElement = video[0];
|
||||||
|
videoDOMElement.pause();
|
||||||
|
videoDOMElement.src = ''; // Remove current source to stop loading
|
||||||
|
dialog.remove();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Now that dialog is up, set the src so video starts
|
||||||
|
console.log('Video url: ' + url);
|
||||||
|
video.attr('src', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch camera view data for a given date/time range.
|
||||||
|
*
|
||||||
|
* @param {Range90k} selectedRange Desired time range
|
||||||
|
* @param {Number} videoLength Desired length of video segments, or Infinity
|
||||||
|
*/
|
||||||
|
function fetch(selectedRange, videoLength) {
|
||||||
|
if (selectedRange.startTime90k === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
'Fetching> ' +
|
||||||
|
selectedRange.formatTimeStamp90k(selectedRange.startTime90k) +
|
||||||
|
' to ' +
|
||||||
|
selectedRange.formatTimeStamp90k(selectedRange.endTime90k)
|
||||||
|
);
|
||||||
|
for (let cameraView of cameraViews) {
|
||||||
|
let url = api.recordingsUrl(
|
||||||
|
cameraView.camera.uuid,
|
||||||
|
selectedRange.startTime90k,
|
||||||
|
selectedRange.endTime90k,
|
||||||
|
videoLength
|
||||||
|
);
|
||||||
|
if (cameraView.recordingsReq !== null) {
|
||||||
|
/*
|
||||||
|
* If there is another request, it would be because settings changed
|
||||||
|
* and so an abort is to make room for this new request, now necessary
|
||||||
|
* for the changed situation.
|
||||||
|
*/
|
||||||
|
cameraView.recordingsReq.abort();
|
||||||
|
}
|
||||||
|
cameraView.delayedShowLoading(500);
|
||||||
|
let r = api.request(url);
|
||||||
|
cameraView.recordingsUrl = url;
|
||||||
|
cameraView.recordingsReq = r;
|
||||||
|
cameraView.recordingsRange = selectedRange.range90k();
|
||||||
|
r.always(function() {
|
||||||
|
cameraView.recordingsReq = null;
|
||||||
|
});
|
||||||
|
r
|
||||||
|
.then(function(data, status, req) {
|
||||||
|
// Sort recordings in descending order.
|
||||||
|
data.recordings.sort(function(a, b) {
|
||||||
|
return b.startId - a.startId;
|
||||||
|
});
|
||||||
|
console.log('Fetched results > updating recordings');
|
||||||
|
cameraView.recordingsJSON = data.recordings;
|
||||||
|
})
|
||||||
|
.catch(function(data, status, err) {
|
||||||
|
console.log(url, ' load failed: ', status, ': ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the page after receiving camera data.
|
||||||
|
*
|
||||||
|
* Sets the following globals:
|
||||||
|
* zone - timezone from data received
|
||||||
|
* cameraViews - array of views, one per camera
|
||||||
|
*
|
||||||
|
* Builds the dom for the left side controllers
|
||||||
|
*
|
||||||
|
* @param {Object} data JSON resulting from the main API request /api/?days=
|
||||||
|
*/
|
||||||
|
function onReceivedCameras(data) {
|
||||||
|
newTimeZone(data.timeZoneName);
|
||||||
|
|
||||||
|
// Set up controls and values
|
||||||
|
const nvrSettingsView = new NVRSettingsView();
|
||||||
|
nvrSettingsView.onVideoLengthChange = (vl) =>
|
||||||
|
fetch(calendarView.selectedRange, vl);
|
||||||
|
nvrSettingsView.onTimeFormatChange = (format) =>
|
||||||
|
cameraViews.forEach((view) => (view.timeFormat = format));
|
||||||
|
nvrSettingsView.onTrimChange = (t) =>
|
||||||
|
cameraViews.forEach((view) => (view.trimmed = t));
|
||||||
|
newTimeFormat(nvrSettingsView.timeFormatString);
|
||||||
|
|
||||||
|
calendarView = new CalendarView({timeZone: timeFormatter.tz});
|
||||||
|
calendarView.onRangeChange = (selectedRange) =>
|
||||||
|
fetch(selectedRange, nvrSettingsView.videoLength);
|
||||||
|
|
||||||
|
const camerasParent = $('#cameras');
|
||||||
|
const videos = $('#videos');
|
||||||
|
|
||||||
|
cameraViews = data.cameras.map((cameraJson) => {
|
||||||
|
const camera = new Camera(cameraJson);
|
||||||
|
const cv = new CameraView(
|
||||||
|
camera,
|
||||||
|
new RecordingFormatter(timeFormatter.formatStr, timeFormatter.tz),
|
||||||
|
nvrSettingsView.trim,
|
||||||
|
videos
|
||||||
|
);
|
||||||
|
cv.onRecordingClicked = (recordingModel) => {
|
||||||
|
console.log('Recording clicked', recordingModel);
|
||||||
|
onSelectVideo(
|
||||||
|
nvrSettingsView,
|
||||||
|
camera,
|
||||||
|
calendarView.selectedRange,
|
||||||
|
recordingModel
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return cv;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create camera enable checkboxes
|
||||||
|
const cameraCheckBoxes = new CheckboxGroupView(
|
||||||
|
cameraViews.map((cv) => ({
|
||||||
|
id: cv.camera.uuid,
|
||||||
|
checked: true,
|
||||||
|
text: cv.camera.shortName,
|
||||||
|
camView: cv,
|
||||||
|
})),
|
||||||
|
camerasParent
|
||||||
|
);
|
||||||
|
cameraCheckBoxes.onCheckChange = (groupEl) => {
|
||||||
|
groupEl.camView.enabled = groupEl.checked;
|
||||||
|
calendarView.initializeWith(cameraViews);
|
||||||
|
};
|
||||||
|
|
||||||
|
calendarView.initializeWith(cameraViews);
|
||||||
|
|
||||||
|
console.log('Loaded: ' + cameraViews.length + ' camera views');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing the entire application.
|
||||||
|
*/
|
||||||
|
export default class NVRApplication {
|
||||||
|
/**
|
||||||
|
* Construct the application object.
|
||||||
|
*/
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the application.
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
api
|
||||||
|
.request(api.nvrUrl(true))
|
||||||
|
.done((data) => onReceivedCameras(data))
|
||||||
|
.fail((req, status, err) => {
|
||||||
|
console.log('NVR load error: ', status, err);
|
||||||
|
onReceivedCameras({cameras: []});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log('NVR load exception: ', e);
|
||||||
|
onReceivedCameras({cameras: []});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
76
ui-src/assets/index.css
Normal file
76
ui-src/assets/index.css
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
#nav {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.ui-datepicker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#videos {
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
#videos tbody:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 3ex;
|
||||||
|
}
|
||||||
|
table.videos {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
tbody tr.name {
|
||||||
|
font-size: 110%;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
tbody tr.name th {
|
||||||
|
border-bottom: 1px solid #666;
|
||||||
|
}
|
||||||
|
tbody tr.hdr {
|
||||||
|
color: #555;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
tr.r:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
tr.r td {
|
||||||
|
font-size: 80%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
tr.r th,
|
||||||
|
tr.r td {
|
||||||
|
border: 0;
|
||||||
|
padding: 0.5ex 1.5em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
fieldset legend {
|
||||||
|
font-size: 120%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#from, #to {
|
||||||
|
padding-right: 0.75em;
|
||||||
|
}
|
||||||
|
#st {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#range {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
.ui-dialog .ui-dialog-content {
|
||||||
|
overflow: visible; /* remove stupid scroll bars when resizing. */
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
76
ui-src/assets/index.html
Normal file
76
ui-src/assets/index.html
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- vim: set et: -->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Moonfire NVR</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="nav">
|
||||||
|
<form action="#">
|
||||||
|
<fieldset id="cameras">
|
||||||
|
<legend>Cameras</legend>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset id="datetime">
|
||||||
|
<legend>Date & Time Range</legend>
|
||||||
|
<div id="from">
|
||||||
|
<div id="start-date"></div>
|
||||||
|
<div id="st">
|
||||||
|
<label for="start-time">Time:</label>
|
||||||
|
<input id="start-time" name="start-time" type="text" title="Starting
|
||||||
|
time within the day. Blank for the beginning of the day. Otherwise
|
||||||
|
HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a second.
|
||||||
|
Timezone is normally left out; it's useful once a year during the
|
||||||
|
ambiguous times of the "fall back" hour."></div>
|
||||||
|
</div>
|
||||||
|
<div id="range">Range:
|
||||||
|
<input type="radio" name="end-date-type" id="end-date-same" checked>
|
||||||
|
<label for="end-date-same">Single Day</label>
|
||||||
|
<input type="radio" name="end-date-type" id="end-date-other">
|
||||||
|
<label for="end-date-other">Multi Day</label><br/>
|
||||||
|
</div>
|
||||||
|
<div id="to">
|
||||||
|
<div id="end-date"></div>
|
||||||
|
<label for="start-time">Time:</label>
|
||||||
|
<input id="end-time" name="end-time" type="text" title="Ending
|
||||||
|
time within the day. Blank for the end of the day. Otherwise
|
||||||
|
HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a second.
|
||||||
|
Timezone is normally left out; it's useful once a year during the
|
||||||
|
ambiguous times of the "fall back" hour.">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Recordings Display</legend>
|
||||||
|
<label for="split">Max Video Duration:</label>
|
||||||
|
<select name="split" id="split">
|
||||||
|
<option value="324000000">1 hour</option>
|
||||||
|
<option value="1296000000">4 hours</option>
|
||||||
|
<option value="7776000000">24 hours</option>
|
||||||
|
<option value="infinite">infinite</option>
|
||||||
|
</select><br>
|
||||||
|
<input type="checkbox" checked id="trim" name="trim">
|
||||||
|
<label for="trim">Trim Segment Start & End</label><br>
|
||||||
|
<input type="checkbox" checked id="ts" name="ts">
|
||||||
|
<label for="ts">Timestamp Track</label><br>
|
||||||
|
<label for="timefmt">Time Format:</label>
|
||||||
|
<select name="timefmt" id="timefmt">
|
||||||
|
<option value="MM/DD/YY hh:mm A">US-short</option>
|
||||||
|
<option value="MM/DD/YYYY hh:mm:ss A">US</option>
|
||||||
|
<option value="MM/DD/YY HH:mm" selected>Military-short</option>
|
||||||
|
<option value="MM/DD/YYYY HH:mm:ss">Military</option>
|
||||||
|
<option value="DD.MM.YY HH:mm">EU-short</option>
|
||||||
|
<option value="DD-MM-YYYY HH:mm:ss">EU</option>
|
||||||
|
<option value="YY-MM-DD hh:mm A">ISO-short (12h)</option>
|
||||||
|
<option value="YY-MM-DD HH:mm">ISO-short (24h)</option>
|
||||||
|
<option value="YYYY-MM-DD hh:mm:ss A">ISO (12h)</option>
|
||||||
|
<option value="YYYY-MM-DD HH:mm:ss">ISO (24h)</option>
|
||||||
|
<option value="YYYY-MM-DD HH:mm:ss">ISO 8601-like (No TZ)</option>
|
||||||
|
<option value="YYYY-MM-DDTHH:mm:ss">ISO 8601 (No TZ)</option>
|
||||||
|
<option value="YYYY-MM-DDTHH:mm:ssZ">ISO 8601</option>
|
||||||
|
<option value="YYYY-MM-DDTHH:mm:ss:FFFFFZ">Internal</option>
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<table id="videos"></table>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,79 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!-- vim: set et: -->
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
<style type="text/css">
|
|
||||||
#nav {
|
|
||||||
float: left;
|
|
||||||
width: 17em;
|
|
||||||
}
|
|
||||||
#nav .ui-datepicker { width: 100%; }
|
|
||||||
|
|
||||||
#videos {
|
|
||||||
margin-left: 18em;
|
|
||||||
}
|
|
||||||
#videos tbody:after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
height: 3ex;
|
|
||||||
}
|
|
||||||
tbody .name {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
tr.r:hover { background-color: #ddd; }
|
|
||||||
tr.r th, tr.r td { padding: 0.5ex 1.5em; text-align: right; }
|
|
||||||
|
|
||||||
.ui-dialog .ui-dialog-content {
|
|
||||||
overflow: visible; /* remove stupid scroll bars when resizing. */
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="nav">
|
|
||||||
<form action="#">
|
|
||||||
<fieldset id="cameras">
|
|
||||||
<legend>Cameras</legend>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset id="datetime">
|
|
||||||
<legend>Datetime range</legend>
|
|
||||||
<div id="start-date"></div>
|
|
||||||
<input id="start-time" name="start-time" type="text" title="Starting
|
|
||||||
time within the day. Blank for the beginning of the day. Otherwise
|
|
||||||
HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a second.
|
|
||||||
Timezone is normally left out; it's useful once a year during the
|
|
||||||
ambiguous times of the "fall back" hour."><br>
|
|
||||||
<input type="radio" name="end-date-type" id="end-date-same" checked>
|
|
||||||
<label for="end-date-same">to same day</label><br>
|
|
||||||
<input type="radio" name="end-date-type" id="end-date-other">
|
|
||||||
<label for="end-date-other">to other day</label>
|
|
||||||
<div id="end-date"></div>
|
|
||||||
<input id="end-time" name="end-time" type="text" title="Ending
|
|
||||||
time within the day. Blank for the end of the day. Otherwise
|
|
||||||
HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a second.
|
|
||||||
Timezone is normally left out; it's useful once a year during the
|
|
||||||
ambiguous times of the "fall back" hour."><br>
|
|
||||||
</fieldset>
|
|
||||||
<label for="split">Max video length</label>
|
|
||||||
<select name="split" id="split">
|
|
||||||
<option value="324000000">1 hour</option>
|
|
||||||
<option value="1296000000">4 hours</option>
|
|
||||||
<option value="7776000000">24 hours</option>
|
|
||||||
<option value="">infinite</option>
|
|
||||||
</select><br>
|
|
||||||
<input type="checkbox" checked id="trim" name="trim">
|
|
||||||
<label for="trim">Trim segment start/end</label><br>
|
|
||||||
<input type="checkbox" checked id="ts" name="ts">
|
|
||||||
<label for="ts">Timestamp track</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<table id="videos"></table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
445
ui-src/index.js
445
ui-src/index.js
@ -1,418 +1,41 @@
|
|||||||
// vim: set et sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// TODO: test abort.
|
import NVRApplication from './NVRApplication';
|
||||||
// TODO: add error bar on fetch failure.
|
|
||||||
// TODO: style: no globals? string literals? line length? fn comments?
|
|
||||||
// TODO: live updating.
|
|
||||||
|
|
||||||
import './favicon.ico';
|
|
||||||
|
|
||||||
import 'jquery-ui/themes/base/button.css';
|
|
||||||
import 'jquery-ui/themes/base/core.css';
|
|
||||||
import 'jquery-ui/themes/base/datepicker.css';
|
|
||||||
import 'jquery-ui/themes/base/dialog.css';
|
|
||||||
import 'jquery-ui/themes/base/resizable.css';
|
|
||||||
import 'jquery-ui/themes/base/theme.css';
|
|
||||||
import 'jquery-ui/themes/base/tooltip.css';
|
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import 'jquery-ui/ui/widgets/datepicker';
|
|
||||||
import 'jquery-ui/ui/widgets/dialog';
|
|
||||||
import 'jquery-ui/ui/widgets/tooltip';
|
|
||||||
import moment from 'moment-timezone';
|
|
||||||
|
|
||||||
const apiUrl = '/api/';
|
|
||||||
|
|
||||||
// IANA timezone name.
|
|
||||||
let zone = null;
|
|
||||||
|
|
||||||
// A dict describing the currently selected range.
|
|
||||||
let selectedRange = {
|
|
||||||
startDateStr: null, // null or YYYY-MM-DD
|
|
||||||
startTimeStr: '', // empty or HH:mm[:ss[:FFFFF]][+-HHmm]
|
|
||||||
startTime90k: null, // null or 90k units since epoch
|
|
||||||
endDateStr: null, // null or YYYY-MM-DD
|
|
||||||
endTimeStr: '', // empty or HH:mm[:ss[:FFFFF]][+-HHmm]
|
|
||||||
endTime90k: null, // null or 90k units since epoch
|
|
||||||
singleDateStr: null, // if startDateStr===endDateStr, that value, otherwise null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cameras is a dictionary as retrieved from apiUrl + some extra props:
|
|
||||||
// * "enabled" is a boolean indicating if the camera should be displayed and
|
|
||||||
// if it should be used to constrain the datepickers.
|
|
||||||
// * "recordingsUrl" is null or the currently fetched/fetching .../recordings url.
|
|
||||||
// * "recordingsRange" is a null or a dict (in the same format as
|
|
||||||
// selectedRange) describing what is fetching/fetched.
|
|
||||||
// * "recordingsData" is null or the data fetched from "recordingsUrl".
|
|
||||||
// * "recordingsReq" is null or a jQuery ajax object of an active .../recordings
|
|
||||||
// request if currently fetching.
|
|
||||||
let cameras = null;
|
|
||||||
|
|
||||||
function req(url) {
|
|
||||||
return $.ajax(url, {
|
|
||||||
dataType: 'json',
|
|
||||||
headers: {'Accept': 'application/json'},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format timestamp using a format string.
|
|
||||||
*
|
|
||||||
* The timestamp to be formatted is expected to be in units of 90,000 to a
|
|
||||||
* second (90k format).
|
|
||||||
*
|
|
||||||
* The format string should comply with what is accepted by moment.format,
|
|
||||||
* with one addition. A format pattern of FFFFF (5 Fs) can be used. This
|
|
||||||
* format pattern will be replaced with the fractional second part of the
|
|
||||||
* timestamp, still in 90k units. Thus if the timestamp was 89900 (which is
|
|
||||||
* almost a full second; 0.99888 seconds decimal), the output would be
|
|
||||||
* 89900, and NOT 0.99888. Only a pattern of five Fs is recognized and it
|
|
||||||
* will produce exactly a five position output! You cannot vary the number
|
|
||||||
* of Fs to produce less.
|
|
||||||
*
|
|
||||||
* The default format string was chosen to produce results identical to
|
|
||||||
* a previous version of this code that was hard-coded to produce that output.
|
|
||||||
*
|
|
||||||
* @param {Number} ts90k Timestamp in 90k units
|
|
||||||
* @param {String} format moment.format plus FFFFF pattern supported
|
|
||||||
* @return {String} Formatted timestamp
|
|
||||||
*/
|
|
||||||
function formatTime(ts90k, format = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ') {
|
|
||||||
const ms = ts90k / 90.0;
|
|
||||||
const fracFmt = 'FFFFF';
|
|
||||||
let fracLoc = format.indexOf(fracFmt);
|
|
||||||
if (fracLoc != -1) {
|
|
||||||
const frac = ts90k % 90000;
|
|
||||||
format = format.substr(0, fracLoc) + String(100000 + frac).substr(1) +
|
|
||||||
format.substr(fracLoc + fracFmt.length);
|
|
||||||
}
|
|
||||||
return moment.tz(ms, zone).format(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectVideo(camera, range, recording) {
|
|
||||||
let url = apiUrl + 'cameras/' + camera.uuid + '/view.mp4?s=' + recording.startId;
|
|
||||||
if (recording.endId !== undefined) {
|
|
||||||
url += '-' + recording.endId;
|
|
||||||
}
|
|
||||||
const trim = $("#trim").prop("checked");
|
|
||||||
let rel = '';
|
|
||||||
let startTime90k = recording.startTime90k;
|
|
||||||
if (trim && recording.startTime90k < range.startTime90k) {
|
|
||||||
rel += range.startTime90k - recording.startTime90k;
|
|
||||||
startTime90k = range.startTime90k;
|
|
||||||
}
|
|
||||||
rel += '-';
|
|
||||||
let endTime90k = recording.endTime90k;
|
|
||||||
if (trim && recording.endTime90k > range.endTime90k) {
|
|
||||||
rel += range.endTime90k - recording.startTime90k;
|
|
||||||
endTime90k = range.endTime90k;
|
|
||||||
}
|
|
||||||
if (rel !== '-') {
|
|
||||||
url += '.' + rel;
|
|
||||||
}
|
|
||||||
if ($("#ts").prop("checked")) {
|
|
||||||
url += '&ts=true';
|
|
||||||
}
|
|
||||||
console.log('Displaying video: ', url);
|
|
||||||
let video = $('<video controls preload="auto" autoplay="true"/>');
|
|
||||||
let dialog = $('<div class="playdialog"/>').append(video);
|
|
||||||
$("body").append(dialog);
|
|
||||||
|
|
||||||
// Format start and end times for the dialog title. If they're the same day,
|
|
||||||
// abbreviate the end time.
|
|
||||||
let formattedStart = formatTime(startTime90k);
|
|
||||||
let formattedEnd = formatTime(endTime90k);
|
|
||||||
let timePos = 'YYYY-mm-ddT'.length;
|
|
||||||
if (formattedEnd.startsWith(formattedStart.substr(0, timePos))) {
|
|
||||||
formattedEnd = formattedEnd.substr(timePos);
|
|
||||||
}
|
|
||||||
dialog.dialog({
|
|
||||||
title: camera.shortName + ", " + formattedStart + " to " + formattedEnd,
|
|
||||||
width: recording.videoSampleEntryWidth / 4,
|
|
||||||
close: () => {
|
|
||||||
const videoDOMElement = video[0];
|
|
||||||
videoDOMElement.pause();
|
|
||||||
videoDOMElement.src = ''; // Remove current source to stop loading
|
|
||||||
dialog.remove();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
video.attr("src", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRecordings(camera) {
|
|
||||||
let tbody = $("#tab-" + camera.uuid);
|
|
||||||
$(".loading", tbody).hide();
|
|
||||||
$(".r", tbody).remove();
|
|
||||||
const frameRateFmt = new Intl.NumberFormat([], {maximumFractionDigits: 0});
|
|
||||||
const sizeFmt = new Intl.NumberFormat([], {maximumFractionDigits: 1});
|
|
||||||
const trim = $("#trim").prop("checked");
|
|
||||||
for (let recording of camera.recordingsData.recordings) {
|
|
||||||
const duration = (recording.endTime90k - recording.startTime90k) / 90000;
|
|
||||||
let row = $('<tr class="r"/>');
|
|
||||||
const startTime90k = trim && recording.startTime90k < camera.recordingsRange.startTime90k
|
|
||||||
? camera.recordingsRange.startTime90k : recording.startTime90k;
|
|
||||||
const endTime90k = trim && recording.endTime90k > camera.recordingsRange.endTime90k
|
|
||||||
? camera.recordingsRange.endTime90k : recording.endTime90k;
|
|
||||||
let formattedStart = formatTime(startTime90k);
|
|
||||||
let formattedEnd = formatTime(endTime90k);
|
|
||||||
const singleDateStr = camera.recordingsRange.singleDateStr;
|
|
||||||
if (singleDateStr !== null && formattedStart.startsWith(singleDateStr)) {
|
|
||||||
formattedStart = formattedStart.substr(11);
|
|
||||||
}
|
|
||||||
if (singleDateStr !== null && formattedEnd.startsWith(singleDateStr)) {
|
|
||||||
formattedEnd = formattedEnd.substr(11);
|
|
||||||
}
|
|
||||||
row.append(
|
|
||||||
$("<td/>").text(formattedStart),
|
|
||||||
$("<td/>").text(formattedEnd),
|
|
||||||
$("<td/>").text(recording.videoSampleEntryWidth + "x" + recording.videoSampleEntryHeight),
|
|
||||||
$("<td/>").text(frameRateFmt.format(recording.videoSamples / duration)),
|
|
||||||
$("<td/>").text(sizeFmt.format(recording.sampleFileBytes / 1048576) + " MB"),
|
|
||||||
$("<td/>").text(sizeFmt.format(recording.sampleFileBytes / duration * .000008) + " Mbps"));
|
|
||||||
row.on("click", function() { onSelectVideo(camera, camera.recordingsRange, recording); });
|
|
||||||
tbody.append(row);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function reselectDateRange(startDateStr, endDateStr) {
|
|
||||||
selectedRange.startDateStr = startDateStr;
|
|
||||||
selectedRange.endDateStr = endDateStr;
|
|
||||||
selectedRange.startTime90k = parseDateTime(startDateStr, selectedRange.startTimeStr, false);
|
|
||||||
selectedRange.endTime90k = parseDateTime(endDateStr, selectedRange.endTimeStr, true);
|
|
||||||
fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run when selectedRange is populated/changed or when split changes.
|
|
||||||
function fetch() {
|
|
||||||
console.log('Fetching ', formatTime(selectedRange.startTime90k), ' to ',
|
|
||||||
formatTime(selectedRange.endTime90k));
|
|
||||||
let split = $("#split").val();
|
|
||||||
for (let camera of cameras) {
|
|
||||||
let url = apiUrl + 'cameras/' + camera.uuid + '/recordings?startTime90k=' +
|
|
||||||
selectedRange.startTime90k + '&endTime90k=' + selectedRange.endTime90k;
|
|
||||||
if (split !== '') {
|
|
||||||
url += '&split90k=' + split;
|
|
||||||
}
|
|
||||||
if (url === camera.recordingsUrl) {
|
|
||||||
continue; // nothing to do.
|
|
||||||
}
|
|
||||||
if (camera.recordingsReq !== null) {
|
|
||||||
camera.recordingsReq.abort();
|
|
||||||
}
|
|
||||||
let tbody = $("#tab-" + camera.uuid);
|
|
||||||
$(".r", tbody).remove();
|
|
||||||
$(".loading", tbody).show();
|
|
||||||
let r = req(url);
|
|
||||||
camera.recordingsUrl = url;
|
|
||||||
camera.recordingsRange = selectedRange;
|
|
||||||
camera.recordingsReq = r;
|
|
||||||
r.always(function() { camera.recordingsReq = null; });
|
|
||||||
r.then(function(data, status, req) {
|
|
||||||
// Sort recordings in descending order.
|
|
||||||
data.recordings.sort(function(a, b) { return b.startId - a.startId; });
|
|
||||||
camera.recordingsData = data;
|
|
||||||
formatRecordings(camera);
|
|
||||||
}).catch(function(data, status, err) {
|
|
||||||
console.log(url, ' load failed: ', status, ': ', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run initially and when changing camera filter.
|
|
||||||
function setupCalendar() {
|
|
||||||
let merged = {};
|
|
||||||
for (const camera of cameras) {
|
|
||||||
if (!camera.enabled) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const dateStr in camera.days) {
|
|
||||||
merged[dateStr] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let minDateStr = '9999-99-99';
|
|
||||||
let maxDateStr = '0000-00-00';
|
|
||||||
for (const dateStr in merged) {
|
|
||||||
if (dateStr > maxDateStr) {
|
|
||||||
maxDateStr = dateStr;
|
|
||||||
}
|
|
||||||
if (dateStr < minDateStr) {
|
|
||||||
minDateStr = dateStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let from = $("#start-date");
|
|
||||||
let to = $("#end-date");
|
|
||||||
let beforeShowDay = function(date) {
|
|
||||||
let dateStr = date.toISOString().substr(0, 10);
|
|
||||||
return [dateStr in merged, "", ""];
|
|
||||||
}
|
|
||||||
if ($("#end-date-same").prop("checked")) {
|
|
||||||
from.datepicker("option", {
|
|
||||||
dateFormat: $.datepicker.ISO_8601,
|
|
||||||
minDate: minDateStr,
|
|
||||||
maxDate: maxDateStr,
|
|
||||||
onSelect: function(dateStr, picker) {
|
|
||||||
reselectDateRange(dateStr, dateStr);
|
|
||||||
},
|
|
||||||
beforeShowDay: beforeShowDay,
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
to.datepicker("destroy");
|
|
||||||
to.datepicker({disabled: true});
|
|
||||||
} else {
|
|
||||||
from.datepicker("option", {
|
|
||||||
dateFormat: $.datepicker.ISO_8601,
|
|
||||||
minDate: minDateStr,
|
|
||||||
onSelect: function(dateStr, picker) {
|
|
||||||
to.datepicker("option", "minDate", from.datepicker("getDate").toISOString().substr(0, 10));
|
|
||||||
reselectDateRange(dateStr, to.datepicker("getDate").toISOString().substr(0, 10));
|
|
||||||
},
|
|
||||||
beforeShowDay: beforeShowDay,
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
to.datepicker("option", {
|
|
||||||
dateFormat: $.datepicker.ISO_8601,
|
|
||||||
minDate: from.datepicker("getDate"),
|
|
||||||
maxDate: maxDateStr,
|
|
||||||
onSelect: function(dateStr, picker) {
|
|
||||||
from.datepicker("option", "maxDate", to.datepicker("getDate").toISOString().substr(0, 10));
|
|
||||||
reselectDateRange(from.datepicker("getDate").toISOString().substr(0, 10), dateStr);
|
|
||||||
},
|
|
||||||
beforeShowDay: beforeShowDay,
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
to.datepicker("setDate", from.datepicker("getDate"));
|
|
||||||
from.datepicker("option", {maxDate: to.datepicker("getDate")});
|
|
||||||
}
|
|
||||||
const date = from.datepicker("getDate");
|
|
||||||
if (date !== null) {
|
|
||||||
const dateStr = date.toISOString().substr(0, 10);
|
|
||||||
reselectDateRange(dateStr, dateStr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function onCameraChange(event, camera) {
|
|
||||||
camera.enabled = event.target.checked;
|
|
||||||
if (camera.enabled) {
|
|
||||||
$("#tab-" + camera.uuid).show();
|
|
||||||
} else {
|
|
||||||
$("#tab-" + camera.uuid).hide();
|
|
||||||
}
|
|
||||||
console.log('Camera ', camera.shortName, camera.enabled ? 'enabled' : 'disabled');
|
|
||||||
setupCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parses the given date and time string into a valid time90k or null.
|
|
||||||
function parseDateTime(dateStr, timeStr, isEnd) {
|
|
||||||
// Match HH:mm[:ss[:FFFFF]][+-HH:mm]
|
|
||||||
// Group 1 is the hour and minute (HH:mm).
|
|
||||||
// Group 2 is the seconds (:ss), if any.
|
|
||||||
// Group 3 is the fraction (FFFFF), if any.
|
|
||||||
// Group 4 is the zone (+-HH:mm), if any.
|
|
||||||
const timeRe =
|
|
||||||
/^([0-9]{1,2}:[0-9]{2})(?:(:[0-9]{2})(?::([0-9]{5}))?)?([+-][0-9]{1,2}:?(?:[0-9]{2})?)?$/;
|
|
||||||
|
|
||||||
if (timeStr === '') {
|
|
||||||
const m = moment.tz(dateStr, zone);
|
|
||||||
if (isEnd) {
|
|
||||||
m.add({days: 1});
|
|
||||||
}
|
|
||||||
return m.valueOf() * 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = timeRe.exec(timeStr);
|
|
||||||
if (match === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orBlank = function(s) { return s === undefined ? "" : s; };
|
|
||||||
const datetimeStr = dateStr + 'T' + match[1] + orBlank(match[2]) + orBlank(match[4]);
|
|
||||||
const m = moment.tz(datetimeStr, zone);
|
|
||||||
if (!m.isValid()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frac = match[3] === undefined ? 0 : parseInt(match[3], 10);
|
|
||||||
return m.valueOf() * 90 + frac;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTimeChange(e, isEnd) {
|
|
||||||
let parsed = parseDateTime(isEnd ? selectedRange.endDateStr : selectedRange.startDateStr,
|
|
||||||
e.target.value, isEnd);
|
|
||||||
if (parsed == null) {
|
|
||||||
console.log('bad time change');
|
|
||||||
$(e.target).addClass('ui-state-error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$(e.target).removeClass('ui-state-error');
|
|
||||||
console.log(isEnd ? "end" : "start", ' time change to: ', parsed, ' (', formatTime(parsed), ')');
|
|
||||||
if (isEnd) {
|
|
||||||
selectedRange.endTimeStr = e.target.value;
|
|
||||||
selectedRange.endTime90k = parsed;
|
|
||||||
} else {
|
|
||||||
selectedRange.startTimeStr = e.target.value;
|
|
||||||
selectedRange.startTime90k = parsed;
|
|
||||||
}
|
|
||||||
fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onReceivedCameras(data) {
|
|
||||||
let fieldset = $("#cameras");
|
|
||||||
if (data.cameras.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var reqs = [];
|
|
||||||
let videos = $("#videos");
|
|
||||||
for (let camera of data.cameras) {
|
|
||||||
const id = "cam-" + camera.uuid;
|
|
||||||
let checkBox = $('<input type="checkbox" checked>').attr("name", id).attr("id", id);
|
|
||||||
checkBox.change(function(event) { onCameraChange(event, camera); });
|
|
||||||
fieldset.append(checkBox,
|
|
||||||
$("<label/>").attr("for", id).text(camera.shortName),
|
|
||||||
$("<br/>"));
|
|
||||||
let tab = $("<tbody>").attr("id", "tab-" + camera.uuid);
|
|
||||||
tab.append(
|
|
||||||
$('<tr class="name">').append($('<th colspan=6/>').text(camera.shortName)),
|
|
||||||
$('<tr class="hdr"><th>start</th><th>end</th><th>resolution</th><th>fps</th><th>size</th><th>bitrate</th></tr>'),
|
|
||||||
$('<tr class="loading"><td colspan=6>loading...</td></tr>'));
|
|
||||||
videos.append(tab);
|
|
||||||
camera.enabled = true;
|
|
||||||
camera.recordingsUrl = null;
|
|
||||||
camera.recordingsRange = null;
|
|
||||||
camera.recordingsData = null;
|
|
||||||
camera.recordingsReq = null;
|
|
||||||
}
|
|
||||||
$("#end-date-same").change(function(e) { setupCalendar(); });
|
|
||||||
$("#end-date-other").change(function(e) { setupCalendar(); });
|
|
||||||
$("#start-date").datepicker({disabled: true});
|
|
||||||
$("#end-date").datepicker({disabled: true});
|
|
||||||
$("#start-time").change(function(e) { onTimeChange(e, false); });
|
|
||||||
$("#end-time").change(function(e) { onTimeChange(e, true); });
|
|
||||||
$("#split").change(function(e) {
|
|
||||||
if (selectedRange.startTime90k !== null) {
|
|
||||||
fetch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$("#trim").change(function(e) {
|
|
||||||
// Changing the trim doesn't need to refetch data, but it does need to
|
|
||||||
// reformat the tables.
|
|
||||||
let newTrim = e.target.checked;
|
|
||||||
for (camera of cameras) {
|
|
||||||
if (camera.recordingsData !== null) {
|
|
||||||
formatRecordings(camera);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
zone = data.timeZoneName;
|
|
||||||
cameras = data.cameras;
|
|
||||||
console.log('Loaded cameras.');
|
|
||||||
setupCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// On document load, start application
|
||||||
$(function() {
|
$(function() {
|
||||||
$(document).tooltip();
|
$(document).tooltip();
|
||||||
req(apiUrl + '?days=true').then(function(data, status, req) {
|
(new NVRApplication()).start();
|
||||||
onReceivedCameras(data);
|
|
||||||
}).catch(function(data, status, err) {
|
|
||||||
console.log('cameras load failed: ', status, err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
151
ui-src/lib/MoonfireAPI.js
Normal file
151
ui-src/lib/MoonfireAPI.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
import URLBuilder from './support/URLBuilder';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to insulate rest of app from details of Moonfire API.
|
||||||
|
*
|
||||||
|
* Can produce URLs for specifc operations, or a request that has been
|
||||||
|
* started and can have handlers attached.
|
||||||
|
*/
|
||||||
|
export default class MoonfireAPI {
|
||||||
|
/**
|
||||||
|
* Construct.
|
||||||
|
*
|
||||||
|
* The defaults correspond to a standard Moonfire installation on the
|
||||||
|
* same host that this code runs on.
|
||||||
|
*
|
||||||
|
* Requesting relative URLs effectively disregards the host and port options.
|
||||||
|
*
|
||||||
|
* @param {String} options.host Host where the API resides
|
||||||
|
* @param {Number} options.port Port on which the API resides
|
||||||
|
* @param {[type]} options.relativeUrls True if we should produce relative
|
||||||
|
* urls
|
||||||
|
*/
|
||||||
|
constructor({host = 'localhost', port = 8080, relativeUrls = true} = {}) {
|
||||||
|
const url = new URL('/api/', `http://${host}`);
|
||||||
|
url.protocol = 'http:';
|
||||||
|
url.hostname = host;
|
||||||
|
url.port = port;
|
||||||
|
console.log('API: ' + url.origin + url.pathname);
|
||||||
|
this._builder = new URLBuilder(url.origin + url.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL that will cause the state of the NVR to be returned.
|
||||||
|
*
|
||||||
|
* @param {Boolean} days True if a return of days with available recordings
|
||||||
|
* is desired.
|
||||||
|
* @return {String} Constructed url
|
||||||
|
*/
|
||||||
|
nvrUrl(days = true) {
|
||||||
|
return this._builder.makeUrl('', {days: days});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL that will cause the state of a specific recording to be returned.
|
||||||
|
*
|
||||||
|
* @param {String} cameraUUID UUID for the camera
|
||||||
|
* @param {String} start90k Timestamp for beginning of range of interest
|
||||||
|
* @param {String} end90k Timestamp for end of range of interest
|
||||||
|
* @param {String} split90k Desired maximum size of segments returned, or
|
||||||
|
* Infinity for infinite range
|
||||||
|
* @return {String} Constructed url
|
||||||
|
*/
|
||||||
|
recordingsUrl(cameraUUID, start90k, end90k, split90k = Infinity) {
|
||||||
|
const query = {
|
||||||
|
startTime90k: start90k,
|
||||||
|
endTime90k: end90k,
|
||||||
|
};
|
||||||
|
if (split90k != Infinity) {
|
||||||
|
query.split90k = split90k;
|
||||||
|
}
|
||||||
|
return this._builder.makeUrl(
|
||||||
|
'cameras/' + cameraUUID + '/recordings',
|
||||||
|
query
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL that will playback a video segment.
|
||||||
|
*
|
||||||
|
* @param {String} cameraUUID UUID for the camera from whence comes the video
|
||||||
|
* @param {Recording} recording Recording model object
|
||||||
|
* @param {Range90k} trimmedRange Range restricting segments
|
||||||
|
* @param {Boolean} timestampTrack True if track should be timestamped
|
||||||
|
* @return {String} Constructed url
|
||||||
|
*/
|
||||||
|
videoPlayUrl(cameraUUID, recording, trimmedRange, timestampTrack = true) {
|
||||||
|
let sParam = recording.startId;
|
||||||
|
if (recording.endId !== undefined) {
|
||||||
|
sParam += '-' + recording.endId;
|
||||||
|
}
|
||||||
|
let rel = '';
|
||||||
|
if (recording.startTime90k < trimmedRange.startTime90k) {
|
||||||
|
rel += trimmedRange.startTime90k - recording.startTime90k;
|
||||||
|
}
|
||||||
|
rel += '-';
|
||||||
|
if (recording.endTime90k > trimmedRange.endTime90k) {
|
||||||
|
rel += trimmedRange.endTime90k - recording.startTime90k;
|
||||||
|
}
|
||||||
|
if (rel !== '-') {
|
||||||
|
sParam += '.' + rel;
|
||||||
|
}
|
||||||
|
console.log('Video query:', {
|
||||||
|
s: sParam,
|
||||||
|
ts: timestampTrack,
|
||||||
|
});
|
||||||
|
return this._builder.makeUrl('cameras/' + cameraUUID + '/view.mp4', {
|
||||||
|
s: sParam,
|
||||||
|
ts: timestampTrack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new AJAX request with the specified URL.
|
||||||
|
*
|
||||||
|
* @param {String} url URL to use
|
||||||
|
* @param {String} cacheOk True if cached results are OK
|
||||||
|
* @return {Request} jQuery request type
|
||||||
|
*/
|
||||||
|
request(url, cacheOk = false) {
|
||||||
|
return $.ajax(url, {
|
||||||
|
dataType: 'json',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
cache: cacheOk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
245
ui-src/lib/models/CalendarTSRange.js
Normal file
245
ui-src/lib/models/CalendarTSRange.js
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import Time90kParser from '../support/Time90kParser';
|
||||||
|
import {TimeStamp90kFormatter} from '../support/TimeFormatter';
|
||||||
|
import Range90k from './Range90k';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a calendar timestamp range based on 90k units.
|
||||||
|
*
|
||||||
|
* A calendar timestamp differs from a Range90k in that a date string
|
||||||
|
* is involved on each end as well.
|
||||||
|
*
|
||||||
|
* The range has a start and end property (via getters) and each has three
|
||||||
|
* contained properties:
|
||||||
|
* - dateStr: string for date in ISO8601 format
|
||||||
|
* - timeStr: string for time in ISO8601 format
|
||||||
|
* - ts90k: Number for the timestamp in 90k units
|
||||||
|
*/
|
||||||
|
export default class CalendarTSRange {
|
||||||
|
/**
|
||||||
|
* Construct a range with a given timezone for display purposes.
|
||||||
|
*
|
||||||
|
* @param {String} timeZone Desired timezone, e.g. 'America/Los_Angeles'
|
||||||
|
*/
|
||||||
|
constructor(timeZone) {
|
||||||
|
this._start = {dateStr: null, timeStr: '', ts90k: null};
|
||||||
|
this._end = {dateStr: null, timeStr: '', ts90k: null};
|
||||||
|
// Don't need to keep timezone, but need parser and formatter
|
||||||
|
this._timeFormatter = new TimeStamp90kFormatter(timeZone);
|
||||||
|
this._timeParser = new Time90kParser(timeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a valid start date string is present.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
hasStart() {
|
||||||
|
return this.start.dateStr !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a valid end date string is present.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
hasEnd() {
|
||||||
|
return this.end.dateStr !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a valid start and end date string is present.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
hasRange() {
|
||||||
|
return this.hasStart() && this.hasEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's start component.
|
||||||
|
*
|
||||||
|
* @return {object} Object containing dateStr, timeStr, and ts90k components
|
||||||
|
*/
|
||||||
|
get start() {
|
||||||
|
return this._start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's end component.
|
||||||
|
*
|
||||||
|
* @return {object} Object containing dateStr, timeStr, and ts90k components
|
||||||
|
*/
|
||||||
|
get end() {
|
||||||
|
return this._end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's start component's ts90k property
|
||||||
|
*
|
||||||
|
* @return {object} timestamp in 90k units
|
||||||
|
*/
|
||||||
|
get startTime90k() {
|
||||||
|
return this.start.ts90k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's end component's ts90k property
|
||||||
|
*
|
||||||
|
* @return {object} timestamp in 90k units
|
||||||
|
*/
|
||||||
|
get endTime90k() {
|
||||||
|
return this.end.ts90k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the range has a defined start timestamp in 90k units.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
get hasStartTime() {
|
||||||
|
return this.startTime90k !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the calendar range in terms of a range over 90k timestamps.
|
||||||
|
*
|
||||||
|
* @return {Range90k} Range object or null if don't have start and end
|
||||||
|
*/
|
||||||
|
range90k() {
|
||||||
|
return this.hasRange()
|
||||||
|
? new Range90k(this.startTime90k, this.endTime90k)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function to update either start or end type range component.
|
||||||
|
*
|
||||||
|
* Strings are parsed to check if they are valid. Update only takes place
|
||||||
|
* if they are. Parsing is in accordance with the installed Time90kParser
|
||||||
|
* which means:
|
||||||
|
* - HH:MM:ss:FFFFFZ format, where each componet may be empty to indicate 0
|
||||||
|
* - YYYY-MM-DD format for the date
|
||||||
|
*
|
||||||
|
* NOTE: This function potentially modifies the content of the range
|
||||||
|
* argument. This is on purpose and should reflect the new range values
|
||||||
|
* upon succesful parsing!
|
||||||
|
*
|
||||||
|
* @param {object} range A range component
|
||||||
|
* @param {String} dateStr Date string, if null range's value is re-used
|
||||||
|
* @param {String} timeStr Time string, if null range's value is re-used
|
||||||
|
* @param {Boolean} dateOnlyThenEndOfDay True if one should be added to date
|
||||||
|
* which is only meaningful if there
|
||||||
|
* is no time specified here, and also
|
||||||
|
* not present in the range.
|
||||||
|
* @return {Number} New timestamp if succesfully parsed, null otherwise
|
||||||
|
*/
|
||||||
|
_setRangeTime(range, dateStr, timeStr, dateOnlyThenEndOfDay) {
|
||||||
|
dateStr = dateStr || range.dateStr;
|
||||||
|
timeStr = timeStr || range.timeStr;
|
||||||
|
const newTs90k = this._timeParser.parseDateTime90k(
|
||||||
|
dateStr,
|
||||||
|
timeStr,
|
||||||
|
dateOnlyThenEndOfDay
|
||||||
|
);
|
||||||
|
if (newTs90k !== null) {
|
||||||
|
range.dateStr = dateStr;
|
||||||
|
range.timeStr = timeStr;
|
||||||
|
range.ts90k = newTs90k;
|
||||||
|
return newTs90k;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set start component of range from date and time strings.
|
||||||
|
*
|
||||||
|
* Uses _setRangeTime with appropriate dateOnlyThenEndOfDay value.
|
||||||
|
*
|
||||||
|
* @param {String} dateStr Date string
|
||||||
|
* @param {String} timeStr Time string
|
||||||
|
* @return {Number} New timestamp if succesfully parsed, null otherwise
|
||||||
|
*/
|
||||||
|
setStartDate(dateStr, timeStr = null) {
|
||||||
|
return this._setRangeTime(this._start, dateStr, timeStr, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set time of start component of range time string.
|
||||||
|
*
|
||||||
|
* Uses _setRangeTime with appropriate dateOnlyThenEndOfDay value.
|
||||||
|
*
|
||||||
|
* @param {String} timeStr Time string
|
||||||
|
* @return {Number} New timestamp if succesfully parsed, null otherwise
|
||||||
|
*/
|
||||||
|
setStartTime(timeStr) {
|
||||||
|
return this._setRangeTime(this._start, null, timeStr, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set end component of range from date and time strings.
|
||||||
|
*
|
||||||
|
* Uses _setRangeTime with appropriate addOne value.
|
||||||
|
*
|
||||||
|
* @param {String} dateStr Date string
|
||||||
|
* @param {String} timeStr Time string
|
||||||
|
* @return {Number} New timestamp if succesfully parsed, null otherwise
|
||||||
|
*/
|
||||||
|
setEndDate(dateStr, timeStr = null) {
|
||||||
|
return this._setRangeTime(this._end, dateStr, timeStr, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set time of end component of range time string.
|
||||||
|
*
|
||||||
|
* Uses _setRangeTime with appropriate addOne value.
|
||||||
|
*
|
||||||
|
* @param {String} timeStr Time string
|
||||||
|
* @return {Number} New timestamp if succesfully parsed, null otherwise
|
||||||
|
*/
|
||||||
|
setEndTime(timeStr) {
|
||||||
|
return this._setRangeTime(this._end, null, timeStr, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a timestamp in 90k units in the manner consistent with
|
||||||
|
* what the parser of this module expects.
|
||||||
|
*
|
||||||
|
* @param {Number} ts90k Timestamp in 90k units
|
||||||
|
* @return {String} Formatted string
|
||||||
|
*/
|
||||||
|
formatTimeStamp90k(ts90k) {
|
||||||
|
return this._timeFormatter.formatTimeStamp90k(ts90k);
|
||||||
|
}
|
||||||
|
}
|
132
ui-src/lib/models/Camera.js
Normal file
132
ui-src/lib/models/Camera.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import JsonWrapper from './JsonWrapper';
|
||||||
|
import Range90k from './Range90k';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera JSON wrapper.
|
||||||
|
*/
|
||||||
|
export default class Camera extends JsonWrapper {
|
||||||
|
/**
|
||||||
|
* Construct from JSON.
|
||||||
|
*
|
||||||
|
* @param {JSON} cameraJson JSON for single camera.
|
||||||
|
*/
|
||||||
|
constructor(cameraJson) {
|
||||||
|
super(cameraJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get camera uuid.
|
||||||
|
*
|
||||||
|
* @return {String} Camera's uuid
|
||||||
|
*/
|
||||||
|
get uuid() {
|
||||||
|
return this.json.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get camera's short name.
|
||||||
|
*
|
||||||
|
* @return {String} Name of the camera
|
||||||
|
*/
|
||||||
|
get shortName() {
|
||||||
|
return this.json.shortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get camera's description.
|
||||||
|
*
|
||||||
|
* @return {String} Camera's description
|
||||||
|
*/
|
||||||
|
get description() {
|
||||||
|
return this.json.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get maximimum amount of storage allowed to be used for camera's video
|
||||||
|
* samples.
|
||||||
|
*
|
||||||
|
* @return {Number} Amount in bytes
|
||||||
|
*/
|
||||||
|
get retainBytes() {
|
||||||
|
return this.json.retainBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a Range90K object representing the range encompassing all available
|
||||||
|
* video samples for the camera.
|
||||||
|
*
|
||||||
|
* This range does not mean every second of the range has video!
|
||||||
|
*
|
||||||
|
* @return {Range90k} The camera's available recordings range
|
||||||
|
*/
|
||||||
|
get range90k() {
|
||||||
|
return new Range90k(
|
||||||
|
this.json.minStartTime90k,
|
||||||
|
this.json.maxEndTime90k,
|
||||||
|
this.json.totalDuration90k
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total amount of storage currently taken up by the camera's video
|
||||||
|
* samples.
|
||||||
|
*
|
||||||
|
* @return {Number} Amount in bytes
|
||||||
|
*/
|
||||||
|
get totalSampleFileBytes() {
|
||||||
|
return this.json.totalSampleFileBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of the camera's days for which there are video samples.
|
||||||
|
*
|
||||||
|
* The result is a Map with dates as keys (in YYYY-MM-DD format) and each
|
||||||
|
* value is a Range90k object for that day. Here too, the range does not
|
||||||
|
* mean every second in the range has video, but presence of an entry for
|
||||||
|
* a day does mean there is at least one (however short) video segment
|
||||||
|
* available.
|
||||||
|
*
|
||||||
|
* @return {Map} Dates are keys, values are Range90K objects.
|
||||||
|
*/
|
||||||
|
get days() {
|
||||||
|
return new Map(
|
||||||
|
Object.entries(this.json.days).map(function(t) {
|
||||||
|
let [k, v] = t;
|
||||||
|
v = new Range90k(v.startTime90k, v.endTime90k, v.totalDuration90k);
|
||||||
|
return [k, v];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
80
ui-src/lib/models/JsonWrapper.js
Normal file
80
ui-src/lib/models/JsonWrapper.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WeakMap that keeps our private data.
|
||||||
|
*
|
||||||
|
* @type {WeakMap}
|
||||||
|
*/
|
||||||
|
let _json = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to encapsulate recording JSON data.
|
||||||
|
* *
|
||||||
|
* The JSON is kept internally, but in a manner that does not allow direct
|
||||||
|
* access. If access is needed, use the "json()" method. Sub-classes for
|
||||||
|
* specific models shoudl provide the necessary getters instead.
|
||||||
|
*/
|
||||||
|
export default class JsonWrapper {
|
||||||
|
/**
|
||||||
|
* Accept JSON data to be encapsulated
|
||||||
|
*
|
||||||
|
* @param {object} jsonData JSON data
|
||||||
|
*/
|
||||||
|
constructor(jsonData) {
|
||||||
|
_json.set(this, jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get associated JSON object.
|
||||||
|
*
|
||||||
|
* Use of this should be avoided. Use functions to access the
|
||||||
|
* data instead.
|
||||||
|
*
|
||||||
|
* @return {object} The JSON object.
|
||||||
|
*/
|
||||||
|
get json() {
|
||||||
|
return _json.get(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @return {String} String version
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return this.json.toString();
|
||||||
|
} else {
|
||||||
|
return super.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
ui-src/lib/models/Range.js
Normal file
73
ui-src/lib/models/Range.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to represent ranges of values.
|
||||||
|
*
|
||||||
|
* The range has a "low", and "high" value property and is inclusive.
|
||||||
|
* The "size" property returns the difference between high and low.
|
||||||
|
*/
|
||||||
|
export default class Range {
|
||||||
|
/**
|
||||||
|
* Create a range.
|
||||||
|
*
|
||||||
|
* @param {Number} low Low value (inclusive) in range.
|
||||||
|
* @param {Number} high High value (inclusive) in range.
|
||||||
|
*/
|
||||||
|
constructor(low, high) {
|
||||||
|
if (high < low) {
|
||||||
|
console.log('Warning range swap: ' + low + ' - ' + high);
|
||||||
|
[low, high] = [high, low];
|
||||||
|
}
|
||||||
|
this.low = low;
|
||||||
|
this.high = high;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the range.
|
||||||
|
*
|
||||||
|
* @return {Number} high - low
|
||||||
|
*/
|
||||||
|
get size() {
|
||||||
|
return this.high - this.low;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if value is inside the range.
|
||||||
|
*
|
||||||
|
* @param {Number} value Value to test
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
isInRange(value) {
|
||||||
|
return value >= this.low && value <= this.high;
|
||||||
|
}
|
||||||
|
}
|
109
ui-src/lib/models/Range90k.js
Normal file
109
ui-src/lib/models/Range90k.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import Range from './Range';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WeakMap that keeps our private data.
|
||||||
|
*
|
||||||
|
* @type {WeakMap}
|
||||||
|
*/
|
||||||
|
let _range = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclass of Range to represent ranges over timestamps in 90k format.
|
||||||
|
*
|
||||||
|
* This mostly means added some getters with names that make more sense.
|
||||||
|
*/
|
||||||
|
export default class Range90k {
|
||||||
|
/**
|
||||||
|
* Create a range.
|
||||||
|
*
|
||||||
|
* @param {Number} low Low value (inclusive) in range.
|
||||||
|
* @param {Number} high High value (inclusive) in range.
|
||||||
|
*/
|
||||||
|
constructor(low, high) {
|
||||||
|
_range.set(this, new Range(low, high));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's start time.
|
||||||
|
*
|
||||||
|
* @return {Number} Number in 90k units
|
||||||
|
*/
|
||||||
|
get startTime90k() {
|
||||||
|
return _range.get(this).low;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's end time.
|
||||||
|
*
|
||||||
|
* @return {Number} Number in 90k units
|
||||||
|
*/
|
||||||
|
get endTime90k() {
|
||||||
|
return _range.get(this).high;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the range's duration.
|
||||||
|
*
|
||||||
|
* @return {Number} Number in 90k units
|
||||||
|
*/
|
||||||
|
get duration90k() {
|
||||||
|
return _range.get(this).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new range by trimming the current range against
|
||||||
|
* another.
|
||||||
|
*
|
||||||
|
* The returned range will lie completely within the provided range.
|
||||||
|
*
|
||||||
|
* @param {Range90k} against Range the be used for limits
|
||||||
|
* @return {Range90k} The trimmed range (always a new object)
|
||||||
|
*/
|
||||||
|
trimmed(against) {
|
||||||
|
return new Range90k(
|
||||||
|
Math.max(this.startTime90k, against.startTime90k),
|
||||||
|
Math.min(this.endTime90k, against.endTime90k)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a copy of this range.
|
||||||
|
*
|
||||||
|
* @return {Range90k} A copy of this range object.
|
||||||
|
*/
|
||||||
|
clone() {
|
||||||
|
return new Range90k(this.startTime90k, this.endTime90k);
|
||||||
|
}
|
||||||
|
}
|
155
ui-src/lib/models/Recording.js
Normal file
155
ui-src/lib/models/Recording.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import JsonWrapper from './JsonWrapper';
|
||||||
|
import Range90k from '../models/Range90k';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to encapsulate recording JSON data.
|
||||||
|
*/
|
||||||
|
export default class Recording extends JsonWrapper {
|
||||||
|
/**
|
||||||
|
* Accept JSON data to be encapsulated
|
||||||
|
*
|
||||||
|
* @param {object} recordingJson JSON for a recording
|
||||||
|
*/
|
||||||
|
constructor(recordingJson) {
|
||||||
|
super(recordingJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recording's startId.
|
||||||
|
*
|
||||||
|
* @return {String} startId for recording
|
||||||
|
*/
|
||||||
|
get startId() {
|
||||||
|
return this.json.startId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recording's endId.
|
||||||
|
*
|
||||||
|
* @return {String} endId for recording
|
||||||
|
*/
|
||||||
|
get endId() {
|
||||||
|
return this.json.endId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return start time of recording in 90k units.
|
||||||
|
* @return {Number} Time in units of 90k parts of a second
|
||||||
|
*/
|
||||||
|
get startTime90k() {
|
||||||
|
return this.json.startTime90k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return end time of recording in 90k units.
|
||||||
|
* @return {Number} Time in units of 90k parts of a second
|
||||||
|
*/
|
||||||
|
get endTime90k() {
|
||||||
|
return this.json.endTime90k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return duration of recording in 90k units.
|
||||||
|
* @return {Number} Time in units of 90k parts of a second
|
||||||
|
*/
|
||||||
|
get duration90k() {
|
||||||
|
const data = this.json;
|
||||||
|
return data.endTime90k - data.startTime90k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the range of the recording in 90k timestamp units,
|
||||||
|
* optionally trimmed by another range.
|
||||||
|
*
|
||||||
|
* @param {Range90k} trimmedAgainst Optional range to trim against
|
||||||
|
* @return {Range90k} Resulting range
|
||||||
|
*/
|
||||||
|
range90k(trimmedAgainst = null) {
|
||||||
|
let result = new Range90k(this.startTime90k, this.endTime90k);
|
||||||
|
return trimmedAgainst ? result.trimmed(trimmedAgainst) : result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return duration of recording in seconds.
|
||||||
|
* @return {Number} Time in units of seconds.
|
||||||
|
*/
|
||||||
|
get duration() {
|
||||||
|
return this.duration90k / 90000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of bytes used by sample storage.
|
||||||
|
*
|
||||||
|
* @return {Number} Total bytes used
|
||||||
|
*/
|
||||||
|
get sampleFileBytes() {
|
||||||
|
return this.json.sampleFileBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of video samples (frames) for the recording.
|
||||||
|
*
|
||||||
|
* @return {Number} Total bytes used
|
||||||
|
*/
|
||||||
|
get frameCount() {
|
||||||
|
return this.json.videoSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the has for the video samples.
|
||||||
|
*
|
||||||
|
* @return {String} Hash
|
||||||
|
*/
|
||||||
|
get videoSampleEntryHash() {
|
||||||
|
return this.json.videoSampleEntrySha1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the width of the frame(s) of the video samples.
|
||||||
|
*
|
||||||
|
* @return {Number} Width in pixels
|
||||||
|
*/
|
||||||
|
get videoSampleEntryWidth() {
|
||||||
|
return this.json.videoSampleEntryWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the height of the frame(s) of the video samples.
|
||||||
|
*
|
||||||
|
* @return {Number} Height in pixels
|
||||||
|
*/
|
||||||
|
get videoSampleEntryHeight() {
|
||||||
|
return this.json.videoSampleEntryHeight;
|
||||||
|
}
|
||||||
|
}
|
101
ui-src/lib/support/RecordingFormatter.js
Normal file
101
ui-src/lib/support/RecordingFormatter.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import TimeFormatter from './TimeFormatter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatter for framerates
|
||||||
|
* @type {Intl} Formatter
|
||||||
|
*/
|
||||||
|
const frameRateFmt = new Intl.NumberFormat([], {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatter for sizes
|
||||||
|
* @type {Intl} Formatter
|
||||||
|
*/
|
||||||
|
const sizeFmt = new Intl.NumberFormat([], {
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class encapsulating formatting of recording time ranges.
|
||||||
|
*/
|
||||||
|
export default class RecordingFormatter {
|
||||||
|
/**
|
||||||
|
* Construct with desired time format and timezone.
|
||||||
|
*
|
||||||
|
* @param {String} formatStr Time format string
|
||||||
|
* @param {String} tz Timezone
|
||||||
|
*/
|
||||||
|
constructor(formatStr, tz) {
|
||||||
|
this._timeFormatter = new TimeFormatter(formatStr, tz);
|
||||||
|
this._singleDateStr = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change time format string, preserving timezone.
|
||||||
|
*
|
||||||
|
* @param {String} formatStr Time format string
|
||||||
|
*/
|
||||||
|
set timeFormat(formatStr) {
|
||||||
|
this._timeFormatter = new TimeFormatter(formatStr, this._timeFormatter.tz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce an object whose properties are individual pieces of a recording's
|
||||||
|
* data, formatted for display purposes.
|
||||||
|
*
|
||||||
|
* @param {Recording} recording Recording to be formatted
|
||||||
|
* @param {Range90k} trimRange Optional time range for trimming the
|
||||||
|
* recording's interval
|
||||||
|
* @return {Object} Map, keyed by _columnOrder element
|
||||||
|
*/
|
||||||
|
format(recording, trimRange = null) {
|
||||||
|
const duration = recording.duration;
|
||||||
|
const trimmedRange = recording.range90k(trimRange);
|
||||||
|
return {
|
||||||
|
start: this._timeFormatter.formatTimeStamp90k(trimmedRange.startTime90k),
|
||||||
|
end: this._timeFormatter.formatTimeStamp90k(trimmedRange.endTime90k),
|
||||||
|
resolution:
|
||||||
|
recording.videoSampleEntryWidth +
|
||||||
|
'x' +
|
||||||
|
recording.videoSampleEntryHeight,
|
||||||
|
frameRate: frameRateFmt.format(recording.frameCount / duration),
|
||||||
|
size: sizeFmt.format(recording.sampleFileBytes / 1048576) + ' MB',
|
||||||
|
rate:
|
||||||
|
sizeFmt.format(recording.sampleFileBytes / duration * 0.000008) +
|
||||||
|
' Mbps',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
134
ui-src/lib/support/Time90kParser.js
Normal file
134
ui-src/lib/support/Time90kParser.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression for parsing time format from timestamps.
|
||||||
|
*
|
||||||
|
* These regex captures groups:
|
||||||
|
* 0: whole match or null if none
|
||||||
|
* 1: HH:MM portion, or undefined
|
||||||
|
* 2: :ss portion, or undefined
|
||||||
|
* 3: FFFFF portion, or undefined
|
||||||
|
* 4: [+-]hh[:mm] portion, or undefined
|
||||||
|
*
|
||||||
|
* @type {RegExp}
|
||||||
|
*/
|
||||||
|
const timeRe = new RegExp(
|
||||||
|
[
|
||||||
|
'^', // Start
|
||||||
|
'([0-9]{1,2}:[0-9]{2})', // Capture HH:MM
|
||||||
|
'(?:(:[0-9]{2})(?::([0-9]{5}))?)?', // Capture [:ss][:FFFFF]
|
||||||
|
'([+-][0-9]{1,2}:?(?:[0-9]{2})?)?', // Capture [+-][zone]
|
||||||
|
'$', // End
|
||||||
|
].join('')
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to parse time strings that possibly contain fractional
|
||||||
|
* seconds in 90k units into a Number representation.
|
||||||
|
*
|
||||||
|
* The general format:
|
||||||
|
* Expected timestamps are in this format:
|
||||||
|
* HH:MM[:ss][:FFFFF][[+-]hh[:mm]]
|
||||||
|
* where
|
||||||
|
* HH = hours in one or two digits
|
||||||
|
* MM = minutes in one or two digits
|
||||||
|
* ss = seconds in one or two digits
|
||||||
|
* FFFFF = fractional seconds in 90k units in exactly 5 digits
|
||||||
|
* hh = hours of timezone offset in one or two digits
|
||||||
|
* mm = minutes of timezone offset in one or two digits
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class Time90kParser {
|
||||||
|
/**
|
||||||
|
* Construct with specific timezone.
|
||||||
|
*
|
||||||
|
* @param {String} tz Timezone
|
||||||
|
*/
|
||||||
|
constructor(tz) {
|
||||||
|
self._tz = tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set (another) timezone.
|
||||||
|
*
|
||||||
|
* @param {String} tz Timezone
|
||||||
|
*/
|
||||||
|
set tz(tz) {
|
||||||
|
self._tz = tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given date and time string into a valid time90k or null.
|
||||||
|
*
|
||||||
|
* The date and time strings must be compatible with the partial ISO-8601
|
||||||
|
* formats for each, or acceptable to the standard Date object.
|
||||||
|
*
|
||||||
|
* If only a date is specified and dateOnlyThenEndOfDay is false, the 00:00
|
||||||
|
* timestamp for that day is returned. If dateOnlyThenEndOfDay is true, the
|
||||||
|
* 00:00 of the very next day is returned.
|
||||||
|
*
|
||||||
|
* @param {String} dateStr String representing date
|
||||||
|
* @param {String} timeStr String representing time
|
||||||
|
* @param {Boolean} dateOnlyThenEndOfDay If only a date was specified and
|
||||||
|
* this is true, then return time
|
||||||
|
* for the end of day
|
||||||
|
* @return {Number} Timestamp in 90k units, or null if parsing failed
|
||||||
|
*/
|
||||||
|
parseDateTime90k(dateStr, timeStr, dateOnlyThenEndOfDay) {
|
||||||
|
// If just date, no special handling needed
|
||||||
|
if (!timeStr) {
|
||||||
|
const m = moment.tz(dateStr, self._tz);
|
||||||
|
if (dateOnlyThenEndOfDay) {
|
||||||
|
m.add({days: 1});
|
||||||
|
}
|
||||||
|
return m.valueOf() * 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [match, hhmm, ss, fffff, tz] = timeRe.exec(timeStr) || [];
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orBlank = (s) => s || '';
|
||||||
|
const datetimeStr = dateStr + 'T' + hhmm + orBlank(ss) + orBlank(tz);
|
||||||
|
const m = moment.tz(datetimeStr, self._tz);
|
||||||
|
if (!m.isValid()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frac = fffff === undefined ? 0 : parseInt(fffff, 10);
|
||||||
|
return m.valueOf() * 90 + frac;
|
||||||
|
}
|
||||||
|
}
|
166
ui-src/lib/support/TimeFormatter.js
Normal file
166
ui-src/lib/support/TimeFormatter.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
export const internalTimeFormat = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ';
|
||||||
|
export const defaultTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for formatting timestamps.
|
||||||
|
*
|
||||||
|
* There are methods for formatting timestamp in three different unit systems:
|
||||||
|
* - 90k: The units are multiples of 1/90,000th of a second
|
||||||
|
* - Sec: The units are multiples of seconds
|
||||||
|
* - Ms: The units are multiples of milliseconds
|
||||||
|
*
|
||||||
|
* The object is initialized with a format string and a timezone. The timezone
|
||||||
|
* is necessary to format times in that timezone.
|
||||||
|
*
|
||||||
|
* The format string is based on those accepted by moment.js with one addition
|
||||||
|
* detailed in formatTimeStamp90k.
|
||||||
|
*/
|
||||||
|
export default class TimeFormatter {
|
||||||
|
/**
|
||||||
|
* Construct with specific format string and timezone.
|
||||||
|
*
|
||||||
|
* @param {String} formatStr Format specification string
|
||||||
|
* @param {String} tz Timezone, e.g. "America/Los_Angeles"
|
||||||
|
*/
|
||||||
|
constructor(formatStr, tz) {
|
||||||
|
this._formatStr = formatStr || defaultTimeFormat;
|
||||||
|
this._tz = tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current format string
|
||||||
|
*
|
||||||
|
* @return {String} Format specification string
|
||||||
|
*/
|
||||||
|
get formatStr() {
|
||||||
|
return this._formatStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current timezone
|
||||||
|
*
|
||||||
|
* @return {String} Timezone
|
||||||
|
*/
|
||||||
|
get tz() {
|
||||||
|
return this._tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a human-readable timestamp in 90k units.
|
||||||
|
*
|
||||||
|
* The format is anything understood by moment's format function,
|
||||||
|
* with the addition of one special format indicator consisting of
|
||||||
|
* five successive Fs. If this pattern is used more than once,
|
||||||
|
* only the first one will be handled. Subsequent ones will become
|
||||||
|
* literal strings with five Fs.
|
||||||
|
*
|
||||||
|
* Using normal format codes, precision of up the three S (SSS) is
|
||||||
|
* supported by moment to display decimal seconds. "moment" truncates
|
||||||
|
* the value passed in to its constructor, effectively truncating
|
||||||
|
* any fractional values in the timestamp. This function rounds
|
||||||
|
* to compensate for that, except in the case of the FFFFF pattern,
|
||||||
|
* where rounding is left out for historical reasons.
|
||||||
|
*
|
||||||
|
* FFFFF produces a string indicating how many 90k units are present
|
||||||
|
* in the sub-second portion of the timestamp. Therefore this is *not*
|
||||||
|
* a decimal fraction!
|
||||||
|
*
|
||||||
|
* @param {Number} ts90k timestamp in 90,000ths of a second resolution
|
||||||
|
* @return {String} Formatted timestamp
|
||||||
|
*/
|
||||||
|
formatTimeStamp90k(ts90k) {
|
||||||
|
let format = this._formatStr;
|
||||||
|
const ms = ts90k / 90.0;
|
||||||
|
const fracFmt = 'FFFFF';
|
||||||
|
let fracLoc = format.indexOf(fracFmt);
|
||||||
|
if (fracLoc != -1) {
|
||||||
|
const frac = ts90k % 90000;
|
||||||
|
format =
|
||||||
|
format.substr(0, fracLoc) +
|
||||||
|
String(100000 + frac).substr(1) +
|
||||||
|
format.substr(fracLoc + fracFmt.length);
|
||||||
|
}
|
||||||
|
return moment.tz(ms, this._tz).format(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specialized class similar to TimeFormatter but forcing a specific time format
|
||||||
|
* for internal usage purposes.
|
||||||
|
*/
|
||||||
|
export class TimeStamp90kFormatter {
|
||||||
|
/**
|
||||||
|
* Construct from just a timezone specification.
|
||||||
|
*
|
||||||
|
* @param {String} tz Timezone
|
||||||
|
*/
|
||||||
|
constructor(tz) {
|
||||||
|
this._formatter = new TimeFormatter(internalTimeFormat, tz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a timestamp in 90k units using internal format.
|
||||||
|
*
|
||||||
|
* @param {Number} ts90k timestamp in 90,000ths of a second resolution
|
||||||
|
* @return {String} Formatted timestamp
|
||||||
|
*/
|
||||||
|
formatTimeStamp90k(ts90k) {
|
||||||
|
return this._formatter.formatTimeStamp90k(ts90k);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two timestamp return formatted versions of both, where the second
|
||||||
|
* one may have been shortened if it falls on the same date as the first one.
|
||||||
|
*
|
||||||
|
* @param {Number} ts1 First timestamp in 90k units
|
||||||
|
* @param {Number} ts2 Secodn timestamp in 90k units
|
||||||
|
* @return {Array} Array with two elements: [ ts1Formatted, ts2Formatted ]
|
||||||
|
*/
|
||||||
|
formatSameDayShortened(ts1, ts2) {
|
||||||
|
let ts1Formatted = this.formatTimeStamp90k(ts1);
|
||||||
|
let ts2Formatted = this.formatTimeStamp90k(ts2);
|
||||||
|
let timePos = this._formatter.formatStr.indexOf('T');
|
||||||
|
if (timePos != -1) {
|
||||||
|
const datePortion = ts1Formatted.substr(0, timePos);
|
||||||
|
ts1Formatted = datePortion + ' ' + ts1Formatted.substr(timePos + 1);
|
||||||
|
if (ts2Formatted.startsWith(datePortion)) {
|
||||||
|
ts2Formatted = ts2Formatted.substr(timePos + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [ts1Formatted, ts2Formatted];
|
||||||
|
}
|
||||||
|
}
|
81
ui-src/lib/support/URLBuilder.js
Normal file
81
ui-src/lib/support/URLBuilder.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to help with URL construction.
|
||||||
|
*/
|
||||||
|
export default class URLBuilder {
|
||||||
|
/**
|
||||||
|
* Construct builder with a base url.
|
||||||
|
*
|
||||||
|
* It is possible to indicate the we only want to extract relative
|
||||||
|
* urls. In that case, pass a dummy scheme and host.
|
||||||
|
*
|
||||||
|
* @param {String} base Base url, including scheme and host
|
||||||
|
* @param {Boolean} relative True if relative urls desired
|
||||||
|
*/
|
||||||
|
constructor(base, relative = true) {
|
||||||
|
this._baseUrl = base;
|
||||||
|
this._relative = relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append query parameters from a map to a URL.
|
||||||
|
*
|
||||||
|
* This is cumulative, so if you call this multiple times on the same URL
|
||||||
|
* the resulting URL will have the combined query parameters and values.
|
||||||
|
*
|
||||||
|
* @param {URL} url URL to add query parameters to
|
||||||
|
* @param {Object} query Object with parameter name/value pairs
|
||||||
|
* @return {URL} URL where query params have been added
|
||||||
|
*/
|
||||||
|
_addQuery(url, query = {}) {
|
||||||
|
Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a String url based on an initial path and an optional set
|
||||||
|
* of query parameters.
|
||||||
|
*
|
||||||
|
* The url will be constructed based on the base url, with path appended.
|
||||||
|
*
|
||||||
|
* @param {String} path Path to be added to base url
|
||||||
|
* @param {Object} query Object with query parameters
|
||||||
|
* @return {String} Formatted url, relative if so configured
|
||||||
|
*/
|
||||||
|
makeUrl(path, query = {}) {
|
||||||
|
const url = new URL(path || '', this._baseUrl);
|
||||||
|
this._addQuery(url, query);
|
||||||
|
return this._relative ? url.pathname + url.search : url.href;
|
||||||
|
}
|
||||||
|
}
|
418
ui-src/lib/views/CalendarView.js
Normal file
418
ui-src/lib/views/CalendarView.js
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
import 'jquery-ui/themes/base/button.css';
|
||||||
|
import 'jquery-ui/themes/base/core.css';
|
||||||
|
import 'jquery-ui/themes/base/datepicker.css';
|
||||||
|
import 'jquery-ui/themes/base/dialog.css';
|
||||||
|
import 'jquery-ui/themes/base/resizable.css';
|
||||||
|
import 'jquery-ui/themes/base/theme.css';
|
||||||
|
import 'jquery-ui/themes/base/tooltip.css';
|
||||||
|
import 'jquery-ui/ui/widgets/datepicker';
|
||||||
|
import 'jquery-ui/ui/widgets/dialog';
|
||||||
|
import 'jquery-ui/ui/widgets/tooltip';
|
||||||
|
|
||||||
|
import DatePickerView from './DatePickerView';
|
||||||
|
import CalendarTSRange from '../models/CalendarTSRange';
|
||||||
|
import {TimeStamp90kFormatter} from '../support/TimeFormatter';
|
||||||
|
import Time90kParser from '../support/Time90kParser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the earliest and latest dates from an array of CameraView
|
||||||
|
* objects.
|
||||||
|
*
|
||||||
|
* Each camera view has a "days" property, whose keys identify days with
|
||||||
|
* recordings. All such dates are collected and then scanned to find earliest
|
||||||
|
* and latest dates.
|
||||||
|
*
|
||||||
|
* "days" for camera views that are not enabled are ignored.
|
||||||
|
*
|
||||||
|
* @param {[Iterable]} cameraViews Camera views to look into
|
||||||
|
* @return {[Set, String, String]} Array with set of all dates, and
|
||||||
|
* earliest and latest dates
|
||||||
|
*/
|
||||||
|
function minMaxDates(cameraViews) {
|
||||||
|
/*
|
||||||
|
* Produce a set with all dates, across all enabled cameras, that
|
||||||
|
* have at least one recording available (allDates).
|
||||||
|
*/
|
||||||
|
const allDates = new Set(
|
||||||
|
[].concat(
|
||||||
|
...cameraViews
|
||||||
|
.filter((v) => v.enabled)
|
||||||
|
.map((v) => Array.from(v.camera.days.keys()))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
allDates,
|
||||||
|
...Array.from(allDates.values()).reduce((acc, dateStr) => {
|
||||||
|
acc[0] = !acc[0] || dateStr < acc[0] ? dateStr : acc[0];
|
||||||
|
acc[1] = !acc[1] || dateStr > acc[1] ? dateStr : acc[1];
|
||||||
|
return acc;
|
||||||
|
}, []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to represent a calendar view.
|
||||||
|
*
|
||||||
|
* The view consists of:
|
||||||
|
* - Two date pickers (from and to)
|
||||||
|
* - A time input box with each date picker (from time, to time)
|
||||||
|
* - A set of radio buttons to select between same day or not
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class CalendarView {
|
||||||
|
/**
|
||||||
|
* Construct the view with UI elements IDs specified.
|
||||||
|
*
|
||||||
|
* @param {String} options.fromPickerId Id for from datepicker
|
||||||
|
* @param {String} options.toPickerId Id for to datepicker
|
||||||
|
* @param {String} options.isSameDayId Id for same day radio button
|
||||||
|
* @param {String} options.isOtherDayId Id for other day radio button
|
||||||
|
* @param {String} options.fromPickerTimeId Id for from time field
|
||||||
|
* @param {String} options.toPickerTimeId Id for to time field
|
||||||
|
* @param {[type]} options.timeZone Timezone
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
fromPickerId = 'start-date',
|
||||||
|
toPickerId = 'end-date',
|
||||||
|
isSameDayId = 'end-date-same',
|
||||||
|
isOtherDayId = 'end-date-other',
|
||||||
|
fromPickerTimeId = 'start-time',
|
||||||
|
toPickerTimeId = 'end-time',
|
||||||
|
timeZone = null,
|
||||||
|
} = {}) {
|
||||||
|
// Lookup all by id, check and remember
|
||||||
|
[
|
||||||
|
this._fromPickerView,
|
||||||
|
this._toPickerView,
|
||||||
|
this._sameDayElement,
|
||||||
|
this._otherDayElement,
|
||||||
|
this._startTimeElement,
|
||||||
|
this._endTimeElement,
|
||||||
|
] = [
|
||||||
|
fromPickerId,
|
||||||
|
toPickerId,
|
||||||
|
isSameDayId,
|
||||||
|
isOtherDayId,
|
||||||
|
fromPickerTimeId,
|
||||||
|
toPickerTimeId,
|
||||||
|
].map((id) => {
|
||||||
|
const el = $(`#${id}`);
|
||||||
|
if (el.length == 0) {
|
||||||
|
console.log('Warning: Calendar element with id "' + id + '" not found');
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
this._fromPickerView = new DatePickerView(this._fromPickerView);
|
||||||
|
this._toPickerView = new DatePickerView(this._toPickerView);
|
||||||
|
this._timeFormatter = new TimeStamp90kFormatter(timeZone);
|
||||||
|
this._timeParser = new Time90kParser(timeZone);
|
||||||
|
this._selectedRange = new CalendarTSRange(timeZone);
|
||||||
|
this._sameDay = true; // Start in single day view
|
||||||
|
this._sameDayElement.prop('checked', this._sameDay);
|
||||||
|
this._otherDayElement.prop('checked', !this._sameDay);
|
||||||
|
this._availableDates = null;
|
||||||
|
this._minDateStr = null;
|
||||||
|
this._maxDateStr = null;
|
||||||
|
this._singleDateStr = null;
|
||||||
|
this._cameraViews = null;
|
||||||
|
this._rangeChangedHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change timezone.
|
||||||
|
*
|
||||||
|
* @param {String} tz New timezone
|
||||||
|
*/
|
||||||
|
set tz(tz) {
|
||||||
|
this._timeParser.tz = tz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Re)configure the datepickers and other calendar range inputs to reflect
|
||||||
|
* available dates.
|
||||||
|
*/
|
||||||
|
_configureDatePickers() {
|
||||||
|
const dateSet = this._availableDates;
|
||||||
|
const minDateStr = this._minDateStr;
|
||||||
|
const maxDateStr = this._maxDateStr;
|
||||||
|
const fromPickerView = this._fromPickerView;
|
||||||
|
const toPickerView = this._toPickerView;
|
||||||
|
const beforeShowDay = function(date) {
|
||||||
|
let dateStr = date.toISOString().substr(0, 10);
|
||||||
|
return [dateSet.has(dateStr), '', ''];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._sameDay) {
|
||||||
|
fromPickerView.option({
|
||||||
|
dateFormat: $.datepicker.ISO_8601,
|
||||||
|
minDate: minDateStr,
|
||||||
|
maxDate: maxDateStr,
|
||||||
|
onSelect: (dateStr, picker) => this._updateRangeDates(dateStr, dateStr),
|
||||||
|
beforeShowDay: beforeShowDay,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
toPickerView.destroy();
|
||||||
|
toPickerView.activate(); // Default state, but alive
|
||||||
|
} else {
|
||||||
|
fromPickerView.option({
|
||||||
|
dateFormat: $.datepicker.ISO_8601,
|
||||||
|
minDate: minDateStr,
|
||||||
|
onSelect: (dateStr, picker) => {
|
||||||
|
toPickerView.option('minDate', this.fromDateISO);
|
||||||
|
this._updateRangeDates(dateStr, this.toDateISO);
|
||||||
|
},
|
||||||
|
beforeShowDay: beforeShowDay,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
toPickerView.option({
|
||||||
|
dateFormat: $.datepicker.ISO_8601,
|
||||||
|
minDate: fromPickerView.dateISO,
|
||||||
|
maxDate: maxDateStr,
|
||||||
|
onSelect: (dateStr, picker) => {
|
||||||
|
fromPickerView.option('maxDate', this.toDateISO);
|
||||||
|
this._updateRangeDates(this.fromDateISO, dateStr);
|
||||||
|
},
|
||||||
|
beforeShowDay: beforeShowDay,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
toPickerView.date = fromPickerView.date;
|
||||||
|
fromPickerView.maxDate = toPickerView.date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call an installed handler (if any) to inform that range has changed.
|
||||||
|
*/
|
||||||
|
_informRangeChange() {
|
||||||
|
if (this._rangeChangedHandler !== null) {
|
||||||
|
this._rangeChangedHandler(this._selectedRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a change in the time input of either from or to.
|
||||||
|
*
|
||||||
|
* The change requires updating the selected range and then informing
|
||||||
|
* any listeners that the range has changed (so they can update data).
|
||||||
|
*
|
||||||
|
* @param {Object} event Time Event on DOM that triggered calling this
|
||||||
|
* @param {Boolean} isEnd True if this is for end time
|
||||||
|
*/
|
||||||
|
_pickerTimeChanged(event, isEnd) {
|
||||||
|
const pickerElement = event.currentTarget;
|
||||||
|
const newTimeStr = pickerElement.value;
|
||||||
|
const selectedRange = this._selectedRange;
|
||||||
|
const parsedTS = isEnd
|
||||||
|
? selectedRange.setEndTime(newTimeStr)
|
||||||
|
: selectedRange.setStartTime(newTimeStr);
|
||||||
|
if (parsedTS === null) {
|
||||||
|
console.log('bad time change');
|
||||||
|
$(pickerElement).addClass('ui-state-error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$(pickerElement).removeClass('ui-state-error');
|
||||||
|
console.log(
|
||||||
|
(isEnd ? 'End' : 'Start') +
|
||||||
|
' time changed to: ' +
|
||||||
|
parsedTS +
|
||||||
|
' (' +
|
||||||
|
this._timeFormatter.formatTimeStamp90k(parsedTS) +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
this._informRangeChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a change in the calendar's same/other day settings.
|
||||||
|
*
|
||||||
|
* The change means the selected range changes.
|
||||||
|
*
|
||||||
|
* @param {Boolean} isSameDay True if the same day radio button was activated
|
||||||
|
*/
|
||||||
|
_pickerSameDayChanged(isSameDay) {
|
||||||
|
// Prevent change if not real change (can happen on initial setup)
|
||||||
|
if (this._sameDay != isSameDay) {
|
||||||
|
/**
|
||||||
|
* This is called when the status of the same/other day radio buttons
|
||||||
|
* changes. We need to determine a new selected range and activiate it.
|
||||||
|
* Doing so will then also inform the change listener.
|
||||||
|
*/
|
||||||
|
const endDate = isSameDay
|
||||||
|
? this.selectedRange.start.dateStr
|
||||||
|
: this.selectedRange.end.dateStr;
|
||||||
|
this._updateRangeDates(this.selectedRange.start.dateStr, endDate);
|
||||||
|
this._sameDay = isSameDay;
|
||||||
|
|
||||||
|
// Switch between single day and multi-day
|
||||||
|
this._configureDatePickers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflect a change in start and end date in the calendar view.
|
||||||
|
*
|
||||||
|
* The selected range is update, the view is reconfigured as necessary and
|
||||||
|
* any listeners are informed.
|
||||||
|
*
|
||||||
|
* @param {String} startDateStr New starting date
|
||||||
|
* @param {String} endDateStr New ending date
|
||||||
|
*/
|
||||||
|
_updateRangeDates(startDateStr, endDateStr) {
|
||||||
|
const newRange = this._selectedRange;
|
||||||
|
const originalStart = Object.assign({}, newRange.start);
|
||||||
|
const originalEnd = Object.assign({}, newRange.end);
|
||||||
|
newRange.setStartDate(startDateStr);
|
||||||
|
newRange.setEndDate(endDateStr);
|
||||||
|
|
||||||
|
const isSameRange = (a, b) => {
|
||||||
|
return (
|
||||||
|
a.dateStr == b.dateStr && a.timeStr == b.timeStr && a.ts90k == b.ts90k
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do nothing if effectively no change
|
||||||
|
if (
|
||||||
|
!isSameRange(newRange.start, originalStart) ||
|
||||||
|
!isSameRange(newRange.end, originalEnd)
|
||||||
|
) {
|
||||||
|
console.log('New range: ' + startDateStr + ' - ' + endDateStr);
|
||||||
|
this._informRangeChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install event handlers for same/other day radio buttons and the
|
||||||
|
* time input boxes as both need to result in an update of the calendar
|
||||||
|
* view.
|
||||||
|
*/
|
||||||
|
_wireControls() {
|
||||||
|
// If same day status changed, update the view
|
||||||
|
this._sameDayElement.change(() => this._pickerSameDayChanged(true));
|
||||||
|
this._otherDayElement.change(() => this._pickerSameDayChanged(false));
|
||||||
|
|
||||||
|
// Handle changing of a time input (either from or to)
|
||||||
|
const handler = (e, isEnd) => {
|
||||||
|
console.log('Time changed for: ' + (isEnd ? 'end' : 'start'));
|
||||||
|
this._pickerTimeChanged(e, isEnd);
|
||||||
|
};
|
||||||
|
this._startTimeElement.change((e) => handler(e, false));
|
||||||
|
this._endTimeElement.change((e) => handler(e, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Re)Initialize the calendar based on a collection of camera views.
|
||||||
|
*
|
||||||
|
* This is necessary as the camera views ultimately define the limits on
|
||||||
|
* the date pickers.
|
||||||
|
*
|
||||||
|
* @param {Iterable} cameraViews Collection of camera views
|
||||||
|
*/
|
||||||
|
initializeWith(cameraViews) {
|
||||||
|
this._cameraViews = cameraViews;
|
||||||
|
[this._availableDates, this._minDateStr, this._maxDateStr] = minMaxDates(
|
||||||
|
cameraViews
|
||||||
|
);
|
||||||
|
this._configureDatePickers();
|
||||||
|
|
||||||
|
// Initialize the selected range to the from picker's date
|
||||||
|
// if we do not have a selected range yet
|
||||||
|
if (!this.selectedRange.hasStart()) {
|
||||||
|
const date = this.fromDateISO;
|
||||||
|
this._updateRangeDates(date, date);
|
||||||
|
this._wireControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler to be called when the calendar selection range changes.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument, an object of type
|
||||||
|
* CalendarTSRange reflecting the current calendar range. It will be called
|
||||||
|
* whenever that range changes.
|
||||||
|
*
|
||||||
|
* @param {Function} handler Function that will be called
|
||||||
|
*/
|
||||||
|
set onRangeChange(handler) {
|
||||||
|
this._rangeChangedHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "to" selected date as Date object.
|
||||||
|
*
|
||||||
|
* @return {Date} Date value of the "to"date picker
|
||||||
|
*/
|
||||||
|
get toDate() {
|
||||||
|
return this._toPickerView.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "from" selected date as Date object.
|
||||||
|
*
|
||||||
|
* @return {Date} Date value of the "from"date picker
|
||||||
|
*/
|
||||||
|
get fromDate() {
|
||||||
|
return this._fromPickerView.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "to" selected date as the date component of an ISO-8601
|
||||||
|
* formatted string.
|
||||||
|
*
|
||||||
|
* @return {String} Date value (YYYY-MM-D) of the "to" date picker
|
||||||
|
*/
|
||||||
|
get toDateISO() {
|
||||||
|
return this._toPickerView.dateISO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "from" selected date as the date component of an ISO-8601
|
||||||
|
* formatted string.
|
||||||
|
*
|
||||||
|
* @return {String} Date value (YYYY-MM-D) of the "from" date picker
|
||||||
|
*/
|
||||||
|
get fromDateISO() {
|
||||||
|
return this._fromPickerView.dateISO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently selected range in the calendar view.
|
||||||
|
*
|
||||||
|
* @return {CalendarTSRange} Range object
|
||||||
|
*/
|
||||||
|
get selectedRange() {
|
||||||
|
return this._selectedRange;
|
||||||
|
}
|
||||||
|
}
|
174
ui-src/lib/views/CameraView.js
Normal file
174
ui-src/lib/views/CameraView.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import RecordingsView from './RecordingsView';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class handling a camer view.
|
||||||
|
*
|
||||||
|
* A camera view consists of a list of available recording segments for
|
||||||
|
* playback.
|
||||||
|
*/
|
||||||
|
export default class CameraView {
|
||||||
|
/**
|
||||||
|
* Construct the view.
|
||||||
|
*
|
||||||
|
* @param {Camera} cameraModel Model object for camera
|
||||||
|
* @param {[type]} recordingFormatter Formatter to be used by recordings
|
||||||
|
* @param {[type]} trimmed True if rec. ranges should be trimmed
|
||||||
|
* @param {[type]} recordingsParent Parent element to attach to or null)
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
cameraModel,
|
||||||
|
recordingFormatter,
|
||||||
|
trimmed,
|
||||||
|
recordingsParent = null
|
||||||
|
) {
|
||||||
|
this.camera = cameraModel;
|
||||||
|
this.recordingsView = new RecordingsView(
|
||||||
|
this.camera,
|
||||||
|
recordingFormatter,
|
||||||
|
trimmed,
|
||||||
|
recordingsParent
|
||||||
|
);
|
||||||
|
this._enabled = true;
|
||||||
|
this.recordingsUrl = null;
|
||||||
|
this.recordingsReq = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the view is enabled or not.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
get enabled() {
|
||||||
|
return this._enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change enabled state of the view.
|
||||||
|
*
|
||||||
|
* @param {Boolean} enabled Whether view should be enabled
|
||||||
|
*/
|
||||||
|
set enabled(enabled) {
|
||||||
|
this._enabled = enabled;
|
||||||
|
this.recordingsView.show = enabled;
|
||||||
|
console.log(
|
||||||
|
'Camera ',
|
||||||
|
this.camera.shortName,
|
||||||
|
this.enabled ? 'enabled' : 'disabled'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently remembered recordings range for this camera.
|
||||||
|
*
|
||||||
|
* This is just passed on to the recordings view.
|
||||||
|
*
|
||||||
|
* @return {Range90k} Currently remembered range
|
||||||
|
*/
|
||||||
|
get recordingsRange() {
|
||||||
|
return this.recordingsView.recordingsRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the recordings range for this view.
|
||||||
|
*
|
||||||
|
* This is just passed on to the recordings view.
|
||||||
|
*
|
||||||
|
* @param {Range90k} range90k Range to remember
|
||||||
|
*/
|
||||||
|
set recordingsRange(range90k) {
|
||||||
|
this.recordingsView.recordingsRange = range90k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether loading indicator should be shown or not.
|
||||||
|
*
|
||||||
|
* This indicator is really on the recordings list.
|
||||||
|
*
|
||||||
|
* @param {Boolean} show True if indicator should be showing
|
||||||
|
*/
|
||||||
|
set showLoading(show) {
|
||||||
|
this.recordingsView.showLoading = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the loading indicated after a delay, unless the timer has been
|
||||||
|
* cleared already.
|
||||||
|
*
|
||||||
|
* @param {Number} timeOutMs Delay (in ms) before indicator should appear
|
||||||
|
*/
|
||||||
|
delayedShowLoading(timeOutMs) {
|
||||||
|
this.recordingsView.delayedShowLoading(timeOutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set new recordings from JSON data.
|
||||||
|
*
|
||||||
|
* @param {Object} dataJSON JSON data (array)
|
||||||
|
*/
|
||||||
|
set recordingsJSON(dataJSON) {
|
||||||
|
this.recordingsView.recordingsJSON = dataJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new time format string for the recordings list.
|
||||||
|
*
|
||||||
|
* @param {String} formatStr Formatting string
|
||||||
|
*/
|
||||||
|
set timeFormat(formatStr) {
|
||||||
|
this.recordingsView.timeFormat = formatStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the trimming option of the cameraview as desired.
|
||||||
|
*
|
||||||
|
* This is really just passed on to the recordings view.
|
||||||
|
*
|
||||||
|
* @param {Boolean} enabled True if trimming should be enabled
|
||||||
|
*/
|
||||||
|
set trimmed(enabled) {
|
||||||
|
this.recordingsView.trimmed = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler for clicks on a recording.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument, the recording model.
|
||||||
|
*
|
||||||
|
* @param {Function} h Handler function
|
||||||
|
*/
|
||||||
|
set onRecordingClicked(h) {
|
||||||
|
this.recordingsView.onRecordingClicked = h;
|
||||||
|
}
|
||||||
|
}
|
113
ui-src/lib/views/CheckboxGroupView.js
Normal file
113
ui-src/lib/views/CheckboxGroupView.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle a group of (related) checkboxes.
|
||||||
|
*
|
||||||
|
* Each checkbox is managed through a simple object containing properties:
|
||||||
|
* - id: {String} Id (some unique value within the group)
|
||||||
|
* - selector: {String} jQuery compatible selector to find the dom element
|
||||||
|
* - checked: {Boolean} Value for checkbox
|
||||||
|
* - jq: {jQuery} jQuery element for the checkbox, or null if not found
|
||||||
|
*
|
||||||
|
* A handler can be called if a checbox changes value.
|
||||||
|
*/
|
||||||
|
export default class CheckboxGroupView {
|
||||||
|
/**
|
||||||
|
* Construct the seteup for the checkboxes.
|
||||||
|
*
|
||||||
|
* The passed group array should contain individual maps describing each
|
||||||
|
* checkbox. THe maps should contain:
|
||||||
|
* - id
|
||||||
|
* - selector: optional. If not provided #id will be used
|
||||||
|
* - checked: Initial value for checkbox, default true
|
||||||
|
* - text: Text for the checkbox label (not generated if empty)
|
||||||
|
*
|
||||||
|
* @param {Array} group Array of maps, one for each checkbox
|
||||||
|
* @param {jQuery} parent jQuery parent element to append to
|
||||||
|
*/
|
||||||
|
constructor(group = [], parent = null) {
|
||||||
|
this._group = group.slice(); // Copy
|
||||||
|
this._group.forEach((element) => {
|
||||||
|
// If parent specified, create and append
|
||||||
|
if (parent) {
|
||||||
|
let cb = `<input type="checkbox" id="${element.id}" name="${
|
||||||
|
element.id
|
||||||
|
}">`;
|
||||||
|
if (element.text) {
|
||||||
|
cb += `<label for="${element.id}">${element.text}</label>`;
|
||||||
|
}
|
||||||
|
parent.append($(cb + '<br/>'));
|
||||||
|
}
|
||||||
|
const jq = $(element.selector || `#${element.id}`);
|
||||||
|
element.jq = jq;
|
||||||
|
if (jq !== null) {
|
||||||
|
jq.prop('checked', element.checked || true);
|
||||||
|
jq.change((e) => {
|
||||||
|
if (this._checkChangeHandler) {
|
||||||
|
element.checked = e.target.checked;
|
||||||
|
this._checkChangeHandler(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._checkChangeHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the checkbox object for the specified checkbox.
|
||||||
|
*
|
||||||
|
* The checkbox is looked up by the specified id or selector, which must
|
||||||
|
* match what was specified during construction.
|
||||||
|
*
|
||||||
|
* @param {String} idOrSelector Identifying string
|
||||||
|
* @return {Object} Object for checkbox, or null if not found
|
||||||
|
*/
|
||||||
|
checkBox(idOrSelector) {
|
||||||
|
return this._group.find(
|
||||||
|
(el) => el.id === idOrSelector || el.selector === idOrSelector
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler for checkbox changes.
|
||||||
|
*
|
||||||
|
* Handler will be called with same result as would be found by checkBox().
|
||||||
|
*
|
||||||
|
* @param {Function} handler function (checbox)
|
||||||
|
*/
|
||||||
|
set onCheckChange(handler) {
|
||||||
|
this._checkChangeHandler = handler;
|
||||||
|
}
|
||||||
|
}
|
283
ui-src/lib/views/DatePickerView.js
Normal file
283
ui-src/lib/views/DatePickerView.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to encapsulate datepicker UI widget from jQuery.
|
||||||
|
*/
|
||||||
|
export default class DatePickerView {
|
||||||
|
/**
|
||||||
|
* Construct wapper an attach to a specified parent DOM node.
|
||||||
|
*
|
||||||
|
* @param {Node} parent Note to serve for attachign datepicker
|
||||||
|
* @param {Object} options Options to pass to datepicker
|
||||||
|
*/
|
||||||
|
constructor(parent, options = null) {
|
||||||
|
this._pickerElement = $(parent);
|
||||||
|
/*
|
||||||
|
* The widget is somewhat peculiar in that its functionality does
|
||||||
|
* not exist until it has been called with a settings/options argument
|
||||||
|
* as the only parameter to the datepicker() function.
|
||||||
|
* So, unless some are passed in here explicitly, we create a default
|
||||||
|
* and disabled date picker.
|
||||||
|
*/
|
||||||
|
this._initWithOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the date picker with a set of options.
|
||||||
|
*
|
||||||
|
* Attach the datepicker function to its parent and set the specified options.
|
||||||
|
* If the options are not specified a minimum set of options just enabling the
|
||||||
|
* datepicker with defaults is used.
|
||||||
|
*
|
||||||
|
* @param {Object} options Options for datepicker, or null to just enable
|
||||||
|
*/
|
||||||
|
_initWithOptions(options = null) {
|
||||||
|
this._alive = true;
|
||||||
|
options =
|
||||||
|
options !== null
|
||||||
|
? options
|
||||||
|
: {
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
this._pickerElement.datepicker(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a specified datepicker function, passing the arguments.
|
||||||
|
*
|
||||||
|
* This function exists to catch the cases where functions are called when
|
||||||
|
* the picker is not attached (alive).
|
||||||
|
*
|
||||||
|
* The first argument to this function should be the name of the desired
|
||||||
|
* datepicker function, followed by the correct arguments for that function.
|
||||||
|
*
|
||||||
|
* @return {Any} Function result
|
||||||
|
*/
|
||||||
|
_apply() {
|
||||||
|
if (!this._alive) {
|
||||||
|
console.log('WARN: datepicker not constructed yet [' + this.domId + ']');
|
||||||
|
}
|
||||||
|
return this._pickerElement.datepicker(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate the datepicker if not already attached.
|
||||||
|
*
|
||||||
|
* Basically calls _initWithOptions({disabled: disabled}), but only if not
|
||||||
|
* already attached. Otherwise just sets the disabled status.
|
||||||
|
*
|
||||||
|
* @param {Boolean} disabled True if datepicker needs to be disabled
|
||||||
|
*/
|
||||||
|
activate(disabled = true) {
|
||||||
|
if (this._alive) {
|
||||||
|
this.disabled = disabled;
|
||||||
|
} else {
|
||||||
|
this._initWithOptions({
|
||||||
|
disabled: disabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the element the datepicker is attached to.
|
||||||
|
*
|
||||||
|
* @return {jQuery} jQuery element
|
||||||
|
*/
|
||||||
|
get element() {
|
||||||
|
return this._pickerElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set option or options to the datepicker, like the 'option' call with
|
||||||
|
* various arguments.
|
||||||
|
*
|
||||||
|
* Special case is when the datepicker is not (yet) attached. In that case
|
||||||
|
* we need to initialze the datepicker with the options instead.
|
||||||
|
*
|
||||||
|
* @param {object} arg0 First parameter or undefined if not given
|
||||||
|
* @param {array} args Rest of the parameters (might be empty)
|
||||||
|
* @return {object} Result of the 'option' call.
|
||||||
|
*/
|
||||||
|
option(arg0, ...args) {
|
||||||
|
/*
|
||||||
|
* Special case the scenario of calling option setting with just a map of
|
||||||
|
* settings, when the picker is not alive. That really should translate
|
||||||
|
* to a constructor call to the datepicker.
|
||||||
|
*/
|
||||||
|
if (!this._alive && args.length === 0 && typeof arg0 === 'object') {
|
||||||
|
return this._initWithOptions(arg0);
|
||||||
|
}
|
||||||
|
return this._apply('option', arg0, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current set of options.
|
||||||
|
*
|
||||||
|
* This is special cased here vs. documentation. We need to ask for 'all'.
|
||||||
|
*
|
||||||
|
* @return {Object} Datepicker options
|
||||||
|
*/
|
||||||
|
options() {
|
||||||
|
return this.option('all');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether datepicker is disabled.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
get isDisabled() {
|
||||||
|
return this._apply('isDisabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set disabled status of picker.
|
||||||
|
*
|
||||||
|
* @param {Boolean} disabled True to disable
|
||||||
|
*/
|
||||||
|
set disabled(disabled) {
|
||||||
|
this.option('disabled', disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get picker's currently selected date.
|
||||||
|
*
|
||||||
|
* @return {Date} Selected date
|
||||||
|
*/
|
||||||
|
get date() {
|
||||||
|
return this._apply('getDate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the datepicker to a specific date.
|
||||||
|
*
|
||||||
|
* @param {String|Date} date Desired date as string or Date
|
||||||
|
*/
|
||||||
|
set date(date) {
|
||||||
|
this._apply('setDate', date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the picker's current date in ISO format.
|
||||||
|
*
|
||||||
|
* This will return just the date portion of the ISO-8601 format, or in other
|
||||||
|
* words: YYYY-MM-DD
|
||||||
|
*
|
||||||
|
* @return {String} Date portion of ISO-8601 formatted selected date
|
||||||
|
*/
|
||||||
|
get dateISO() {
|
||||||
|
return this.date.toISOString().substr(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently set minimum date.
|
||||||
|
*
|
||||||
|
* @return {Date} Minimum date
|
||||||
|
*/
|
||||||
|
get minDate() {
|
||||||
|
return this.option('minDate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new minimum date.
|
||||||
|
*
|
||||||
|
* @param {String|Date} value Desired minimum date
|
||||||
|
*/
|
||||||
|
set minDate(value) {
|
||||||
|
this.option('minDate', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently set maximum date.
|
||||||
|
*
|
||||||
|
* @return {Date} Maximum date
|
||||||
|
*/
|
||||||
|
get maxDate() {
|
||||||
|
return this.option('maxDate');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new maximum date.
|
||||||
|
*
|
||||||
|
* @param {String|Date} value Desired maximum date
|
||||||
|
*/
|
||||||
|
set maxDate(value) {
|
||||||
|
this.option('maxDate', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the picker to open up in a dialog.
|
||||||
|
*
|
||||||
|
* This takes a variable number of arguments, like the native dialog function.
|
||||||
|
*
|
||||||
|
* @param {varargs} dialogArgs Variable argument list
|
||||||
|
*/
|
||||||
|
dialog(...dialogArgs) {
|
||||||
|
this._apply('option', dialogArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the picker visible.
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
this._apply('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the picker.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this._apply('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the picker.
|
||||||
|
*/
|
||||||
|
refresh() {
|
||||||
|
this._apply('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the picker.
|
||||||
|
*
|
||||||
|
* Destroy means detach it from its element and dispose of everything.
|
||||||
|
* Sets the status in this object to !alive.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
this._alive = true;
|
||||||
|
this._apply('destroy');
|
||||||
|
this._alive = false;
|
||||||
|
}
|
||||||
|
}
|
205
ui-src/lib/views/NVRSettingsView.js
Normal file
205
ui-src/lib/views/NVRSettingsView.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to control the view of NVR Settings.
|
||||||
|
*
|
||||||
|
* These settings/controls include:
|
||||||
|
* - Max video length
|
||||||
|
* - Trim segment start/end
|
||||||
|
* - Time Format
|
||||||
|
*/
|
||||||
|
export default class NVRSettingsView {
|
||||||
|
/**
|
||||||
|
* Construct based on element ids
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
videoLenId = 'split',
|
||||||
|
trimCheckId = 'trim',
|
||||||
|
tsTrackId = 'ts',
|
||||||
|
timeFmtId = 'timefmt',
|
||||||
|
} = {}) {
|
||||||
|
this._ids = {videoLenId, trimCheckId, tsTrackId, timeFmtId};
|
||||||
|
this._videoLength = null;
|
||||||
|
this._videoLengthHandler = null;
|
||||||
|
this._trim = null;
|
||||||
|
this._trimHandler = null;
|
||||||
|
this._timeFmtStr = null;
|
||||||
|
this._timeFmtHandler = null;
|
||||||
|
this._tsTrack = null;
|
||||||
|
this._tsTrackHandler = null;
|
||||||
|
this._wireControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find selected option in <select> and return value, or first option's value.
|
||||||
|
*
|
||||||
|
* The first option's value is returned if no option is selected.
|
||||||
|
*
|
||||||
|
* @param {jQuery} selectEl jQuery element for the <select>
|
||||||
|
* @return {String} Value of the selected/first option
|
||||||
|
*/
|
||||||
|
_findSelectedOrFirst(selectEl) {
|
||||||
|
let value = selectEl.find(':selected').val();
|
||||||
|
if (!value) {
|
||||||
|
value = selectEl.find('option:first-child').val();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire up all controls and handlers.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_wireControls() {
|
||||||
|
const videoLengthEl = $(`#${this._ids.videoLenId}`);
|
||||||
|
this._videoLength = this._findSelectedOrFirst(videoLengthEl);
|
||||||
|
videoLengthEl.change((e) => {
|
||||||
|
const newValueStr = e.currentTarget.value;
|
||||||
|
this._videoLength =
|
||||||
|
newValueStr == 'infinite' ? Infinity : Number(newValueStr);
|
||||||
|
if (this._videoLengthHandler) {
|
||||||
|
this._videoLengthHandler(this._videoLength);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trimEl = $(`#${this._ids.trimCheckId}`);
|
||||||
|
this._trim = trimEl.is(':checked');
|
||||||
|
trimEl.change((e) => {
|
||||||
|
this._trim = e.currentTarget.checked;
|
||||||
|
if (this._trimHandler) {
|
||||||
|
this._trimHandler(this._trim);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeFmtEl = $(`#${this._ids.timeFmtId}`);
|
||||||
|
this._timeFmtStr = this._findSelectedOrFirst(timeFmtEl);
|
||||||
|
timeFmtEl.change((e) => {
|
||||||
|
this._timeFmtStr = e.target.value;
|
||||||
|
if (this._timeFmtHandler) {
|
||||||
|
this._timeFmtHandler(this._timeFmtStr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trackEl = $(`#${this._ids.tsTrackId}`);
|
||||||
|
this._tsTrack = trackEl.is(':checked');
|
||||||
|
trackEl.change((e) => {
|
||||||
|
this._tsTrack = e.target.checked;
|
||||||
|
if (this._tsTrackHandler) {
|
||||||
|
this._tsTrackHandler(this._tsTrack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently selected video length.
|
||||||
|
*
|
||||||
|
* @return {Number} Video length value
|
||||||
|
*/
|
||||||
|
get videoLength() {
|
||||||
|
return this._videoLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently selected time format string.
|
||||||
|
*
|
||||||
|
* @return {String} Format string
|
||||||
|
*/
|
||||||
|
get timeFormatString() {
|
||||||
|
return this._timeFmtStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently selected trim setting.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
get trim() {
|
||||||
|
return this._trim;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine value of timestamp tracking option
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
get timeStampTrack() {
|
||||||
|
return this._tsTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler to be called when the time format string changes.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument: the new format string.
|
||||||
|
*
|
||||||
|
* @param {Function} handler Format change handler
|
||||||
|
*/
|
||||||
|
set onTimeFormatChange(handler) {
|
||||||
|
this._timeFmtHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler to be called when video length popup changes.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument: the new video length.
|
||||||
|
*
|
||||||
|
* @param {Function} handler Video Length change handler
|
||||||
|
*/
|
||||||
|
set onVideoLengthChange(handler) {
|
||||||
|
this._videoLengthHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler to be called when video trim checkbox changes.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument: the new trim value (Boolean).
|
||||||
|
*
|
||||||
|
* @param {Function} handler Trim change handler
|
||||||
|
*/
|
||||||
|
set onTrimChange(handler) {
|
||||||
|
this._trimHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler to be called when video timestamp tracking checkbox changes.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument: the new tsTrack value
|
||||||
|
* (Boolean).
|
||||||
|
*
|
||||||
|
* @param {Function} handler Timestamp track change handler
|
||||||
|
*/
|
||||||
|
set onTimeStampTrackChange(handler) {
|
||||||
|
this._tsTrackHandler = handler;
|
||||||
|
}
|
||||||
|
}
|
283
ui-src/lib/views/RecordingsView.js
Normal file
283
ui-src/lib/views/RecordingsView.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
// vim: set et sw=2 ts=2:
|
||||||
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
import Recording from '../models/Recording';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desired column order in recordings table.
|
||||||
|
*
|
||||||
|
* The column names must correspond to the propertu names in the JSON
|
||||||
|
* representation of recordings.
|
||||||
|
*
|
||||||
|
* @todo This should be decoupled!
|
||||||
|
*
|
||||||
|
* @type {Array} Array of column names
|
||||||
|
*/
|
||||||
|
const _columnOrder = [
|
||||||
|
'start',
|
||||||
|
'end',
|
||||||
|
'resolution',
|
||||||
|
'frameRate',
|
||||||
|
'size',
|
||||||
|
'rate',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Labels for columns.
|
||||||
|
*/
|
||||||
|
const _columnLabels = {
|
||||||
|
start: 'Start',
|
||||||
|
end: 'End',
|
||||||
|
resolution: 'Resolution',
|
||||||
|
frameRate: 'FPS',
|
||||||
|
size: 'Storage',
|
||||||
|
rate: 'BitRate',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to encapsulate a view of a list of recordings from a single camera.
|
||||||
|
*/
|
||||||
|
export default class RecordingsView {
|
||||||
|
/**
|
||||||
|
* Construct display from camera data and use supplied formatter.
|
||||||
|
*
|
||||||
|
* @param {Camera} camera camera object (immutable)
|
||||||
|
* @param {RecordingFormatter} recordingFormatter Desired formatter
|
||||||
|
* @param {Boolean} trimmed True if the display should include trimmed ranges
|
||||||
|
* @param {jQuery} parent Parent to which new DOM is attached, or null
|
||||||
|
*/
|
||||||
|
constructor(camera, recordingFormatter, trimmed = false, parent = null) {
|
||||||
|
this._cameraName = camera.shortName;
|
||||||
|
this._cameraRange = camera.range90k;
|
||||||
|
this._formatter = recordingFormatter;
|
||||||
|
this._element = $(`tab-${camera.uuid}`); // Might not be there initially
|
||||||
|
if (this._element.length == 0) {
|
||||||
|
this._element = this._createElement(camera.uuid, camera.shortName);
|
||||||
|
}
|
||||||
|
this._trimmed = trimmed;
|
||||||
|
this._recordings = null;
|
||||||
|
this._recordingsRange = null;
|
||||||
|
this._clickHandler = null;
|
||||||
|
if (parent) {
|
||||||
|
parent.append(this._element);
|
||||||
|
}
|
||||||
|
this._timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create DOM for the recording.
|
||||||
|
*
|
||||||
|
* @param {String} id DOM id for the main element
|
||||||
|
* @param {String} cameraName Name of the corresponding camera
|
||||||
|
* @return {jQuery} Partial DOM as jQuery object
|
||||||
|
*/
|
||||||
|
_createElement(id, cameraName) {
|
||||||
|
const tab = $('<tbody>').attr('id', id);
|
||||||
|
tab.append(
|
||||||
|
$('<tr class="name">').append($('<th colspan=6/>').text(cameraName)),
|
||||||
|
$('<tr class="hdr">').append(
|
||||||
|
$(
|
||||||
|
_columnOrder
|
||||||
|
.map((name) => '<th>' + _columnLabels[name] + '</th>')
|
||||||
|
.join('')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
$('</tr>'),
|
||||||
|
$('<tr class="loading"><td colspan=6>loading...</td></tr>').hide()
|
||||||
|
);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update display for new recording values.
|
||||||
|
*
|
||||||
|
* Each existing row is reformatted.
|
||||||
|
*
|
||||||
|
* @param {Array} newRecordings
|
||||||
|
* @param {Boolean} trimmed True if timestamps should be trimmed
|
||||||
|
*/
|
||||||
|
_updateRecordings() {
|
||||||
|
const trimRange = this._trimmed ? this.recordingsRange : null;
|
||||||
|
const recordings = this._recordings;
|
||||||
|
this._element.children('tr.r').each((rowIndex, row) => {
|
||||||
|
const values = this._formatter.format(recordings[rowIndex], trimRange);
|
||||||
|
$(row)
|
||||||
|
.children('td')
|
||||||
|
.each((index, element) => $(element).text(values[_columnOrder[index]]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently remembered recordings range for this view.
|
||||||
|
*
|
||||||
|
* This range corresponds to what was in the data time range selector UI
|
||||||
|
* at the time the data for this view was selected. The value is remembered
|
||||||
|
* purely for trimming purposes.
|
||||||
|
*
|
||||||
|
* @return {Range90k} Currently remembered range
|
||||||
|
*/
|
||||||
|
get recordingsRange() {
|
||||||
|
return this._recordingsRange ? this._recordingsRange.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the recordings range for this view.
|
||||||
|
*
|
||||||
|
* @param {Range90k} range90k Range to remember
|
||||||
|
*/
|
||||||
|
set recordingsRange(range90k) {
|
||||||
|
this._recordingsRange = range90k ? range90k.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether time ranges in the recording list are being trimmed.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
get trimmed() {
|
||||||
|
return this._trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether recording time ranges should be trimmed.
|
||||||
|
*
|
||||||
|
* @param {Boolean} value True if trimming desired
|
||||||
|
*/
|
||||||
|
set trimmed(value) {
|
||||||
|
if (value != this._trimmed) {
|
||||||
|
this._trimmed = value;
|
||||||
|
this._updateRecordings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide the display in the DOM.
|
||||||
|
*
|
||||||
|
* @param {Boolean} show True for show, false for hide
|
||||||
|
*/
|
||||||
|
set show(show) {
|
||||||
|
const sel = this._element;
|
||||||
|
if (show) {
|
||||||
|
sel.show();
|
||||||
|
} else {
|
||||||
|
sel.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether loading indicator should be shown or not.
|
||||||
|
*
|
||||||
|
* @param {Boolean} show True if indicator should be showing
|
||||||
|
*/
|
||||||
|
set showLoading(show) {
|
||||||
|
const loading = $('tr.loading', this._element);
|
||||||
|
if (show) {
|
||||||
|
loading.show();
|
||||||
|
} else {
|
||||||
|
if (this._timeoutId) {
|
||||||
|
clearTimeout(this._timeoutId);
|
||||||
|
this._timeoutId = null;
|
||||||
|
}
|
||||||
|
loading.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the loading indicated after a delay, unless the timer has been
|
||||||
|
* cleared already.
|
||||||
|
*
|
||||||
|
* @param {Number} timeOutMs Delay (in ms) before indicator should appear
|
||||||
|
*/
|
||||||
|
delayedShowLoading(timeOutMs) {
|
||||||
|
this._timeoutId = setTimeout(() => (this.showLoading = true), timeOutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new time format string.
|
||||||
|
*
|
||||||
|
* This string is passed on to the formatter and the recordings list
|
||||||
|
* is updated (using the formatter).
|
||||||
|
*
|
||||||
|
* @param {String} formatStr Formatting string
|
||||||
|
*/
|
||||||
|
set timeFormat(formatStr) {
|
||||||
|
// Change the formatter and update recordings (view)
|
||||||
|
this._formatter.timeFormat = formatStr;
|
||||||
|
this._updateRecordings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a handler to receive clicks on a recording.
|
||||||
|
*
|
||||||
|
* The handler will be called with one argument: a recording model.
|
||||||
|
*
|
||||||
|
* @param {Function} h Handler to be called.
|
||||||
|
*/
|
||||||
|
set onRecordingClicked(h) {
|
||||||
|
this._clickHandler = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the list of recordings from JSON data.
|
||||||
|
*
|
||||||
|
* The data is expected to be an array with recording objects.
|
||||||
|
*
|
||||||
|
* @param {String} recordingsJSON JSON data (array)
|
||||||
|
*/
|
||||||
|
set recordingsJSON(recordingsJSON) {
|
||||||
|
this.showLoading = false;
|
||||||
|
// Store as model objects
|
||||||
|
this._recordings = recordingsJSON.map(function(r) {
|
||||||
|
return new Recording(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tbody = this._element;
|
||||||
|
// Remove existing rows, replace with new ones
|
||||||
|
$('tr.r', tbody).remove();
|
||||||
|
this._recordings.forEach((r) => {
|
||||||
|
let row = $('<tr class="r" />');
|
||||||
|
row.append(_columnOrder.map((k) => $('<td/>')));
|
||||||
|
row.on('click', (e) => {
|
||||||
|
console.log('Video clicked');
|
||||||
|
if (this._clickHandler !== null) {
|
||||||
|
console.log('Video clicked handler call');
|
||||||
|
this._clickHandler(r);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tbody.append(row);
|
||||||
|
});
|
||||||
|
// Cause formatting and date to be put in the rows
|
||||||
|
this._updateRecordings();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,35 @@
|
|||||||
// vim: set et ts=2 sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
//
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Settings = require('./parts/Settings');
|
const Settings = require('./parts/Settings');
|
||||||
|
|
||||||
|
@ -1,5 +1,34 @@
|
|||||||
// vim: set et ts=2 sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
//
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
@ -16,6 +45,7 @@ module.exports = (env, args) => {
|
|||||||
output: {
|
output: {
|
||||||
filename: '[name].bundle.js',
|
filename: '[name].bundle.js',
|
||||||
path: nvrSettings._paths.dist_dir,
|
path: nvrSettings._paths.dist_dir,
|
||||||
|
publicPath: '/',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [{
|
rules: [{
|
||||||
@ -25,7 +55,7 @@ module.exports = (env, args) => {
|
|||||||
'presets': ['env'],
|
'presets': ['env'],
|
||||||
},
|
},
|
||||||
exclude: /(node_modules|bower_components)/,
|
exclude: /(node_modules|bower_components)/,
|
||||||
include: [ './ui-src'],
|
include: ['./ui-src'],
|
||||||
}, {
|
}, {
|
||||||
test: /\.png$/,
|
test: /\.png$/,
|
||||||
use: ['file-loader'],
|
use: ['file-loader'],
|
||||||
@ -46,10 +76,14 @@ module.exports = (env, args) => {
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(args.mode),
|
||||||
|
}),
|
||||||
new webpack.IgnorePlugin(/\.\/locale$/),
|
new webpack.IgnorePlugin(/\.\/locale$/),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
title: nvrSettings.app_title,
|
title: nvrSettings.app_title,
|
||||||
template: path.join(nvrSettings._paths.app_src_dir, 'index.html'),
|
filename: 'index.html',
|
||||||
|
template: path.join(nvrSettings._paths.app_src_dir, 'assets', 'index.html'),
|
||||||
}),
|
}),
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
/node_modules\/moment\/moment\.js$/,
|
/node_modules\/moment\/moment\.js$/,
|
||||||
|
@ -1,5 +1,34 @@
|
|||||||
// vim: set et ts=2 sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
//
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const NVRSettings = require('./NVRSettings');
|
const NVRSettings = require('./NVRSettings');
|
||||||
@ -14,6 +43,9 @@ module.exports = (env, args) => {
|
|||||||
warnings: true,
|
warnings: true,
|
||||||
},
|
},
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
|
optimization: {
|
||||||
|
namedChunks: true,
|
||||||
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
contentBase: nvrSettings.app_src_dir,
|
contentBase: nvrSettings.app_src_dir,
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
@ -26,9 +58,6 @@ module.exports = (env, args) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env.NODE_ENV': JSON.stringify('development'),
|
|
||||||
}),
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,35 @@
|
|||||||
// vim: set et ts=2 sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
//
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
|
|
||||||
|
@ -1,7 +1,37 @@
|
|||||||
// vim: set et ts=2 sw=2:
|
// vim: set et sw=2 ts=2:
|
||||||
//
|
//
|
||||||
|
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||||
|
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
const NVRSettings = require('./NVRSettings');
|
const NVRSettings = require('./NVRSettings');
|
||||||
const baseConfig = require('./base.config.js');
|
const baseConfig = require('./base.config.js');
|
||||||
|
|
||||||
@ -12,14 +42,34 @@ module.exports = (env, args) => {
|
|||||||
const nvrSettings = settingsObject.settings;
|
const nvrSettings = settingsObject.settings;
|
||||||
|
|
||||||
return settingsObject.webpackMerge(baseConfig, {
|
return settingsObject.webpackMerge(baseConfig, {
|
||||||
|
//devtool: 'cheap-module-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.html$/,
|
||||||
|
loader: 'html-loader',
|
||||||
|
query: {
|
||||||
|
minimize: true,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
|
minSize: 30000,
|
||||||
|
minChunks: 1,
|
||||||
|
maxAsyncRequests: 5,
|
||||||
|
maxInitialRequests: 3,
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
default: {
|
default: {
|
||||||
minChunks: 2,
|
minChunks: 2,
|
||||||
priority: -20,
|
priority: -20,
|
||||||
},
|
},
|
||||||
commons: {
|
commons: {
|
||||||
|
name: 'commons',
|
||||||
|
chunks: 'all',
|
||||||
|
minChunks: 2,
|
||||||
|
},
|
||||||
|
vendors: {
|
||||||
test: /[\\/]node_modules[\\/]/,
|
test: /[\\/]node_modules[\\/]/,
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
chunks: 'all',
|
chunks: 'all',
|
||||||
@ -29,12 +79,19 @@ module.exports = (env, args) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
||||||
}),
|
|
||||||
new CleanWebpackPlugin([nvrSettings.dist_dir], {
|
new CleanWebpackPlugin([nvrSettings.dist_dir], {
|
||||||
root: nvrSettings._paths.project_root,
|
root: nvrSettings._paths.project_root,
|
||||||
}),
|
}),
|
||||||
|
new CompressionPlugin({
|
||||||
|
asset: '[path].gz[query]',
|
||||||
|
algorithm: 'gzip',
|
||||||
|
test: /\.js$|\.css$|\.html$/,
|
||||||
|
threshold: 10240,
|
||||||
|
minRatio: 0.8,
|
||||||
|
}),
|
||||||
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
|
/node_modules\/jquery\/dist\/jquery\.js$/,
|
||||||
|
'./jquery.min.js'),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
347
yarn.lock
347
yarn.lock
@ -23,10 +23,28 @@ acorn-dynamic-import@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn "^5.0.0"
|
acorn "^5.0.0"
|
||||||
|
|
||||||
|
acorn-jsx@^3.0.0:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
|
||||||
|
dependencies:
|
||||||
|
acorn "^3.0.4"
|
||||||
|
|
||||||
|
acorn@^3.0.4:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
|
||||||
|
|
||||||
acorn@^5.0.0:
|
acorn@^5.0.0:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7"
|
||||||
|
|
||||||
|
acorn@^5.5.0:
|
||||||
|
version "5.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
|
||||||
|
|
||||||
|
ajv-keywords@^2.1.0:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
|
||||||
|
|
||||||
ajv-keywords@^3.1.0:
|
ajv-keywords@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
|
||||||
@ -47,6 +65,15 @@ ajv@^5.0.0:
|
|||||||
json-schema-traverse "^0.3.0"
|
json-schema-traverse "^0.3.0"
|
||||||
json-stable-stringify "^1.0.1"
|
json-stable-stringify "^1.0.1"
|
||||||
|
|
||||||
|
ajv@^5.2.3, ajv@^5.3.0:
|
||||||
|
version "5.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||||
|
dependencies:
|
||||||
|
co "^4.6.0"
|
||||||
|
fast-deep-equal "^1.0.0"
|
||||||
|
fast-json-stable-stringify "^2.0.0"
|
||||||
|
json-schema-traverse "^0.3.0"
|
||||||
|
|
||||||
ajv@^6.1.0:
|
ajv@^6.1.0:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.1.tgz#28a6abc493a2abe0fb4c8507acaedb43fa550671"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.1.tgz#28a6abc493a2abe0fb4c8507acaedb43fa550671"
|
||||||
@ -232,6 +259,10 @@ ast-types@0.11.2:
|
|||||||
version "0.11.2"
|
version "0.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.2.tgz#cc4e1d15a36b39979a1986fe1e91321cbfae7783"
|
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.2.tgz#cc4e1d15a36b39979a1986fe1e91321cbfae7783"
|
||||||
|
|
||||||
|
ast-types@0.9.6:
|
||||||
|
version "0.9.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
|
||||||
|
|
||||||
async-each@^1.0.0:
|
async-each@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
||||||
@ -273,7 +304,7 @@ aws4@^1.2.1:
|
|||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
||||||
|
|
||||||
babel-code-frame@^6.26.0:
|
babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
||||||
version "6.26.0"
|
version "6.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1303,7 +1334,7 @@ bytes@3.0.0:
|
|||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
|
||||||
|
|
||||||
cacache@^10.0.1:
|
cacache@^10.0.1, cacache@^10.0.4:
|
||||||
version "10.0.4"
|
version "10.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
|
resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1347,6 +1378,16 @@ cacheable-request@^2.1.1:
|
|||||||
normalize-url "2.0.1"
|
normalize-url "2.0.1"
|
||||||
responselike "1.0.2"
|
responselike "1.0.2"
|
||||||
|
|
||||||
|
caller-path@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
|
||||||
|
dependencies:
|
||||||
|
callsites "^0.2.0"
|
||||||
|
|
||||||
|
callsites@^0.2.0:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
|
||||||
|
|
||||||
camel-case@3.0.x:
|
camel-case@3.0.x:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
|
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
|
||||||
@ -1461,6 +1502,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
circular-json@^0.3.1:
|
||||||
|
version "0.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||||
|
|
||||||
clap@^1.0.9:
|
clap@^1.0.9:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51"
|
resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51"
|
||||||
@ -1666,6 +1711,16 @@ compressible@~2.0.13:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db ">= 1.33.0 < 2"
|
mime-db ">= 1.33.0 < 2"
|
||||||
|
|
||||||
|
compression-webpack-plugin@^1.1.10:
|
||||||
|
version "1.1.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.10.tgz#dbc2d2d65729a89f7b195668211d38ea0883df98"
|
||||||
|
dependencies:
|
||||||
|
cacache "^10.0.1"
|
||||||
|
find-cache-dir "^1.0.0"
|
||||||
|
neo-async "^2.5.0"
|
||||||
|
serialize-javascript "^1.4.0"
|
||||||
|
webpack-sources "^1.0.1"
|
||||||
|
|
||||||
compression@^1.5.2:
|
compression@^1.5.2:
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
|
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
|
||||||
@ -1682,7 +1737,7 @@ concat-map@0.0.1:
|
|||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
|
||||||
concat-stream@^1.4.7, concat-stream@^1.5.0:
|
concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0:
|
||||||
version "1.6.1"
|
version "1.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26"
|
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1782,7 +1837,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
|
|||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
cross-spawn@^5.0.1:
|
cross-spawn@^5.0.1, cross-spawn@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1979,6 +2034,10 @@ deep-extend@^0.4.0, deep-extend@~0.4.0:
|
|||||||
version "0.4.2"
|
version "0.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
|
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
|
||||||
|
|
||||||
|
deep-is@~0.1.3:
|
||||||
|
version "0.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||||
|
|
||||||
define-properties@^1.1.2:
|
define-properties@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
|
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
|
||||||
@ -2009,6 +2068,18 @@ defined@^1.0.0:
|
|||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
|
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
|
||||||
|
|
||||||
|
del@^2.0.2:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
|
||||||
|
dependencies:
|
||||||
|
globby "^5.0.0"
|
||||||
|
is-path-cwd "^1.0.0"
|
||||||
|
is-path-in-cwd "^1.0.0"
|
||||||
|
object-assign "^4.0.1"
|
||||||
|
pify "^2.0.0"
|
||||||
|
pinkie-promise "^2.0.0"
|
||||||
|
rimraf "^2.2.8"
|
||||||
|
|
||||||
del@^3.0.0:
|
del@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
|
resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
|
||||||
@ -2094,6 +2165,12 @@ dns-txt@^2.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
buffer-indexof "^1.0.0"
|
buffer-indexof "^1.0.0"
|
||||||
|
|
||||||
|
doctrine@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||||
|
dependencies:
|
||||||
|
esutils "^2.0.2"
|
||||||
|
|
||||||
dom-converter@~0.1:
|
dom-converter@~0.1:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
|
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
|
||||||
@ -2266,6 +2343,13 @@ es-to-primitive@^1.1.1:
|
|||||||
is-date-object "^1.0.1"
|
is-date-object "^1.0.1"
|
||||||
is-symbol "^1.0.1"
|
is-symbol "^1.0.1"
|
||||||
|
|
||||||
|
es6-templates@^0.2.3:
|
||||||
|
version "0.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4"
|
||||||
|
dependencies:
|
||||||
|
recast "~0.11.12"
|
||||||
|
through "~2.3.6"
|
||||||
|
|
||||||
escape-html@~1.0.3:
|
escape-html@~1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||||
@ -2274,6 +2358,10 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
|||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||||
|
|
||||||
|
eslint-config-google@^0.9.1:
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.9.1.tgz#83353c3dba05f72bb123169a4094f4ff120391eb"
|
||||||
|
|
||||||
eslint-scope@^3.7.1:
|
eslint-scope@^3.7.1:
|
||||||
version "3.7.1"
|
version "3.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
|
||||||
@ -2281,14 +2369,77 @@ eslint-scope@^3.7.1:
|
|||||||
esrecurse "^4.1.0"
|
esrecurse "^4.1.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
|
eslint-visitor-keys@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
|
||||||
|
|
||||||
|
eslint@^4.18.2:
|
||||||
|
version "4.18.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.2.tgz#0f81267ad1012e7d2051e186a9004cc2267b8d45"
|
||||||
|
dependencies:
|
||||||
|
ajv "^5.3.0"
|
||||||
|
babel-code-frame "^6.22.0"
|
||||||
|
chalk "^2.1.0"
|
||||||
|
concat-stream "^1.6.0"
|
||||||
|
cross-spawn "^5.1.0"
|
||||||
|
debug "^3.1.0"
|
||||||
|
doctrine "^2.1.0"
|
||||||
|
eslint-scope "^3.7.1"
|
||||||
|
eslint-visitor-keys "^1.0.0"
|
||||||
|
espree "^3.5.2"
|
||||||
|
esquery "^1.0.0"
|
||||||
|
esutils "^2.0.2"
|
||||||
|
file-entry-cache "^2.0.0"
|
||||||
|
functional-red-black-tree "^1.0.1"
|
||||||
|
glob "^7.1.2"
|
||||||
|
globals "^11.0.1"
|
||||||
|
ignore "^3.3.3"
|
||||||
|
imurmurhash "^0.1.4"
|
||||||
|
inquirer "^3.0.6"
|
||||||
|
is-resolvable "^1.0.0"
|
||||||
|
js-yaml "^3.9.1"
|
||||||
|
json-stable-stringify-without-jsonify "^1.0.1"
|
||||||
|
levn "^0.3.0"
|
||||||
|
lodash "^4.17.4"
|
||||||
|
minimatch "^3.0.2"
|
||||||
|
mkdirp "^0.5.1"
|
||||||
|
natural-compare "^1.4.0"
|
||||||
|
optionator "^0.8.2"
|
||||||
|
path-is-inside "^1.0.2"
|
||||||
|
pluralize "^7.0.0"
|
||||||
|
progress "^2.0.0"
|
||||||
|
require-uncached "^1.0.3"
|
||||||
|
semver "^5.3.0"
|
||||||
|
strip-ansi "^4.0.0"
|
||||||
|
strip-json-comments "~2.0.1"
|
||||||
|
table "4.0.2"
|
||||||
|
text-table "~0.2.0"
|
||||||
|
|
||||||
|
espree@^3.5.2:
|
||||||
|
version "3.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
|
||||||
|
dependencies:
|
||||||
|
acorn "^5.5.0"
|
||||||
|
acorn-jsx "^3.0.0"
|
||||||
|
|
||||||
esprima@^2.6.0:
|
esprima@^2.6.0:
|
||||||
version "2.7.3"
|
version "2.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
|
||||||
|
|
||||||
esprima@~4.0.0:
|
esprima@^4.0.0, esprima@~4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
|
||||||
|
|
||||||
|
esprima@~3.1.0:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||||
|
|
||||||
|
esquery@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
|
||||||
|
dependencies:
|
||||||
|
estraverse "^4.0.0"
|
||||||
|
|
||||||
esrecurse@^4.1.0:
|
esrecurse@^4.1.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
|
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
|
||||||
@ -2296,7 +2447,7 @@ esrecurse@^4.1.0:
|
|||||||
estraverse "^4.1.0"
|
estraverse "^4.1.0"
|
||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
|
|
||||||
estraverse@^4.1.0, estraverse@^4.1.1:
|
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
|
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
|
||||||
|
|
||||||
@ -2474,6 +2625,10 @@ fast-json-stable-stringify@^2.0.0:
|
|||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||||
|
|
||||||
|
fast-levenshtein@~2.0.4:
|
||||||
|
version "2.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||||
|
|
||||||
fastparse@^1.1.1:
|
fastparse@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
||||||
@ -2503,6 +2658,13 @@ figures@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp "^1.0.5"
|
escape-string-regexp "^1.0.5"
|
||||||
|
|
||||||
|
file-entry-cache@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
|
||||||
|
dependencies:
|
||||||
|
flat-cache "^1.2.1"
|
||||||
|
object-assign "^4.0.1"
|
||||||
|
|
||||||
file-loader@^1.1.11:
|
file-loader@^1.1.11:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
|
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
|
||||||
@ -2572,6 +2734,15 @@ first-chunk-stream@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readable-stream "^2.0.2"
|
readable-stream "^2.0.2"
|
||||||
|
|
||||||
|
flat-cache@^1.2.1:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
|
||||||
|
dependencies:
|
||||||
|
circular-json "^0.3.1"
|
||||||
|
del "^2.0.2"
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
write "^0.2.1"
|
||||||
|
|
||||||
flatten@^1.0.2:
|
flatten@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
|
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
|
||||||
@ -2675,6 +2846,10 @@ function-bind@^1.0.2, function-bind@^1.1.1:
|
|||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||||
|
|
||||||
|
functional-red-black-tree@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
|
|
||||||
gauge@~2.7.3:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
@ -2796,6 +2971,10 @@ global@^4.3.2:
|
|||||||
min-document "^2.19.0"
|
min-document "^2.19.0"
|
||||||
process "~0.5.1"
|
process "~0.5.1"
|
||||||
|
|
||||||
|
globals@^11.0.1:
|
||||||
|
version "11.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
|
||||||
|
|
||||||
globals@^9.18.0:
|
globals@^9.18.0:
|
||||||
version "9.18.0"
|
version "9.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
||||||
@ -2811,6 +2990,17 @@ globby@^4.0.0:
|
|||||||
pify "^2.0.0"
|
pify "^2.0.0"
|
||||||
pinkie-promise "^2.0.0"
|
pinkie-promise "^2.0.0"
|
||||||
|
|
||||||
|
globby@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
|
||||||
|
dependencies:
|
||||||
|
array-union "^1.0.1"
|
||||||
|
arrify "^1.0.0"
|
||||||
|
glob "^7.0.3"
|
||||||
|
object-assign "^4.0.1"
|
||||||
|
pify "^2.0.0"
|
||||||
|
pinkie-promise "^2.0.0"
|
||||||
|
|
||||||
globby@^6.1.0:
|
globby@^6.1.0:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
|
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
|
||||||
@ -3035,7 +3225,17 @@ html-entities@^1.2.0:
|
|||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
|
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
|
||||||
|
|
||||||
html-minifier@^3.2.3:
|
html-loader@^0.5.5:
|
||||||
|
version "0.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea"
|
||||||
|
dependencies:
|
||||||
|
es6-templates "^0.2.3"
|
||||||
|
fastparse "^1.1.1"
|
||||||
|
html-minifier "^3.5.8"
|
||||||
|
loader-utils "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
|
html-minifier@^3.2.3, html-minifier@^3.5.8:
|
||||||
version "3.5.10"
|
version "3.5.10"
|
||||||
resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.10.tgz#8522c772c388db81aa5c26f62033302d906ea1c7"
|
resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.10.tgz#8522c772c388db81aa5c26f62033302d906ea1c7"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3140,6 +3340,10 @@ iferr@^0.1.5:
|
|||||||
version "0.1.5"
|
version "0.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||||
|
|
||||||
|
ignore@^3.3.3:
|
||||||
|
version "3.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
|
||||||
|
|
||||||
import-local@^1.0.0:
|
import-local@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
|
resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
|
||||||
@ -3211,7 +3415,7 @@ inquirer@^1.0.2:
|
|||||||
strip-ansi "^3.0.0"
|
strip-ansi "^3.0.0"
|
||||||
through "^2.3.6"
|
through "^2.3.6"
|
||||||
|
|
||||||
inquirer@^3.3.0:
|
inquirer@^3.0.6, inquirer@^3.3.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3497,6 +3701,10 @@ is-regex@^1.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has "^1.0.1"
|
has "^1.0.1"
|
||||||
|
|
||||||
|
is-resolvable@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
|
||||||
|
|
||||||
is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
|
is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
|
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
|
||||||
@ -3594,6 +3802,13 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
|
|||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||||
|
|
||||||
|
js-yaml@^3.9.1:
|
||||||
|
version "3.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
|
||||||
|
dependencies:
|
||||||
|
argparse "^1.0.7"
|
||||||
|
esprima "^4.0.0"
|
||||||
|
|
||||||
js-yaml@~3.7.0:
|
js-yaml@~3.7.0:
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
|
||||||
@ -3645,6 +3860,10 @@ json-schema@0.2.3:
|
|||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||||
|
|
||||||
|
json-stable-stringify-without-jsonify@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||||
|
|
||||||
json-stable-stringify@^1.0.1:
|
json-stable-stringify@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
||||||
@ -3718,6 +3937,13 @@ lcid@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
invert-kv "^1.0.0"
|
invert-kv "^1.0.0"
|
||||||
|
|
||||||
|
levn@^0.3.0, levn@~0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
|
||||||
|
dependencies:
|
||||||
|
prelude-ls "~1.1.2"
|
||||||
|
type-check "~0.3.2"
|
||||||
|
|
||||||
listr-silent-renderer@^1.1.1:
|
listr-silent-renderer@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
|
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
|
||||||
@ -4207,6 +4433,10 @@ nanomatch@^1.2.9:
|
|||||||
snapdragon "^0.8.1"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.1"
|
to-regex "^3.0.1"
|
||||||
|
|
||||||
|
natural-compare@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
|
|
||||||
ncname@1.0.x:
|
ncname@1.0.x:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c"
|
resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c"
|
||||||
@ -4443,6 +4673,17 @@ opn@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-wsl "^1.1.0"
|
is-wsl "^1.1.0"
|
||||||
|
|
||||||
|
optionator@^0.8.2:
|
||||||
|
version "0.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
|
||||||
|
dependencies:
|
||||||
|
deep-is "~0.1.3"
|
||||||
|
fast-levenshtein "~2.0.4"
|
||||||
|
levn "~0.3.0"
|
||||||
|
prelude-ls "~1.1.2"
|
||||||
|
type-check "~0.3.2"
|
||||||
|
wordwrap "~1.0.0"
|
||||||
|
|
||||||
ora@^0.2.3:
|
ora@^0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
|
resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
|
||||||
@ -4618,7 +4859,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||||
|
|
||||||
path-is-inside@^1.0.1:
|
path-is-inside@^1.0.1, path-is-inside@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||||
|
|
||||||
@ -4686,6 +4927,10 @@ pkg-dir@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^2.1.0"
|
find-up "^2.1.0"
|
||||||
|
|
||||||
|
pluralize@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
|
||||||
|
|
||||||
portfinder@^1.0.9:
|
portfinder@^1.0.9:
|
||||||
version "1.0.13"
|
version "1.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
|
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
|
||||||
@ -4944,6 +5189,10 @@ postcss@^6.0.1:
|
|||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
supports-color "^5.2.0"
|
supports-color "^5.2.0"
|
||||||
|
|
||||||
|
prelude-ls@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
|
|
||||||
prepend-http@^1.0.0, prepend-http@^1.0.1:
|
prepend-http@^1.0.0, prepend-http@^1.0.1:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||||
@ -4956,7 +5205,7 @@ preserve@^0.2.0:
|
|||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||||
|
|
||||||
prettier@^1.5.3:
|
prettier@1.11.1, prettier@^1.5.3:
|
||||||
version "1.11.1"
|
version "1.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
|
||||||
|
|
||||||
@ -4991,6 +5240,10 @@ process@~0.5.1:
|
|||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
|
resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
|
||||||
|
|
||||||
|
progress@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
|
||||||
|
|
||||||
promise-inflight@^1.0.1:
|
promise-inflight@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||||
@ -5223,6 +5476,15 @@ recast@^0.14.4:
|
|||||||
private "~0.1.5"
|
private "~0.1.5"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
|
recast@~0.11.12:
|
||||||
|
version "0.11.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
|
||||||
|
dependencies:
|
||||||
|
ast-types "0.9.6"
|
||||||
|
esprima "~3.1.0"
|
||||||
|
private "~0.1.5"
|
||||||
|
source-map "~0.5.0"
|
||||||
|
|
||||||
rechoir@^0.6.2:
|
rechoir@^0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
||||||
@ -5380,6 +5642,13 @@ require-main-filename@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||||
|
|
||||||
|
require-uncached@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
|
||||||
|
dependencies:
|
||||||
|
caller-path "^0.1.0"
|
||||||
|
resolve-from "^1.0.0"
|
||||||
|
|
||||||
requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0:
|
requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
@ -5397,6 +5666,10 @@ resolve-dir@^1.0.0:
|
|||||||
expand-tilde "^2.0.0"
|
expand-tilde "^2.0.0"
|
||||||
global-modules "^1.0.0"
|
global-modules "^1.0.0"
|
||||||
|
|
||||||
|
resolve-from@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
|
||||||
|
|
||||||
resolve-from@^3.0.0:
|
resolve-from@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||||
@ -5657,6 +5930,12 @@ slice-ansi@0.0.4:
|
|||||||
version "0.0.4"
|
version "0.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
|
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
|
||||||
|
|
||||||
|
slice-ansi@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
|
||||||
|
dependencies:
|
||||||
|
is-fullwidth-code-point "^2.0.0"
|
||||||
|
|
||||||
slide@^1.1.5:
|
slide@^1.1.5:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
|
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
|
||||||
@ -5748,7 +6027,7 @@ source-map-url@^0.4.0:
|
|||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
||||||
|
|
||||||
source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.3:
|
source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.3:
|
||||||
version "0.5.7"
|
version "0.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||||
|
|
||||||
@ -5895,7 +6174,7 @@ string-width@^1.0.1, string-width@^1.0.2:
|
|||||||
is-fullwidth-code-point "^1.0.0"
|
is-fullwidth-code-point "^1.0.0"
|
||||||
strip-ansi "^3.0.0"
|
strip-ansi "^3.0.0"
|
||||||
|
|
||||||
string-width@^2.0.0, string-width@^2.1.0:
|
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6012,6 +6291,17 @@ symbol-observable@^0.2.2:
|
|||||||
version "0.2.4"
|
version "0.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
|
||||||
|
|
||||||
|
table@4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
|
||||||
|
dependencies:
|
||||||
|
ajv "^5.2.3"
|
||||||
|
ajv-keywords "^2.1.0"
|
||||||
|
chalk "^2.1.0"
|
||||||
|
lodash "^4.17.4"
|
||||||
|
slice-ansi "1.0.0"
|
||||||
|
string-width "^2.1.1"
|
||||||
|
|
||||||
tapable@^1.0.0:
|
tapable@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
|
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
|
||||||
@ -6044,7 +6334,7 @@ temp@^0.8.1:
|
|||||||
os-tmpdir "^1.0.0"
|
os-tmpdir "^1.0.0"
|
||||||
rimraf "~2.2.6"
|
rimraf "~2.2.6"
|
||||||
|
|
||||||
text-table@^0.2.0:
|
text-table@^0.2.0, text-table@~0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
|
|
||||||
@ -6059,7 +6349,7 @@ through2@^2.0.0, through2@^2.0.1:
|
|||||||
readable-stream "^2.1.5"
|
readable-stream "^2.1.5"
|
||||||
xtend "~4.0.1"
|
xtend "~4.0.1"
|
||||||
|
|
||||||
through@^2.3.6:
|
through@^2.3.6, through@~2.3.6:
|
||||||
version "2.3.8"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||||
|
|
||||||
@ -6151,6 +6441,12 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|||||||
version "0.14.5"
|
version "0.14.5"
|
||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||||
|
|
||||||
|
type-check@~0.3.2:
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
||||||
|
dependencies:
|
||||||
|
prelude-ls "~1.1.2"
|
||||||
|
|
||||||
type-is@~1.6.15:
|
type-is@~1.6.15:
|
||||||
version "1.6.16"
|
version "1.6.16"
|
||||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
|
||||||
@ -6189,6 +6485,19 @@ uglifyjs-webpack-plugin@^1.1.1, uglifyjs-webpack-plugin@^1.2.2:
|
|||||||
webpack-sources "^1.1.0"
|
webpack-sources "^1.1.0"
|
||||||
worker-farm "^1.5.2"
|
worker-farm "^1.5.2"
|
||||||
|
|
||||||
|
uglifyjs-webpack-plugin@^1.2.3:
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.3.tgz#bf23197b37a8fc953fecfbcbab66e506f9a0ae72"
|
||||||
|
dependencies:
|
||||||
|
cacache "^10.0.4"
|
||||||
|
find-cache-dir "^1.0.0"
|
||||||
|
schema-utils "^0.4.5"
|
||||||
|
serialize-javascript "^1.4.0"
|
||||||
|
source-map "^0.6.1"
|
||||||
|
uglify-es "^3.3.4"
|
||||||
|
webpack-sources "^1.1.0"
|
||||||
|
worker-farm "^1.5.2"
|
||||||
|
|
||||||
uid-number@^0.0.6:
|
uid-number@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
|
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
|
||||||
@ -6601,6 +6910,10 @@ wide-align@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width "^1.0.2"
|
string-width "^1.0.2"
|
||||||
|
|
||||||
|
wordwrap@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||||
|
|
||||||
worker-farm@^1.5.2:
|
worker-farm@^1.5.2:
|
||||||
version "1.5.4"
|
version "1.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.4.tgz#4debbe46b40edefcc717ebde74a90b1ae1e909a1"
|
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.4.tgz#4debbe46b40edefcc717ebde74a90b1ae1e909a1"
|
||||||
@ -6627,6 +6940,12 @@ write-file-atomic@^1.2.0:
|
|||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
slide "^1.1.5"
|
slide "^1.1.5"
|
||||||
|
|
||||||
|
write@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
|
||||||
|
dependencies:
|
||||||
|
mkdirp "^0.5.1"
|
||||||
|
|
||||||
xml-char-classes@^1.0.0:
|
xml-char-classes@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"
|
resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"
|
||||||
|
Loading…
Reference in New Issue
Block a user