From a796150d808ce9ae96ae04053d46f6cb77290b74 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 5 Oct 2013 16:47:02 +0300 Subject: [PATCH] added ui for adding a device --- doc/todo.txt | 5 +- src/handlers.py | 10 ++- static/css/main.css | 47 +++++++++---- static/css/ui.css | 55 +++++++++------ static/img/modal-progress.gif | Bin 0 -> 4389 bytes static/img/settings.svg | 49 +++++++++---- static/js/main.js | 125 ++++++++++++++++++++++++++++++++-- static/js/ui.js | 92 +++++++++++++++++++++++-- templates/main.html | 3 +- 9 files changed, 321 insertions(+), 65 deletions(-) create mode 100644 static/img/modal-progress.gif 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 0000000000000000000000000000000000000000..df1657ce543ae9161758c723a1d149820a2ef043 GIT binary patch literal 4389 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 A@Bjb+ literal 0 HcmV?d00001 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