From: Calin Crisan Date: Sat, 5 Oct 2013 13:47:02 +0000 (+0300) Subject: added ui for adding a device X-Git-Url: http://www.vanbest.org/gitweb/?a=commitdiff_plain;h=a796150d808ce9ae96ae04053d46f6cb77290b74;p=motioneye-debian added ui for adding a device --- diff --git a/doc/todo.txt b/doc/todo.txt index 0b81b5b..3b57167 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -19,6 +19,5 @@ -> 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 diff --git a/src/handlers.py b/src/handlers.py index 3127648..abc589e 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -200,7 +200,15 @@ class ConfigHandler(BaseHandler): 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') diff --git a/static/css/main.css b/static/css/main.css index a95a9ce..740e641 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -42,7 +42,7 @@ html { div.page, div.header-container { position: relative; - min-width: 320px; + min-width: 360px; min-height: 60px; width: 100%; } @@ -89,12 +89,28 @@ div.page-container.stretched { } - /* 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 { @@ -135,7 +151,7 @@ div.settings { div.settings.open { width: 40%; - min-width: 320px; + min-width: 360px; } div.settings-container { @@ -158,7 +174,7 @@ div.settings-top-bar { div.settings-top-bar.open { background-color: #414141; - min-width: 320px; + min-width: 360px; } div.settings-section-title { @@ -204,9 +220,9 @@ select.video-device { 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 { @@ -271,6 +287,15 @@ input[type=text].working-schedule.number { } + /* 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 { @@ -318,10 +343,6 @@ div.camera-button { transition: all 0.1s linear; } -div.camera-button:ACTIVE { - opacity: 0.6; -} - div.camera-button.close { background-position: 0px 0px; } diff --git a/static/css/ui.css b/static/css/ui.css index eba130f..ecf0385 100644 --- a/static/css/ui.css +++ b/static/css/ui.css @@ -24,11 +24,11 @@ div.button { -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; @@ -41,27 +41,21 @@ div.button.dialog { 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; } @@ -312,16 +306,12 @@ div.modal-close-button { 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; @@ -336,6 +326,27 @@ table.modal-buttons-container div.button.dialog { 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 */ diff --git a/static/img/modal-progress.gif b/static/img/modal-progress.gif new file mode 100644 index 0000000..df1657c Binary files /dev/null and b/static/img/modal-progress.gif differ diff --git a/static/img/settings.svg b/static/img/settings.svg index 683f538..c11c28e 100644 --- a/static/img/settings.svg +++ b/static/img/settings.svg @@ -9,7 +9,7 @@ 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" @@ -27,9 +27,9 @@ 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" @@ -50,7 +50,9 @@ 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" /> @@ -69,19 +71,38 @@ id="layer1" transform="translate(0,-1004.3622)"> + id="rect3755" + inkscape:connector-curvature="0" /> + id="rect3757" + inkscape:connector-curvature="0" /> + id="rect3759" + inkscape:connector-curvature="0" /> + + diff --git a/static/js/main.js b/static/js/main.js index 7874cd7..0f39a61 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -208,7 +208,21 @@ function initUI() { $('#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, ' + @@ -368,7 +382,7 @@ function updateConfigUi() { }); /* 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(); }); @@ -687,6 +701,12 @@ function doApply() { return; } + if (!configUiValid()) { + runAlertDialog('Make sure all the configuration options are valid!'); + + return; + } + showProgress(); for (var i = 0; i < configs.length; i++) { @@ -725,11 +745,15 @@ function fetchCurrentConfig() { var camera = cameras[i]; videoDeviceSelect.append(''); } + videoDeviceSelect.append(''); - if (cameras.length) { + if (cameras.length > 1) { videoDeviceSelect[0].selectedIndex = 0; fetchCurrentCameraConfig(); } + else { + videoDeviceSelect[0].selectedIndex = -1; + } recreateCameraFrames(cameras); }); @@ -785,6 +809,95 @@ function pushPreview() { } + /* dialogs */ + +function runAlertDialog(message) { + runModalDialog({title: message, buttons: 'ok'}); +} + +function runAddCameraDialog(devices) { + var content = + $('' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Device?
Host?
Port?
Username?
Password?
'); + + /* 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(''); + + /* 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(''); + } + }); + + deviceSelect.append(''); + + runModalDialog({ + title: 'Add Camera...', + closeButton: true, + buttons: 'okcancel', + content: content, + onOk: function () { + + } + }); + }); +} + + /* camera frames */ function addCameraFrameUi(cameraId, cameraName, framerate) { @@ -793,8 +906,8 @@ function addCameraFrameUi(cameraId, cameraName, framerate) { '
' + '' + '
' + - '
' + - '
' + + '
' + + '
' + '
' + '
' + '
' + @@ -969,7 +1082,7 @@ function refreshCameraFrames() { $(document).ready(function () { /* open/close settings */ - $('img.settings-button').click(function () { + $('div.settings-button').click(function () { if (isSettingsOpen()) { closeSettings(); } diff --git a/static/js/ui.js b/static/js/ui.js index 0b2eaab..2b28bf6 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -266,6 +266,7 @@ function makeTextValidator($input, required) { $input.blur(validate); $input.change(validate).change(); + $input.addClass('validator'); $input.addClass('text-validator'); $input[0].validate = validate; } @@ -357,6 +358,7 @@ function makeNumberValidator($input, minVal, maxVal, floating, sign, required) { $input.blur(validate); $input.change(validate).change(); + $input.addClass('validator'); $input.addClass('number-validator'); $input[0].validate = validate; } @@ -395,10 +397,53 @@ function makeTimeValidator($input) { 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 */ @@ -406,6 +451,21 @@ function showModalDialog(content, onClose) { 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); @@ -445,7 +505,7 @@ function updateModalDialogPosition() { 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); @@ -462,7 +522,7 @@ function makeModalDialogButtons(buttonsInfo) { var tr = buttonsContainer.find('tr'); buttonsInfo.forEach(function (info) { - var buttonDiv = $('
'); + var buttonDiv = $('
'); buttonDiv.click(hideModalDialog); /* every button closes the dialog */ buttonDiv.attr('tabIndex', '0'); /* make button focusable */ @@ -481,6 +541,9 @@ function makeModalDialogButtons(buttonsInfo) { tr.append(td); }); + /* limit the size of the buttons container */ + buttonsContainer.css('max-width', (buttonsInfo.length * 10) + 'em'); + return buttonsContainer; } @@ -498,7 +561,7 @@ function makeModalDialogTitleBar(options) { titleBar.append(titleSpan); if (options.closeButton) { - var closeButton = $(''); + var closeButton = $(''); closeButton.click(hideModalDialog); titleBar.append(closeButton); } @@ -511,7 +574,7 @@ function runModalDialog(options) { * * title: String * * closeButton: Boolean * * content: any - * * buttons: 'yesno'|'okcancel'|Array + * * buttons: 'ok'|'yesno'|'okcancel'|Array * * onYes: Function * * onNo: Function * * onOk: Function @@ -532,7 +595,9 @@ function runModalDialog(options) { /* add supplied content */ if (options.content) { - content.append(options.content); + var contentWrapper = $('
'); + contentWrapper.append(options.content); + content.append(contentWrapper); } /* add buttons */ @@ -542,12 +607,24 @@ function runModalDialog(options) { {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); @@ -560,10 +637,15 @@ function runModalDialog(options) { }); } + /* 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: diff --git a/templates/main.html b/templates/main.html index 810cf68..01d24f9 100644 --- a/templates/main.html +++ b/templates/main.html @@ -16,8 +16,9 @@
- +
+
Apply