-> click to zoom on cameras
-> add a previewer for movies
-> add a previewer for snapshots
--> other todos
--> use svg instead of pngs for icons
--> add a motioneye.svg icon
\ No newline at end of file
+-> add a motioneye.svg icon
+-> other todos
\ No newline at end of file
def list_devices(self):
logging.debug('listing devices')
- self.finish_json({'devices': v4l2ctl.list_devices()})
+ configured_devices = {}
+ for camera_id in config.get_camera_ids():
+ data = config.get_camera(camera_id)
+ configured_devices[data['videodevice']] = True
+
+ devices = [{'device': d[0], 'name': d[1], 'configured': d[0] in configured_devices}
+ for d in v4l2ctl.list_devices()]
+
+ self.finish_json({'devices': devices})
def add_camera(self):
logging.debug('adding new camera')
div.page,
div.header-container {
position: relative;
- min-width: 320px;
+ min-width: 360px;
min-height: 60px;
width: 100%;
}
}
- /* icons */
+ /* icons & icon buttons */
-img.settings-button {
- margin: 2px;
- cursor: pointer;
+div.button.settings-button {
+ margin: 1px;
+ vertical-align: middle;
+ background-image: url(../img/settings.svg);
+ width: 48px;
+ height: 48px;
+}
+
+div.button.rem-camera-button {
+ display: none;
+ margin: 1px;
vertical-align: middle;
+ background-image: url(../img/settings.svg);
+ width: 48px;
+ height: 48px;
+ background-position: -48px 0px;
+}
+
+div.settings-top-bar.open div.button.rem-camera-button {
+ display: inline-block;
}
div.logo {
div.settings.open {
width: 40%;
- min-width: 320px;
+ min-width: 360px;
}
div.settings-container {
div.settings-top-bar.open {
background-color: #414141;
- min-width: 320px;
+ min-width: 360px;
}
div.settings-section-title {
display: none;
padding: 4px 1.5em 4px 4px;
vertical-align: middle;
- font-size: 20px;
+ font-size: 1.1em;
width: auto;
- max-width: 40%;
+ max-width: 35%;
}
div.apply-button {
}
+ /* dialogs */
+
+table.add-camera-dialog select,
+table.add-camera-dialog input[type=text],
+table.add-camera-dialog input[type=password] {
+ width: 10em;
+}
+
+
/* camera frames */
div.camera-list {
transition: all 0.1s linear;
}
-div.camera-button:ACTIVE {
- opacity: 0.6;
-}
-
div.camera-button.close {
background-position: 0px 0px;
}
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
- cursor: pointer;
+ cursor: pointer;
+ display: inline-block;
}
div.button.dialog {
- display: inline-block;
background-color: #414141;
min-width: 60px;
height: 1.2em;
transition: all 0.1s linear;
}
-div.button.dialog:FOCUS,
-div.button.dialog:HOVER {
- border: 1px solid #3498db;
- background-color: #515151;
-}
-
-div.button.dialog:ACTIVE {
- background-color: #414141;
-}
-
div.button.dialog.default {
background-color: #317CAD;
}
-div.button.dialog.default:FOCUS,
-div.button.dialog.default:HOVER {
- background-color: #3498db;
+div.button.mouse-effect {
+ opacity: 0.9;
+ transition: opacity 0.1s linear;
}
-div.button.dialog.default:ACTIVE {
- background-color: #317CAD;
+div.button.mouse-effect:HOVER {
+ opacity: 1;
+}
+
+div.button.mouse-effect:ACTIVE {
+ opacity: 0.6;
}
height: 16px;
background-image: url(../img/top-bar-buttons.svg);
opacity: 1;
- transition: all 0.1s linear;
cursor: pointer;
}
-div.modal-close-button:ACTIVE {
- opacity: 0.6;
-}
-
table.modal-buttons-container {
width: 100%;
+ margin: auto;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
display: block;
}
+div.modal-progress {
+ border-radius: 10px;
+ background-image: url(../img/modal-progress.gif);
+ width: 64px;
+ height: 64px;
+}
+
+td.dialog-item-label {
+ text-align: right;
+ padding-right: 5px;
+}
+
+td.dialog-item-value {
+ text-align: left;
+ padding-left: 5px;
+}
+
+span.dialog-item-label {
+ font-size: 0.9em;
+}
+
/* misc */
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="48"
+ width="96"
height="48"
id="svg2"
version="1.1"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="7.59375"
- inkscape:cx="72.187114"
- inkscape:cy="23.704825"
+ inkscape:zoom="17.085938"
+ inkscape:cx="93.239037"
+ inkscape:cy="25.695745"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:snap-page="true"
- inkscape:snap-intersection-paths="true" />
+ inkscape:snap-intersection-paths="true"
+ showguides="true"
+ inkscape:guide-bbox="true" />
<metadata
id="metadata7">
<rdf:RDF>
id="layer1"
transform="translate(0,-1004.3622)">
<path
- style="fill:#3498db;fill-opacity:1;stroke:none;opacity:1"
- d="M 23 30 C 20.954307 30 19.211772 31.240478 18.4375 33 L 5 33 C 4.446 33 4 33.446 4 34 L 4 36 C 4 36.554 4.446 37 5 37 L 18.4375 37 C 19.211772 38.759522 20.954307 40 23 40 C 25.045693 40 26.788228 38.759522 27.5625 37 L 43 37 C 43.554 37 44 36.554 44 36 L 44 34 C 44 33.446 43.554 33 43 33 L 27.5625 33 C 26.788228 31.240478 25.045693 30 23 30 z M 23 33 C 24.10457 33 25 33.89543 25 35 C 25 36.10457 24.10457 37 23 37 C 21.89543 37 21 36.10457 21 35 C 21 33.89543 21.89543 33 23 33 z "
+ style="fill:#3498db;fill-opacity:1;stroke:none"
+ d="m 23,30 c -2.045693,0 -3.788228,1.240478 -4.5625,3 L 5,33 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 13.4375,0 c 0.774272,1.759522 2.516807,3 4.5625,3 2.045693,0 3.788228,-1.240478 4.5625,-3 L 43,37 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 L 27.5625,33 C 26.788228,31.240478 25.045693,30 23,30 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
transform="translate(0,1004.3622)"
- id="rect3755" />
+ id="rect3755"
+ inkscape:connector-curvature="0" />
<path
- style="fill:#3498db;fill-opacity:1;stroke:none;opacity:1"
- d="M 36 20 C 33.954307 20 32.211772 21.240478 31.4375 23 L 5 23 C 4.446 23 4 23.446 4 24 L 4 26 C 4 26.554 4.446 27 5 27 L 31.4375 27 C 32.211772 28.759522 33.954307 30 36 30 C 38.045693 30 39.788228 28.759522 40.5625 27 L 43 27 C 43.554 27 44 26.554 44 26 L 44 24 C 44 23.446 43.554 23 43 23 L 40.5625 23 C 39.788228 21.240478 38.045693 20 36 20 z M 36 23 C 37.10457 23 38 23.89543 38 25 C 38 26.10457 37.10457 27 36 27 C 34.89543 27 34 26.10457 34 25 C 34 23.89543 34.89543 23 36 23 z "
+ style="fill:#3498db;fill-opacity:1;stroke:none"
+ d="m 36,20 c -2.045693,0 -3.788228,1.240478 -4.5625,3 L 5,23 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 26.4375,0 c 0.774272,1.759522 2.516807,3 4.5625,3 2.045693,0 3.788228,-1.240478 4.5625,-3 L 43,27 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 l -2.4375,0 C 39.788228,21.240478 38.045693,20 36,20 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
transform="translate(0,1004.3622)"
- id="rect3757" />
+ id="rect3757"
+ inkscape:connector-curvature="0" />
<path
- style="fill:#3498db;fill-opacity:1;stroke:none;opacity:1"
- d="M 15 10 C 12.954308 10 11.211772 11.240478 10.4375 13 L 5 13 C 4.446 13 4 13.446 4 14 L 4 16 C 4 16.554 4.446 17 5 17 L 10.4375 17 C 11.211772 18.759522 12.954308 20 15 20 C 17.045692 20 18.788228 18.759522 19.5625 17 L 43 17 C 43.554 17 44 16.554 44 16 L 44 14 C 44 13.446 43.554 13 43 13 L 19.5625 13 C 18.788228 11.240478 17.045692 10 15 10 z M 15 13 C 16.10457 13 17 13.89543 17 15 C 17 16.10457 16.10457 17 15 17 C 13.89543 17 13 16.10457 13 15 C 13 13.89543 13.89543 13 15 13 z "
+ style="fill:#3498db;fill-opacity:1;stroke:none"
+ d="m 15,10 c -2.045692,0 -3.788228,1.240478 -4.5625,3 L 5,13 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 5.4375,0 c 0.774272,1.759522 2.516808,3 4.5625,3 2.045692,0 3.788228,-1.240478 4.5625,-3 L 43,17 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 L 19.5625,13 C 18.788228,11.240478 17.045692,10 15,10 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
transform="translate(0,1004.3622)"
- id="rect3759" />
+ id="rect3759"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#3498db;stroke-width:2.6408689;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path3764"
+ sodipodi:cx="67.901237"
+ sodipodi:cy="6.4814816"
+ sodipodi:rx="20.098766"
+ sodipodi:ry="19.518518"
+ d="m 88.000004,6.4814816 a 20.098766,19.518518 0 1 1 -40.197533,0 20.098766,19.518518 0 1 1 40.197533,0 z"
+ transform="matrix(0.74631447,0,0,0.76850096,21.324322,1023.3813)" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3766"
+ d="m 82,1030.3622 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 -13.004,0 -8.7461,0 -20,0 -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 12.4816,0 8.4097,0 20,0 z"
+ style="fill:#3498db;fill-opacity:1;stroke:none"
+ sodipodi:nodetypes="cssccsscc" />
</g>
</svg>
$('#workingScheduleSwitch').change(updateConfigUi);
/* fetch & push handlers */
- $('#videoDeviceSelect').change(fetchCurrentCameraConfig);
+ $('#videoDeviceSelect').change(function () {
+ if ($('#videoDeviceSelect').val() === 'add') {
+ runAddCameraDialog();
+ if ($('#videoDeviceSelect').find('option').length > 1) {
+ $('#videoDeviceSelect')[0].selectedIndex = 0;
+ }
+ else {
+ $('#videoDeviceSelect')[0].selectedIndex = -1;
+ }
+ $('#videoDeviceSelect').change();
+ }
+ else {
+ fetchCurrentCameraConfig();
+ }
+ });
$('input.general').change(pushMainConfig);
$('input.device, select.device, ' +
'input.storage, select.storage, ' +
});
/* re-validate all the input validators */
- $('div.settings').find('input.text-validator, input.number-validator, input.time-validator').each(function () {
+ $('div.settings').find('input.validator').each(function () {
this.validate();
});
return;
}
+ if (!configUiValid()) {
+ runAlertDialog('Make sure all the configuration options are valid!');
+
+ return;
+ }
+
showProgress();
for (var i = 0; i < configs.length; i++) {
var camera = cameras[i];
videoDeviceSelect.append('<option value="' + camera['id'] + '">' + camera['name'] + '</option>');
}
+ videoDeviceSelect.append('<option value="add">add camera...</option>');
- if (cameras.length) {
+ if (cameras.length > 1) {
videoDeviceSelect[0].selectedIndex = 0;
fetchCurrentCameraConfig();
}
+ else {
+ videoDeviceSelect[0].selectedIndex = -1;
+ }
recreateCameraFrames(cameras);
});
}
+ /* dialogs */
+
+function runAlertDialog(message) {
+ runModalDialog({title: message, buttons: 'ok'});
+}
+
+function runAddCameraDialog(devices) {
+ var content =
+ $('<table class="add-camera-dialog">' +
+ '<tr>' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Device</span></td>' +
+ '<td class="dialog-item-value"><select class="styled" id="deviceSelect"></select></td>' +
+ '<td><span class="help-mark" title="the device you wish to add to motionEye">?</span></td>' +
+ '</tr>' +
+ '<tr class="remote">' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Host</span></td>' +
+ '<td class="dialog-item-value"><input type="text" class="styled" id="hostEntry" placeholder="e.g. 192.168.1.2"></td>' +
+ '<td><span class="help-mark" title="the remote motionEye host (e.g. 192.168.1.2)">?</span></td>' +
+ '</tr>' +
+ '<tr class="remote">' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Port</span></td>' +
+ '<td class="dialog-item-value"><input type="text" class="styled" id="portEntry" placeholder="e.g. 80"></td>' +
+ '<td><span class="help-mark" title="the remote motionEye port (e.g. 80)">?</span></td>' +
+ '</tr>' +
+ '<tr class="remote">' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Username</span></td>' +
+ '<td class="dialog-item-value"><input type="text" class="styled" id="usernameEntry" placeholder="username..."></td>' +
+ '<td><span class="help-mark" title="the remote administrator\'s username">?</span></td>' +
+ '</tr>' +
+ '<tr class="remote">' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Password</span></td>' +
+ '<td class="dialog-item-value"><input type="password" class="styled" id="passwordEntry" placeholder="password..."></td>' +
+ '<td><span class="help-mark" title="the remote administrator\'s password">?</span></td>' +
+ '</tr>' +
+ '</table>');
+
+ /* collect ui widgets */
+ var deviceSelect = content.find('#deviceSelect');
+ var hostEntry = content.find('#hostEntry');
+ var portEntry = content.find('#portEntry');
+ var usernameEntry = content.find('#usernameEntry');
+ var passwordEntry = content.find('#passwordEntry');
+
+ /* make validators */
+ makeTextValidator(hostEntry, true);
+ makeNumberValidator(portEntry, 1, 65535, false, false, true);
+ makeTextValidator(usernameEntry, true);
+
+ /* ui interaction */
+ content.find('tr.remote').css('display', 'none');
+ var updateUi = function () {
+ if (deviceSelect.val() === 'remote') {
+ content.find('tr.remote').css('display', 'table-row');
+ }
+ else {
+ content.find('tr.remote').css('display', 'none');
+ }
+
+ updateModalDialogPosition();
+ };
+
+ deviceSelect.change(updateUi).change();
+
+ showModalDialog('<div class="modal-progress"></div>');
+
+ /* fetch the available devices */
+ ajax('GET', '/config/list_devices/', null, function (data) {
+ /* add available devices */
+ data.devices.forEach(function (device) {
+ if (!device.configured) {
+ deviceSelect.append('<option value="' + device.device + '">' + device.name + '</option>');
+ }
+ });
+
+ deviceSelect.append('<option value="remote">Remote device...</option>');
+
+ runModalDialog({
+ title: 'Add Camera...',
+ closeButton: true,
+ buttons: 'okcancel',
+ content: content,
+ onOk: function () {
+
+ }
+ });
+ });
+}
+
+
/* camera frames */
function addCameraFrameUi(cameraId, cameraName, framerate) {
'<div class="camera-top-bar">' +
'<span class="camera-name"></span>' +
'<div class="camera-buttons">' +
- '<div class="camera-button configure" title="configure"></div>' +
- '<div class="camera-button close" title="close"></div>' +
+ '<div class="button camera-button mouse-effect configure" title="configure"></div>' +
+ '<div class="button camera-button mouse-effect close" title="close"></div>' +
'</div>' +
'</div>' +
'<div class="camera-container">' +
$(document).ready(function () {
/* open/close settings */
- $('img.settings-button').click(function () {
+ $('div.settings-button').click(function () {
if (isSettingsOpen()) {
closeSettings();
}
$input.blur(validate);
$input.change(validate).change();
+ $input.addClass('validator');
$input.addClass('text-validator');
$input[0].validate = validate;
}
$input.blur(validate);
$input.change(validate).change();
+ $input.addClass('validator');
$input.addClass('number-validator');
$input[0].validate = validate;
}
timeFormat: 'H:i',
});
+ $input.addClass('validator');
$input.addClass('time-validator');
$input[0].validate = validate;
}
+function makeRegexValidator($input, regex, required) {
+ if (required == null) {
+ required = true;
+ }
+
+ function isValid(strVal) {
+ if (!$input.parents('tr:eq(0)').is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
+
+ if (strVal.length === 0 && !required) {
+ return true;
+ }
+
+ return strVal.match(new RegExp(regex)) != null;
+ }
+
+ var msg = 'enter a valid value';
+
+ function validate() {
+ var strVal = $input.val();
+ if (isValid(strVal)) {
+ $input.attr('title', '');
+ $input.removeClass('error');
+ $input[0].invalid = false;
+ }
+ else {
+ $input.attr('title', msg);
+ $input.addClass('error');
+ $input[0].invalid = true;
+ }
+ }
+
+ $input.keyup(validate);
+ $input.blur(validate);
+ $input.change(validate).change();
+
+ $input.addClass('validator');
+ $input.addClass('regex-validator');
+ $input[0].validate = validate;
+}
+
/* modal dialog */
var glass = $('div.modal-glass');
var container = $('div.modal-container');
+ if (container.is(':visible')) {
+ /* the modal dialog is already visible,
+ * we just replace the content */
+
+ if (container[0]._onClose) {
+ container[0]._onClose();
+ }
+
+ container[0]._onClose = onClose; /* remember the onClose handler */
+ container.html(content);
+ updateModalDialogPosition();
+
+ return;
+ }
+
glass.css('display', 'block');
glass.animate({'opacity': '0.7'}, 200);
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var modalWidth = container.width();
- var modalHeight = container.width();
+ var modalHeight = container.height();
container.css('left', (windowWidth - modalWidth) / 2);
container.css('top', (windowHeight - modalHeight) / 2);
var tr = buttonsContainer.find('tr');
buttonsInfo.forEach(function (info) {
- var buttonDiv = $('<div class="button dialog"></div>');
+ var buttonDiv = $('<div class="button dialog mouse-effect"></div>');
buttonDiv.click(hideModalDialog); /* every button closes the dialog */
buttonDiv.attr('tabIndex', '0'); /* make button focusable */
tr.append(td);
});
+ /* limit the size of the buttons container */
+ buttonsContainer.css('max-width', (buttonsInfo.length * 10) + 'em');
+
return buttonsContainer;
}
titleBar.append(titleSpan);
if (options.closeButton) {
- var closeButton = $('<div class="modal-close-button" title="close"></div>');
+ var closeButton = $('<div class="button modal-close-button mouse-effect" title="close"></div>');
closeButton.click(hideModalDialog);
titleBar.append(closeButton);
}
* * title: String
* * closeButton: Boolean
* * content: any
- * * buttons: 'yesno'|'okcancel'|Array
+ * * buttons: 'ok'|'yesno'|'okcancel'|Array
* * onYes: Function
* * onNo: Function
* * onOk: Function
/* add supplied content */
if (options.content) {
- content.append(options.content);
+ var contentWrapper = $('<div style="padding: 10px;"></div>');
+ contentWrapper.append(options.content);
+ content.append(contentWrapper);
}
/* add buttons */
{caption: 'Yes', isDefault: true, click: options.onYes}
];
}
+ if (options.buttons === 'yesnocancel') {
+ options.buttons = [
+ {caption: 'Cancel', click: options.onCancel},
+ {caption: 'No', click: options.onNo},
+ {caption: 'Yes', isDefault: true, click: options.onYes}
+ ];
+ }
else if (options.buttons === 'okcancel') {
options.buttons = [
{caption: 'Cancel', click: options.onCancel},
{caption: 'OK', isDefault: true, click: options.onOk}
];
}
+ else if (options.buttons === 'ok') {
+ options.buttons = [
+ {caption: 'OK', isDefault: true, click: options.onOk}
+ ];
+ }
if (options.buttons) {
buttonsDiv = makeModalDialogButtons(options.buttons);
});
}
+ /* add some margins */
if ((buttonsDiv || options.content) && titleBar) {
titleBar.css('margin-bottom', '5px');
}
+ if (buttonsDiv && options.content) {
+ buttonsDiv.css('margin-top', '5px');
+ }
+
var handleKeyUp = function (e) {
switch (e.which) {
case 13:
<div class="header">
<div class="header-container">
<div class="settings-top-bar">
- <img class="settings-button" src="{{STATIC_URL}}img/settings.svg" title="settings">
+ <div class="button settings-button mouse-effect" title="settings"></div>
<select class="video-device styled" id="videoDeviceSelect"></select>
+ <div class="button rem-camera-button mouse-effect" id="remCameraButton" title="remove camera"></div>
<div class="button apply-button" id="applyButton">Apply</div>
</div>
<div class="logo">