A little more UI refactor, cleanup, eslint more strict (#54)
* A little more UI refactor, cleanup, eslint more strict * Split out imports for jQuery components and put them where needed. * No longer do all of it in application module. * Prepares better for code splitting. * Split out video player dialog * Simplifies jquery-ui dependencies for code splitting * Simplifies code * Configure to generate more, but smaller bundles. * Setup some more strict eslint settings * Fix css to import rather than require * Change settings to correctly support tree shaking in production build Signed-off-by: Dolf Starreveld <dolf@starreveld.com> * Remove “old” code from TimeFormatter * Accidentally left behind due to overlapping PRs Signed-off-by: Dolf Starreveld <dolf@starreveld.com>
This commit is contained in:
parent
eaae640703
commit
f5aa0080bb
|
@ -3,7 +3,20 @@
|
|||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "google",
|
||||
"rules": {
|
||||
"init-declarations": ["error", "always"],
|
||||
"no-catch-shadow": ["error"],
|
||||
"no-delete-var": ["error"],
|
||||
"no-shadow": ["error", { "builtinGlobals": false, "hoist": "functions", "allow": [] }],
|
||||
"no-shadow-restricted-names": ["error"],
|
||||
"no-undef": ["error", {"typeof": true}],
|
||||
"no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }],
|
||||
"no-use-before-define": ["error", { "functions": true, "classes": true }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-minify-webpack-plugin": "^0.3.0",
|
||||
"babel-plugin-transform-imports": "^1.5.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"clean-webpack-plugin": "^0.1.18",
|
||||
"compression-webpack-plugin": "^1.1.10",
|
||||
|
|
|
@ -34,31 +34,32 @@
|
|||
// TODO: add error bar on fetch failure.
|
||||
// TODO: live updating.
|
||||
|
||||
import 'jquery-ui/themes/base/button.css';
|
||||
import $ from 'jquery';
|
||||
|
||||
// tooltip needs:
|
||||
// css.structure: ../../themes/base/core.css
|
||||
// css.structure: ../../themes/base/tooltip.css
|
||||
// css.theme: ../../themes/base/theme.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/themes/base/theme.css';
|
||||
|
||||
// This causes our custom css to be loaded after the above!
|
||||
require('./assets/index.css');
|
||||
import './assets/index.css';
|
||||
|
||||
import $ from 'jquery';
|
||||
import 'jquery-ui/ui/widgets/datepicker';
|
||||
import 'jquery-ui/ui/widgets/dialog';
|
||||
// Get ui widgets themselves
|
||||
import 'jquery-ui/ui/widgets/tooltip';
|
||||
|
||||
import Camera from './lib/models/Camera';
|
||||
import CameraView from './lib/views/CameraView';
|
||||
import CalendarView from './lib/views/CalendarView';
|
||||
import VideoDialogView from './lib/views/VideoDialogView';
|
||||
import NVRSettingsView from './lib/views/NVRSettingsView';
|
||||
import CheckboxGroupView from './lib/views/CheckboxGroupView';
|
||||
import RecordingFormatter from './lib/support/RecordingFormatter';
|
||||
import TimeFormatter, {
|
||||
TimeStamp90kFormatter,
|
||||
} from './lib/support/TimeFormatter';
|
||||
import TimeFormatter from './lib/support/TimeFormatter';
|
||||
import TimeStamp90kFormatter from './lib/support/TimeStamp90kFormatter';
|
||||
import MoonfireAPI from './lib/MoonfireAPI';
|
||||
|
||||
const api = new MoonfireAPI();
|
||||
|
@ -130,27 +131,18 @@ function onSelectVideo(nvrSettingsView, camera, range, recording) {
|
|||
trimmedRange,
|
||||
nvrSettingsView.timeStampTrack
|
||||
);
|
||||
const video = $('<video controls preload="auto" autoplay="true" />');
|
||||
const dialog = $('<div class="playdialog" />').append(video);
|
||||
$('body').append(dialog);
|
||||
|
||||
let [formattedStart, formattedEnd] = timeFormatter90k.formatSameDayShortened(
|
||||
const [
|
||||
formattedStart,
|
||||
formattedEnd,
|
||||
] = timeFormatter90k.formatSameDayShortened(
|
||||
trimmedRange.startTime90k,
|
||||
trimmedRange.endTime90k
|
||||
);
|
||||
dialog.dialog({
|
||||
title: camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd,
|
||||
width: recording.videoSampleEntryWidth / 4,
|
||||
close: () => {
|
||||
const videoDOMElement = video[0];
|
||||
videoDOMElement.pause();
|
||||
videoDOMElement.src = ''; // Remove current source to stop loading
|
||||
dialog.remove();
|
||||
},
|
||||
});
|
||||
// Now that dialog is up, set the src so video starts
|
||||
console.log('Video url: ' + url);
|
||||
video.attr('src', url);
|
||||
const videoTitle =
|
||||
camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd;
|
||||
new VideoDialogView()
|
||||
.attach($('body'))
|
||||
.play(videoTitle, recording.videoSampleEntryWidth / 4, url);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -193,16 +185,19 @@ function fetch(selectedRange, videoLength) {
|
|||
cameraView.recordingsReq = null;
|
||||
});
|
||||
r
|
||||
.then(function(data, status, req) {
|
||||
.then(function(data /* , status, req */) {
|
||||
// Sort recordings in descending order.
|
||||
data.recordings.sort(function(a, b) {
|
||||
return b.startId - a.startId;
|
||||
});
|
||||
console.log('Fetched results > updating recordings');
|
||||
console.log(
|
||||
'Fetched results for "%s" > updating recordings',
|
||||
cameraView.camera.shortName
|
||||
);
|
||||
cameraView.recordingsJSON = data.recordings;
|
||||
})
|
||||
.catch(function(data, status, err) {
|
||||
console.log(url, ' load failed: ', status, ': ', err);
|
||||
console.error(url, ' load failed: ', status, ': ', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -295,11 +290,11 @@ export default class NVRApplication {
|
|||
.request(api.nvrUrl(true))
|
||||
.done((data) => onReceivedCameras(data))
|
||||
.fail((req, status, err) => {
|
||||
console.log('NVR load error: ', status, err);
|
||||
console.error('NVR load error: ', status, err);
|
||||
onReceivedCameras({cameras: []});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('NVR load exception: ', e);
|
||||
console.error('NVR load exception: ', e);
|
||||
onReceivedCameras({cameras: []});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ body {
|
|||
#nav {
|
||||
float: left;
|
||||
}
|
||||
.ui-datepicker {
|
||||
|
||||
#datetime .ui-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
import NVRApplication from './NVRApplication';
|
||||
|
||||
import $ from 'jquery';
|
||||
import './favicon.ico';
|
||||
|
||||
// On document load, start application
|
||||
$(function() {
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class MoonfireAPI {
|
|||
url.hostname = host;
|
||||
url.port = port;
|
||||
console.log('API: ' + url.origin + url.pathname);
|
||||
this._builder = new URLBuilder(url.origin + url.pathname);
|
||||
this._builder = new URLBuilder(url.origin + url.pathname, relativeUrls);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Time90kParser from '../support/Time90kParser';
|
||||
import {TimeStamp90kFormatter} from '../support/TimeFormatter';
|
||||
import TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
|
||||
import Range90k from './Range90k';
|
||||
|
||||
/**
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class Range {
|
|||
*/
|
||||
constructor(low, high) {
|
||||
if (high < low) {
|
||||
console.log('Warning range swap: ' + low + ' - ' + high);
|
||||
console.warn('Warning range swap: ' + low + ' - ' + high);
|
||||
[low, high] = [high, low];
|
||||
}
|
||||
this.low = low;
|
||||
|
|
|
@ -40,9 +40,10 @@ import Range from './Range';
|
|||
let _range = new WeakMap();
|
||||
|
||||
/**
|
||||
* Subclass of Range to represent ranges over timestamps in 90k format.
|
||||
* Class like Range to represent ranges over timestamps in 90k format.
|
||||
*
|
||||
* This mostly means added some getters with names that make more sense.
|
||||
* A composed member of the Range class is use for the heavy lifting, while
|
||||
* this class provides a different interface.
|
||||
*/
|
||||
export default class Range90k {
|
||||
/**
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
export const internalTimeFormat = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ';
|
||||
|
||||
export const defaultTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
/**
|
||||
|
@ -117,50 +117,3 @@ export default class TimeFormatter {
|
|||
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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
// 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';
|
||||
|
||||
|
||||
export const internalTimeFormat = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ';
|
||||
|
||||
/**
|
||||
* Specialized class similar to TimeFormatter but forcing a specific time format
|
||||
* for internal usage purposes.
|
||||
*/
|
||||
export default 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];
|
||||
}
|
||||
}
|
|
@ -31,20 +31,10 @@
|
|||
// 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 TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
|
||||
import Time90kParser from '../support/Time90kParser';
|
||||
|
||||
/**
|
||||
|
@ -177,10 +167,11 @@ export default class CalendarView {
|
|||
|
||||
if (this._sameDay) {
|
||||
fromPickerView.option({
|
||||
dateFormat: $.datepicker.ISO_8601,
|
||||
dateFormat: DatePickerView.datepicker.ISO_8601,
|
||||
minDate: minDateStr,
|
||||
maxDate: maxDateStr,
|
||||
onSelect: (dateStr, picker) => this._updateRangeDates(dateStr, dateStr),
|
||||
onSelect: (dateStr /* , picker */) =>
|
||||
this._updateRangeDates(dateStr, dateStr),
|
||||
beforeShowDay: beforeShowDay,
|
||||
disabled: false,
|
||||
});
|
||||
|
@ -188,21 +179,21 @@ export default class CalendarView {
|
|||
toPickerView.activate(); // Default state, but alive
|
||||
} else {
|
||||
fromPickerView.option({
|
||||
dateFormat: $.datepicker.ISO_8601,
|
||||
dateFormat: DatePickerView.datepicker.ISO_8601,
|
||||
minDate: minDateStr,
|
||||
onSelect: (dateStr, picker) => {
|
||||
toPickerView.option('minDate', this.fromDateISO);
|
||||
onSelect: (dateStr /* , picker */) => {
|
||||
toPickerView.minDate = this.fromDateISO;
|
||||
this._updateRangeDates(dateStr, this.toDateISO);
|
||||
},
|
||||
beforeShowDay: beforeShowDay,
|
||||
disabled: false,
|
||||
});
|
||||
toPickerView.option({
|
||||
dateFormat: $.datepicker.ISO_8601,
|
||||
dateFormat: DatePickerView.datepicker.ISO_8601,
|
||||
minDate: fromPickerView.dateISO,
|
||||
maxDate: maxDateStr,
|
||||
onSelect: (dateStr, picker) => {
|
||||
fromPickerView.option('maxDate', this.toDateISO);
|
||||
onSelect: (dateStr /* , picker */) => {
|
||||
fromPickerView.maxDate = this.toDateISO;
|
||||
this._updateRangeDates(this.fromDateISO, dateStr);
|
||||
},
|
||||
beforeShowDay: beforeShowDay,
|
||||
|
@ -228,7 +219,7 @@ export default class CalendarView {
|
|||
* 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 {event} event DOM event that triggered us
|
||||
* @param {Boolean} isEnd True if this is for end time
|
||||
*/
|
||||
_pickerTimeChanged(event, isEnd) {
|
||||
|
@ -239,7 +230,7 @@ export default class CalendarView {
|
|||
? selectedRange.setEndTime(newTimeStr)
|
||||
: selectedRange.setStartTime(newTimeStr);
|
||||
if (parsedTS === null) {
|
||||
console.log('bad time change');
|
||||
console.warn('bad time change');
|
||||
$(pickerElement).addClass('ui-state-error');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export default class CameraView {
|
|||
this._enabled = enabled;
|
||||
this.recordingsView.show = enabled;
|
||||
console.log(
|
||||
'Camera ',
|
||||
'Camera %s %s',
|
||||
this.camera.shortName,
|
||||
this.enabled ? 'enabled' : 'disabled'
|
||||
);
|
||||
|
|
|
@ -32,10 +32,27 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
|
||||
import 'jquery-ui/themes/base/core.css';
|
||||
import 'jquery-ui/themes/base/datepicker.css';
|
||||
import 'jquery-ui/themes/base/theme.css';
|
||||
import 'jquery-ui/ui/widgets/datepicker';
|
||||
|
||||
/**
|
||||
* Class to encapsulate datepicker UI widget from jQuery.
|
||||
*/
|
||||
export default class DatePickerView {
|
||||
/**
|
||||
* Get the singleton datepicker instance.
|
||||
*
|
||||
* This is useful for accessing implementation constants, such as
|
||||
* date formats etc.
|
||||
*
|
||||
* @return {jQuery.datepicker} JQuery datepicker instance
|
||||
*/
|
||||
static get datepicker() {
|
||||
return $.datepicker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct wapper an attach to a specified parent DOM node.
|
||||
*
|
||||
|
@ -85,11 +102,11 @@ export default class DatePickerView {
|
|||
*
|
||||
* @return {Any} Function result
|
||||
*/
|
||||
_apply() {
|
||||
_apply(...args) {
|
||||
if (!this._alive) {
|
||||
console.log('WARN: datepicker not constructed yet [' + this.domId + ']');
|
||||
console.warn('datepicker not constructed yet [%s]', this.domId);
|
||||
}
|
||||
return this._pickerElement.datepicker(...arguments);
|
||||
return this._pickerElement.datepicker(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -267,8 +267,8 @@ export default class RecordingsView {
|
|||
$('tr.r', tbody).remove();
|
||||
this._recordings.forEach((r) => {
|
||||
let row = $('<tr class="r" />');
|
||||
row.append(_columnOrder.map((k) => $('<td/>')));
|
||||
row.on('click', (e) => {
|
||||
row.append(_columnOrder.map(() => $('<td/>')));
|
||||
row.on('click', () => {
|
||||
console.log('Video clicked');
|
||||
if (this._clickHandler !== null) {
|
||||
console.log('Video clicked handler call');
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
// 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/core.css';
|
||||
import 'jquery-ui/themes/base/dialog.css';
|
||||
import 'jquery-ui/themes/base/theme.css';
|
||||
// This not needed for pure dialog, but we want it resizable
|
||||
import 'jquery-ui/themes/base/resizable.css';
|
||||
|
||||
// Get dialog ui widget
|
||||
import 'jquery-ui/ui/widgets/dialog';
|
||||
|
||||
/**
|
||||
* Class to implement a simple jQuery dialog based video player.
|
||||
*/
|
||||
export default class VideoDialogView {
|
||||
/**
|
||||
* Construct the player.
|
||||
*
|
||||
* This does not attach the player to the DOM anywhere! In fact, construction
|
||||
* of the necessary video element is delayed until an attach is requested.
|
||||
* Since the close of the video removes all traces of it in the DOM, this
|
||||
* apprach allows repeated use by calling attach again!
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Attach the player to the specified DOM element.
|
||||
*
|
||||
* @param {Node} domElement DOM element to attach to
|
||||
* @return {VideoDialogView} Returns "this" for chaining.
|
||||
*/
|
||||
attach(domElement) {
|
||||
this._videoElement = $('<video controls preload="auto" autoplay="true" />');
|
||||
this._dialogElement = $('<div class="playdialog" />').append(
|
||||
this._videoElement
|
||||
);
|
||||
$(domElement).append(this._dialogElement);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the player, and start playing the given url.
|
||||
*
|
||||
* @param {String} title Title of the video player
|
||||
* @param {Number} width Width of the player
|
||||
* @param {String} url URL for source media
|
||||
* @return {VideoDialogView} Returns "this" for chaining.
|
||||
*/
|
||||
play(title, width, url) {
|
||||
this._dialogElement.dialog({
|
||||
title: title,
|
||||
width: width,
|
||||
close: () => {
|
||||
const videoDOMElement = this._videoElement[0];
|
||||
videoDOMElement.pause();
|
||||
videoDOMElement.src = ''; // Remove current source to stop loading
|
||||
this._videoElement = null;
|
||||
this._dialogElement.remove();
|
||||
this._dialogElement = null;
|
||||
},
|
||||
});
|
||||
// Now that dialog is up, set the src so video starts
|
||||
console.log('Video url: ' + url);
|
||||
this._videoElement.attr('src', url);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -48,32 +48,41 @@ module.exports = (env, args) => {
|
|||
publicPath: '/',
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
'presets': ['env'],
|
||||
presets: ['env', {modules: false}],
|
||||
},
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
include: ['./ui-src'],
|
||||
}, {
|
||||
},
|
||||
{
|
||||
test: /\.png$/,
|
||||
use: ['file-loader'],
|
||||
}, {
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.png$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
{
|
||||
// Load css and then in-line in head
|
||||
test: /\.css$/,
|
||||
loader: 'style-loader!css-loader',
|
||||
}],
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
|
@ -83,14 +92,20 @@ module.exports = (env, args) => {
|
|||
new HtmlWebpackPlugin({
|
||||
title: nvrSettings.app_title,
|
||||
filename: 'index.html',
|
||||
template: path.join(nvrSettings._paths.app_src_dir, 'assets', 'index.html'),
|
||||
template: path.join(
|
||||
nvrSettings._paths.app_src_dir,
|
||||
'assets',
|
||||
'index.html'
|
||||
),
|
||||
}),
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/node_modules\/moment\/moment\.js$/,
|
||||
'./min/moment.min.js'),
|
||||
'./min/moment.min.js'
|
||||
),
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/node_modules\/moment-timezone\/index\.js$/,
|
||||
'./builds/moment-timezone-with-data-2012-2022.min.js'),
|
||||
'./builds/moment-timezone-with-data-2012-2022.min.js'
|
||||
),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -44,6 +44,7 @@ module.exports = (env, args) => {
|
|||
},
|
||||
devtool: 'inline-source-map',
|
||||
optimization: {
|
||||
minimize: false,
|
||||
namedChunks: true,
|
||||
},
|
||||
devServer: {
|
||||
|
@ -54,11 +55,11 @@ module.exports = (env, args) => {
|
|||
hot: true,
|
||||
clientLogLevel: 'info',
|
||||
proxy: {
|
||||
'/api': `http://${nvrSettings.moonfire.server}:${nvrSettings.moonfire.port}`,
|
||||
'/api': `http://${nvrSettings.moonfire.server}:${
|
||||
nvrSettings.moonfire.port
|
||||
}`,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
plugins: [new webpack.HotModuleReplacementPlugin()],
|
||||
});
|
||||
};
|
||||
|
|
|
@ -42,34 +42,34 @@ const merge = require('webpack-merge');
|
|||
* found), we throw an exception.
|
||||
*
|
||||
* If the module that is require-d is a function, it will be executed,
|
||||
* passing the "env" and "args" parameters from the settingsConfig to it.
|
||||
* passing the "env" and "args" parameters to it.
|
||||
* The function should return a map.
|
||||
*
|
||||
* @param {String} path Path to be passed to require()
|
||||
* @param {object} settingsConfig Settings passed to new Settings()
|
||||
* @param {String} requiredPath Path to be passed to require()
|
||||
* @param {object} env webpack's "env" on invocation
|
||||
* @param {object} args webpack's "args" on invocation (options)
|
||||
* @param {Boolean} optional True file not to exist
|
||||
* @return {object} The module, or {} if not found (optional)
|
||||
*/
|
||||
function requireHelper(path, settingsConfig, optional) {
|
||||
function requireHelper(requiredPath, env, args, optional) {
|
||||
let module = {};
|
||||
try {
|
||||
require.resolve(path); // Throws if not found
|
||||
require.resolve(requiredPath); // Throws if not found
|
||||
try {
|
||||
module = require(path);
|
||||
if (typeof(module) === 'function') {
|
||||
module = module(settingsConfig.env, settingsConfig.args);
|
||||
module = require(requiredPath);
|
||||
if (typeof module === 'function') {
|
||||
module = module(env, args);
|
||||
}
|
||||
// Get owned properties only: now a literal map
|
||||
module = Object.assign({}, require(path).settings);
|
||||
module = Object.assign({}, require(requiredPath).settings);
|
||||
} catch (e) {
|
||||
throw new Error('Settings file (' + path + ') has errors.');
|
||||
throw new Error('Settings file (' + requiredPath + ') has errors.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!optional) {
|
||||
throw new Error('Settings file (' + path + ') not found.');
|
||||
throw new Error('Settings file (' + requiredPath + ') not found.');
|
||||
}
|
||||
}
|
||||
const args = settingsConfig.args;
|
||||
const webpackMode = (args ? args.mode : null) || 'none';
|
||||
const modes = module.webpack_mode || {};
|
||||
delete module.webpack_mode; // Not modifying original module. We have a copy!
|
||||
|
@ -188,13 +188,14 @@ class Settings {
|
|||
const secondaryPath = path.resolve(projectRoot, secondaryFile);
|
||||
|
||||
// Check if we can resolve the primary file and if we can, require it.
|
||||
const _settings =
|
||||
requireHelper(primaryPath, this.settings_config, optional);
|
||||
const _settings = requireHelper(primaryPath, env, args, optional);
|
||||
|
||||
// Merge secondary override file, if it exists
|
||||
this.settings = merge(_settings,
|
||||
requireHelper(secondaryPath, this.settings_config, true));
|
||||
};
|
||||
this.settings = merge(
|
||||
_settings,
|
||||
requireHelper(secondaryPath, env, args, true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take one or more webpack configurations and merge them.
|
||||
|
@ -212,12 +213,14 @@ class Settings {
|
|||
*/
|
||||
webpackMerge(...packs) {
|
||||
const unpack = (webpackConfig) => {
|
||||
if ((typeof(webpackConfig) === 'string') ||
|
||||
(webpackConfig instanceof String)) {
|
||||
if (
|
||||
typeof webpackConfig === 'string' ||
|
||||
webpackConfig instanceof String
|
||||
) {
|
||||
webpackConfig = require(webpackConfig);
|
||||
}
|
||||
const config = this.settings_config;
|
||||
if (typeof(webpackConfig) === 'function') {
|
||||
if (typeof webpackConfig === 'function') {
|
||||
return webpackConfig(config.env, config.args);
|
||||
}
|
||||
return webpackConfig;
|
||||
|
|
|
@ -42,34 +42,76 @@ module.exports = (env, args) => {
|
|||
const nvrSettings = settingsObject.settings;
|
||||
|
||||
return settingsObject.webpackMerge(baseConfig, {
|
||||
//devtool: 'cheap-module-source-map',
|
||||
devtool: 'cheap-module-source-map',
|
||||
module: {
|
||||
rules: [{
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader',
|
||||
query: {
|
||||
minimize: true,
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
{
|
||||
apply: (compiler) => {
|
||||
/**
|
||||
* Setup the UglifyJsPlugin as webpack4 does, plus options
|
||||
* we decide to override.
|
||||
*/
|
||||
// Lazy load the uglifyjs plugin
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
new UglifyJsPlugin({
|
||||
cache: true, // webpack4: default
|
||||
parallel: true, // webpack4: default
|
||||
sourceMap:
|
||||
(args.devtool && /source-?map/.test(args.devtool)) ||
|
||||
(args.plugins &&
|
||||
args.plugins.some(
|
||||
(p) => p instanceof webpack.SourceMapDevToolPlugin
|
||||
)),
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
drop_console: true, // Remove all console.log etc.
|
||||
keep_infinity: true, // Do not change to 1/0
|
||||
warnings: false, // Do not warn when dropping
|
||||
},
|
||||
output: {
|
||||
// Eliminate most comments, but not marked ones
|
||||
comments: 'some',
|
||||
},
|
||||
},
|
||||
}).apply(compiler);
|
||||
},
|
||||
},
|
||||
],
|
||||
splitChunks: {
|
||||
minSize: 30000,
|
||||
minChunks: 1,
|
||||
maxAsyncRequests: 5,
|
||||
maxInitialRequests: 3,
|
||||
maxInitialRequests: 4,
|
||||
cacheGroups: {
|
||||
default: {
|
||||
'default': {
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
},
|
||||
commons: {
|
||||
name: 'commons',
|
||||
'jquery-ui': {
|
||||
test: /[\\/]node_modules[\\/]jquery-ui[\\/]/,
|
||||
name: 'jquery-ui',
|
||||
chunks: 'all',
|
||||
minChunks: 2,
|
||||
priority: -5,
|
||||
},
|
||||
vendors: {
|
||||
'jquery': {
|
||||
test: /[\\/]node_modules[\\/]jquery[\\/]/,
|
||||
name: 'jquery',
|
||||
chunks: 'all',
|
||||
priority: -5,
|
||||
},
|
||||
'vendors': {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendor',
|
||||
chunks: 'all',
|
||||
|
@ -89,9 +131,6 @@ module.exports = (env, args) => {
|
|||
threshold: 10240,
|
||||
minRatio: 0.8,
|
||||
}),
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/node_modules\/jquery\/dist\/jquery\.js$/,
|
||||
'./jquery.min.js'),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -865,6 +865,17 @@ babel-plugin-transform-flow-strip-types@^6.8.0:
|
|||
babel-plugin-syntax-flow "^6.18.0"
|
||||
babel-runtime "^6.22.0"
|
||||
|
||||
babel-plugin-transform-imports@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-1.5.0.tgz#3105082ab489b1cee162e42d2ffe7b8f7c685f2e"
|
||||
dependencies:
|
||||
babel-types "^6.6.0"
|
||||
is-valid-path "^0.1.1"
|
||||
lodash.camelcase "^4.3.0"
|
||||
lodash.findkey "^4.6.0"
|
||||
lodash.kebabcase "^4.1.1"
|
||||
lodash.snakecase "^4.1.1"
|
||||
|
||||
babel-plugin-transform-inline-consecutive-adds@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.3.0.tgz#f07d93689c0002ed2b2b62969bdd99f734e03f57"
|
||||
|
@ -1095,7 +1106,7 @@ babel-traverse@^6.24.1, babel-traverse@^6.26.0:
|
|||
invariant "^2.2.2"
|
||||
lodash "^4.17.4"
|
||||
|
||||
babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
|
||||
babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0, babel-types@^6.6.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
|
||||
dependencies:
|
||||
|
@ -3625,6 +3636,12 @@ is-glob@^4.0.0:
|
|||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-invalid-path@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-invalid-path/-/is-invalid-path-0.1.0.tgz#307a855b3cf1a938b44ea70d2c61106053714f34"
|
||||
dependencies:
|
||||
is-glob "^2.0.0"
|
||||
|
||||
is-number@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
|
||||
|
@ -3737,6 +3754,12 @@ is-utf8@^0.2.0:
|
|||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||
|
||||
is-valid-path@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df"
|
||||
dependencies:
|
||||
is-invalid-path "^0.1.0"
|
||||
|
||||
is-windows@^1.0.1, is-windows@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
|
@ -4043,14 +4066,26 @@ lodash.camelcase@^4.3.0:
|
|||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
|
||||
lodash.findkey@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.findkey/-/lodash.findkey-4.6.0.tgz#83058e903b51cbb759d09ccf546dea3ea39c4718"
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
|
||||
lodash.kebabcase@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
|
||||
lodash.snakecase@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
|
||||
|
||||
lodash.some@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
|
||||
|
|
Loading…
Reference in New Issue