From e131ad5e914c565ad2f9eff0793cec97e47e99e8 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 5 Oct 2013 13:46:31 +0300 Subject: [PATCH] added modal dialog functionality --- ...{video-buttons.svg => top-bar-buttons.svg} | 54 ++--- doc/colors.txt | 2 +- doc/todo.txt | 4 + motioneye.py | 2 +- src/config.py | 8 +- src/handlers.py | 2 +- static/css/main.css | 19 +- static/css/ui.css | 123 ++++++++++- static/img/top-bar-buttons.png | Bin 0 -> 596 bytes static/img/video-buttons.png | Bin 404 -> 0 bytes static/js/main.js | 47 ++-- static/js/ui.js | 206 ++++++++++++++++++ templates/main.html | 4 +- 13 files changed, 398 insertions(+), 73 deletions(-) rename artwork/{video-buttons.svg => top-bar-buttons.svg} (68%) create mode 100644 static/img/top-bar-buttons.png delete mode 100644 static/img/video-buttons.png diff --git a/artwork/video-buttons.svg b/artwork/top-bar-buttons.svg similarity index 68% rename from artwork/video-buttons.svg rename to artwork/top-bar-buttons.svg index f9a4aad..cc54fdc 100644 --- a/artwork/video-buttons.svg +++ b/artwork/top-bar-buttons.svg @@ -14,8 +14,8 @@ id="svg2" version="1.1" inkscape:version="0.48.4 r9939" - sodipodi:docname="video-buttons.svg" - inkscape:export-filename="/media/data/projects/motioneye/static/img/validation-error.png" + sodipodi:docname="top-bar-buttons.svg" + inkscape:export-filename="/media/data/projects/motioneye/static/img/top-bar-buttons.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"> image/svg+xml - + @@ -72,16 +72,16 @@ transform="translate(0,-1036.3622)"> + style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" /> + style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" /> + style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" /> + style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" /> + style="fill:none;stroke:#3498db;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#3498db;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#3498db;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> make camera frames positions configurable +-> hide horrible 404 image on cameras +-> prevent Request closed errors by stopping mjpg clients before stopping motion +-> network server storage should only be enabled on special devices (rpi) -> camera not available background and icon design -> remove current snapshot GET logs -> add a motion running status indicator (and maybe a start/stop button) diff --git a/motioneye.py b/motioneye.py index b6a7938..8e45f10 100644 --- a/motioneye.py +++ b/motioneye.py @@ -48,7 +48,7 @@ def _start_server(): def _start_motion(): - if not motionctl.running() and len(config.get_enabled_cameras()) > 0: + if not motionctl.running() and config.has_enabled_cameras(): motionctl.start() logging.info('motion started') diff --git a/src/config.py b/src/config.py index 4f2fbf6..52d1028 100644 --- a/src/config.py +++ b/src/config.py @@ -136,10 +136,13 @@ def get_camera_ids(): return camera_ids -def get_enabled_cameras(): +def has_enabled_cameras(): + if not get_main().get('@enabled'): + return False + camera_ids = get_camera_ids() cameras = [get_camera(camera_id) for camera_id in camera_ids] - return [c for c in cameras if c['@enabled']] + return bool([c for c in cameras if c['@enabled']]) def get_camera(camera_id, as_lines=False): @@ -515,6 +518,7 @@ def _set_default_motion_camera(data): data.setdefault('quality', 75) data.setdefault('@preserve_images', 0) + data.setdefault('motion_movies', False) data.setdefault('ffmpeg_variable_bitrate', 14) data.setdefault('movie_filename', '') data.setdefault('ffmpeg_cap_new', False) diff --git a/src/handlers.py b/src/handlers.py index baedcd7..3127648 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -146,7 +146,7 @@ class ConfigHandler(BaseHandler): finally: if restart: - if len(config.get_enabled_cameras()) > 0: + if config.has_enabled_cameras(): motionctl.start() def set_preview(self, camera_id): diff --git a/static/css/main.css b/static/css/main.css index 4d65e15..2bfd409 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -43,6 +43,7 @@ div.page, div.header-container { position: relative; min-width: 320px; + min-height: 60px; width: 100%; } @@ -252,14 +253,6 @@ div.check-box.section { float: left; } -div.check-box.section div.check-box-button { - background-color: #515151; -} - -div.check-box.on.section div.check-box-button { - background-color: #3498db; -} - input[type=text].working-schedule.number { width: 50px; } @@ -305,19 +298,15 @@ div.camera-button { display: inline-block; width: 16px; height: 16px; - background-image: url(../img/video-buttons.png); + background-image: url(../img/top-bar-buttons.png); margin-left: 3px; cursor: pointer; - opacity: 0.7; - transition: all 0.1s linear; -} - -div.camera-button:HOVER { opacity: 1; + transition: all 0.1s linear; } div.camera-button:ACTIVE { - opacity: 0.5; + opacity: 0.6; } div.camera-button.close { diff --git a/static/css/ui.css b/static/css/ui.css index 97afe28..3fcc012 100644 --- a/static/css/ui.css +++ b/static/css/ui.css @@ -18,7 +18,7 @@ input[type=checkbox].styled { } - /* button */ + /* buttons */ div.button { -webkit-user-select: none; @@ -27,6 +27,43 @@ div.button { cursor: pointer; } +div.button.dialog { + display: inline-block; + background-color: #414141; + min-width: 60px; + height: 1.2em; + line-height: 1.2em; + text-align: center; + padding: 0.2em 0.4em; + border: 1px solid #317CAD; + border-radius: 2px; + color: white; + 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.dialog.default:ACTIVE { + background-color: #317CAD; +} + /* check box */ @@ -35,7 +72,7 @@ div.check-box { position: relative; width: 2.5em; height: 1em; - border: 1px solid #2A6C96; + border: 1px solid #317CAD; border-radius: 2px; color: #aaaaaa; vertical-align: middle; @@ -72,17 +109,22 @@ span.check-box-text { div.check-box.on div.check-box-button { left: 50%; - background-color: #3498db; + background-color: #317CAD; color: white; } +div.check-box.on:FOCUS div.check-box-button, +div.check-box.on:HOVER div.check-box-button { + background-color: #3498db; +} + /* input box */ input[type=password].styled, input[type=text].styled { width: 90%; - border: 1px solid #2A6C96; + border: 1px solid #317CAD; border-radius: 2px; background-color: transparent; padding: 1px; @@ -128,7 +170,7 @@ select.styled { -webkit-appearance: none; appearance: none; width: 90%; - border: 1px solid #2A6C96; + border: 1px solid #317CAD; border-radius: 2px; background-color: transparent; padding: 1px 1.25em 1px 1px; @@ -198,7 +240,7 @@ div.slider-bar { } div.slider-bar-inside { - border: 1px solid #2A6C96; + border: 1px solid #317CAD; height: 3px; position: relative; top: 3px; @@ -226,6 +268,75 @@ div.slider-cursor { } + /* modal dialogs */ + +div.modal-glass { + display: none; + position: fixed; + z-index: 10000; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + background-color: black; + opacity: 0; +} + +div.modal-container { + position: fixed; + display: none; + z-index: 10001; + background-color: #313131; + border-radius: 3px; + opacity: 0; + padding: 5px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); +} + +div.modal-title-bar { + height: 1.5em; + line-height: 1.5em; + text-align: center; + position: relative; +} + +span.modal-title { + color: white; +} + +div.modal-close-button { + position: absolute; + top: 0.3em; + right: 0.3em; + width: 16px; + height: 16px; + background-image: url(../img/top-bar-buttons.png); + opacity: 1; + transition: all 0.1s linear; + cursor: pointer; +} + +div.modal-close-button:ACTIVE { + opacity: 0.6; +} + +table.modal-buttons-container { + width: 100%; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +table.modal-buttons-container td:not(:FIRST-CHILD) { + padding-left: 5px; +} + +table.modal-buttons-container div.button.dialog { + display: block; +} + + /* misc */ span.help-mark { diff --git a/static/img/top-bar-buttons.png b/static/img/top-bar-buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..871543b5c961e31f0d56bd417ef288d41614ef2b GIT binary patch literal 596 zcmV-a0;~OrP)>}iMxN-_vn1E`N!njr{AsnQDQ7!35xHD(%OQBn7?|gSA3UQ{}+xcc@-^}e&68@*X zN`H8FK7YL`;{@V9p(X$boPZja9`N<{bnm_>;GgowU(|bx)~bvdfU1bgk{-j79zzkA z0II7pX7%2pm6v=Mo~{M;-lAowffYagIazy8w#JW1rmJ9-b}ldk*|c$8vr2G3~Y4gh-tSn5>1=Z zHD#|w)1G!KBex&e=*~@e7N3+KtLDXAz7jtFBnV=?cvMn>0Z)YX<`Sm?OjR_rkCl4L$+5sqZyMzuwjW_NEeAcOb z{-6Jb&+!HTZcq2_t!2khgXeBKLk+BD$G=klhNu4+x#|E61>U&Hj!aOCCH_!!%3XM- iU&EJvjGh$m()Jr@jH~ztH}XaR0000d^6w)xC3s@w+D8>D{$(`If|dO;3aSXZh$pV znAYYySWL>s^OAy=l8!c#P*=>a_^>dORQJF>No~v!ej({O#Run$q{;>D4NN7ej7jlc zAFW(J_~3YxRJJyJx6#kTdTC6G_fq5#4ezVWBUkrnYM&rCri%|b&Q^rKiH4v4VT&(- z4R8-^Q$Z8gr}GH>VA_~3*xVR;_{YTN4Ic{F0V@|!ICyLG!mY22VE`YqZ~g*p*vbE! yhp1g$Wv!B%O7qnNHWBv5pHX)0@AwOE`)GxQ_Ohw~0000' + camera['name'] + ''); - } - - if (cameras.length) { - videoDeviceSelect[0].selectedIndex = 0; - fetchCurrentCameraConfig(); - } - - recreateCameraFrames(cameras); + + /* fetch the camera list */ + ajax('GET', '/config/list/', null, function (data) { + var i, cameras = data.cameras; + var videoDeviceSelect = $('#videoDeviceSelect'); + videoDeviceSelect.html(''); + for (i = 0; i < cameras.length; i++) { + var camera = cameras[i]; + videoDeviceSelect.append(''); + } + + if (cameras.length) { + videoDeviceSelect[0].selectedIndex = 0; + fetchCurrentCameraConfig(); + } + + recreateCameraFrames(cameras); + }); }); } @@ -866,6 +870,11 @@ function remCameraFrameUi(cameraId) { } function recreateCameraFrames(cameras) { + /* if motioneye is globally disabled, we remove all the camera frames */; + if (!$('#motionEyeSwitch')[0].checked) { + cameras = []; + } + var pageContainer = $('div.page-container'); function updateCameras(cameras) { diff --git a/static/js/ui.js b/static/js/ui.js index 463fbe5..0b2eaab 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -1,4 +1,7 @@ + + /* UI widgets */ + function makeCheckBox($input) { var mainDiv = $('
'); var buttonDiv = $('
'); @@ -223,6 +226,9 @@ function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decima return slider; } + + /* validators */ + function makeTextValidator($input, required) { if (required == null) { required = true; @@ -392,3 +398,203 @@ function makeTimeValidator($input) { $input.addClass('time-validator'); $input[0].validate = validate; } + + + /* modal dialog */ + +function showModalDialog(content, onClose) { + var glass = $('div.modal-glass'); + var container = $('div.modal-container'); + + glass.css('display', 'block'); + glass.animate({'opacity': '0.7'}, 200); + + container[0]._onClose = onClose; /* remember the onClose handler */ + container.html(content); + + container.css('display', 'block'); + updateModalDialogPosition(); + container.animate({'opacity': '1'}, 200); +} + +function hideModalDialog() { + var glass = $('div.modal-glass'); + var container = $('div.modal-container'); + + glass.animate({'opacity': '0'}, 200, function () { + glass.css('display', 'none'); + }); + + container.animate({'opacity': '0'}, 200, function () { + container.css('display', 'none'); + container.html(''); + }); + + /* run the onClose handler, if supplied */ + if (container[0]._onClose) { + container[0]._onClose(); + } +} + +function updateModalDialogPosition() { + var container = $('div.modal-container'); + if (!container.is(':visible')) { + return; + } + + var windowWidth = $(window).width(); + var windowHeight = $(window).height(); + var modalWidth = container.width(); + var modalHeight = container.width(); + + container.css('left', (windowWidth - modalWidth) / 2); + container.css('top', (windowHeight - modalHeight) / 2); +} + +function makeModalDialogButtons(buttonsInfo) { + /* buttonsInfo is an array of: + * * caption: String + * * isDefault: Boolean + * * click: Function + */ + + var buttonsContainer = $(''); + var tr = buttonsContainer.find('tr'); + + buttonsInfo.forEach(function (info) { + var buttonDiv = $('
'); + + buttonDiv.click(hideModalDialog); /* every button closes the dialog */ + buttonDiv.attr('tabIndex', '0'); /* make button focusable */ + buttonDiv.html(info.caption); + + if (info.isDefault) { + buttonDiv.addClass('default'); + } + + if (info.click) { + buttonDiv.click(info.click); + } + + var td = $(''); + td.append(buttonDiv); + tr.append(td); + }); + + return buttonsContainer; +} + +function makeModalDialogTitleBar(options) { + /* available options: + * * title: String + * * closeButton: Boolean + */ + + var titleBar = $(''); + + var titleSpan = $(''); + titleSpan.html(options.title || ''); + + titleBar.append(titleSpan); + + if (options.closeButton) { + var closeButton = $(''); + closeButton.click(hideModalDialog); + titleBar.append(closeButton); + } + + return titleBar; +} + +function runModalDialog(options) { + /* available options: + * * title: String + * * closeButton: Boolean + * * content: any + * * buttons: 'yesno'|'okcancel'|Array + * * onYes: Function + * * onNo: Function + * * onOk: Function + * * onCancel: Function + * * onClose: Function + */ + + var content = $('
'); + var titleBar = null; + var buttonsDiv = null; + var defaultClick = null; + + /* add title bar */ + if (options.title) { + titleBar = makeModalDialogTitleBar({title: options.title, closeButton: options.closeButton}); + content.append(titleBar); + } + + /* add supplied content */ + if (options.content) { + content.append(options.content); + } + + /* add buttons */ + if (options.buttons === 'yesno') { + options.buttons = [ + {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} + ]; + } + + if (options.buttons) { + buttonsDiv = makeModalDialogButtons(options.buttons); + content.append(buttonsDiv); + + options.buttons.forEach(function (info) { + if (info.isDefault) { + defaultClick = info.click; + } + }); + } + + if ((buttonsDiv || options.content) && titleBar) { + titleBar.css('margin-bottom', '5px'); + } + + var handleKeyUp = function (e) { + switch (e.which) { + case 13: + if (defaultClick) { + defaultClick(); + } + /* intentionally no break */ + + case 27: + hideModalDialog(); + } + }; + + var onClose = function () { + if (options.onClose) { + options.onClose(); + } + + /* unbind html handlers */ + + $('html').unbind('keyup', handleKeyUp); + }; + + /* bind key handlers */ + $('html').bind('keyup', handleKeyUp); + + /* and finally, show the dialog */ + showModalDialog(content, onClose); + + /* focus the default button if nothing else is focused */ + if (content.find('*:focus').length === 0) { + content.find('div.button.default').focus(); + } +} diff --git a/templates/main.html b/templates/main.html index 04bf770..5d07a3d 100644 --- a/templates/main.html +++ b/templates/main.html @@ -124,8 +124,8 @@ - ? + ? Frame Rate @@ -421,4 +421,6 @@ copyright © Calin Crisan 2013 + + {% endblock %} -- 2.39.5