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:
Dolf Starreveld
2018-03-20 07:03:12 -07:00
committed by Scott Lamb
parent caac324bd5
commit 58152e8d94
35 changed files with 4089 additions and 518 deletions

View 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
View 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];
})
);
}
}

View 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();
}
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}