mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-20 01:50:24 -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:
committed by
Scott Lamb
parent
caac324bd5
commit
58152e8d94
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user