From 2af2b90353090d8fb32fdd8d33ef717a0626c318 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 28 Sep 2013 17:47:23 +0300 Subject: [PATCH] added an apply button --- doc/todo.txt | 2 - src/config.py | 20 ++-- src/handlers.py | 26 ++++- static/css/base-site.css | 40 ++++++- static/css/ui.css | 10 ++ static/img/progress.gif | Bin 0 -> 4782 bytes static/js/base-site.js | 235 ++++++++++++++++++++++++++++----------- static/js/ui.js | 55 +++++++++ templates/base-site.html | 6 +- 9 files changed, 308 insertions(+), 86 deletions(-) create mode 100644 static/img/progress.gif diff --git a/doc/todo.txt b/doc/todo.txt index 9fd65dc..c5af23a 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -1,6 +1,4 @@ --> add complete js validation -> group @config rules to top --> notification: multiple email addresses -> what do we do with working schedule -> browser compatibility test -> hint text next to section titles diff --git a/src/config.py b/src/config.py index 9d67eab..c5c1923 100644 --- a/src/config.py +++ b/src/config.py @@ -69,6 +69,14 @@ def set_main(data): # read the actual configuration from file lines = get_main(as_lines=True) + # preserve the threads + if 'thread' not in data: + threads = data.setdefault('thread', []) + for line in lines: + match = re.match('^\s*thread\s+([a-zA-Z0-9.\-]+)', line) + if match: + threads.append(match.groups()[0]) + # write the configuration to file logging.debug('writing main config to %(path)s...' % {'path': _MAIN_CONFIG_FILE_PATH}) @@ -186,11 +194,7 @@ def set_camera(camera_id, data): elif not data['@enabled']: threads = [t for t in threads if t != config_file_name] - if len(threads): - main_config['thread'] = threads - - elif 'thread' in main_config: - del main_config['thread'] + main_config['thread'] = threads set_main(main_config) @@ -274,11 +278,7 @@ def rem_camera(camera_id): threads = main_config.setdefault('thread', []) threads = [t for t in threads if t != camera_config_name] - if len(threads): - main_config['thread'] = threads - - elif 'thread' in main_config: - del main_config['thread'] + main_config['thread'] = threads set_main(main_config) diff --git a/src/handlers.py b/src/handlers.py index b509729..2278593 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -69,8 +69,8 @@ class ConfigHandler(BaseHandler): else: logging.debug('getting main config') - # TODO _main_dict_to_ui - self.finish_json(config.get_main()) + ui_config = self._main_dict_to_ui(config.get_main()) + self.finish_json(ui_config) def set_config(self, camera_id): try: @@ -102,7 +102,7 @@ class ConfigHandler(BaseHandler): raise - # TODO _main_ui_to_dict + data = self._main_ui_to_dict(data) config.set_main(data) def list_cameras(self): @@ -130,6 +130,26 @@ class ConfigHandler(BaseHandler): logging.debug('removing camera %(id)s' % {'id': camera_id}) config.rem_camera(camera_id) + + def _main_ui_to_dict(self, ui): + return { + '@enabled': ui.get('enabled', True), + '@show_advanced': ui.get('show_advanced', False), + '@admin_username': ui.get('admin_username', ''), + '@admin_password': ui.get('admin_password', ''), + '@normal_username': ui.get('normal_username', ''), + '@normal_password': ui.get('normal_password', '') + } + + def _main_dict_to_ui(self, data): + return { + 'enabled': data.get('@enabled', True), + 'show_advanced': data.get('@show_advanced', False), + 'admin_username': data.get('@admin_username', ''), + 'admin_password': data.get('@admin_password', ''), + 'normal_username': data.get('@normal_username', ''), + 'normal_password': data.get('@normal_password', '') + } def _camera_ui_to_dict(self, ui): video_device = ui.get('device', '') diff --git a/static/css/base-site.css b/static/css/base-site.css index ed8662c..92ffcb0 100644 --- a/static/css/base-site.css +++ b/static/css/base-site.css @@ -42,7 +42,7 @@ div.page { margin-top: 50px; padding-bottom: 20px; font-size: 1em; - transition: all 0.5s; + transition: all 0.5s linear; } div.header { @@ -56,7 +56,7 @@ div.header { } div.header-container { - transition: all 0.5s; + transition: all 0.5s linear; } div.footer { @@ -70,7 +70,7 @@ div.footer { } div.page-container { - transition: all 0.2s; + transition: all 0.2s linear; padding: 5px; } @@ -89,7 +89,7 @@ div.settings { left: 0px; width: 0px; height: 100%; - transition: all 0.2s; + transition: all 0.2s linear; overflow: auto; } @@ -110,6 +110,7 @@ div.settings.open div.settings-container { } div.settings-top-bar { + position: relative; display: inline-block; width: 40%; height: 50px; @@ -165,6 +166,37 @@ select.video-device { vertical-align: middle; font-size: 20px; width: auto; + max-width: 40%; +} + +div.apply-button { + position: relative; + display: none; + opacity: 0; + float: right; + width: 80px; + height: 30px; + line-height: 30px; + text-align: center; + margin: 10px; + color: white; + font-weight: bold; + font-size: 17px; + background-color: #FF6F00; + border-radius: 3px; + transition: all 0.1s linear; +} + +div.apply-button:HOVER { + background-color: #FF7D19; +} + +div.apply-button:ACTIVE { + background-color: #F06800; +} + +div.apply-button.progress { + background-color: #FF9340; } div.settings-top-bar.open select.video-device { diff --git a/static/css/ui.css b/static/css/ui.css index 78bb79f..97afe28 100644 --- a/static/css/ui.css +++ b/static/css/ui.css @@ -18,6 +18,16 @@ input[type=checkbox].styled { } + /* button */ + +div.button { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: pointer; +} + + /* check box */ div.check-box { diff --git a/static/img/progress.gif b/static/img/progress.gif new file mode 100644 index 0000000000000000000000000000000000000000..12ccd9b30dd790348ee9e7c7aaacccb3d7d117c2 GIT binary patch literal 4782 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 W!53$aoc>_tuU0NtdjIdnZ{xpWqrDUW literal 0 HcmV?d00001 diff --git a/static/js/base-site.js b/static/js/base-site.js index 58e55d0..378a4cf 100644 --- a/static/js/base-site.js +++ b/static/js/base-site.js @@ -1,6 +1,5 @@ - -var noPushLock = 0; +var pushConfigs = {}; /* Ajax */ @@ -66,6 +65,23 @@ function initUI() { makeSlider($('#frameChangeThresholdSlider'), 0, 10000, 0, null, 3, 0, 'px'); makeSlider($('#noiseLevelSlider'), 0, 100, 0, null, 5, 0, '%'); + /* text validators */ + makeTextValidator($('#adminUsernameEntry'), true); + makeTextValidator($('#adminPasswordEntry'), true); + makeTextValidator($('#normalUsernameEntry'), true); + makeTextValidator($('#normalPasswordEntry'), true); + makeTextValidator($('#deviceNameEntry'), true); + makeTextValidator($('#networkServerEntry'), true); + makeTextValidator($('#networkShareNameEntry'), true); + makeTextValidator($('#networkUsernameEntry'), false); + makeTextValidator($('#networkPasswordEntry'), false); + makeTextValidator($('#rootDirectoryEntry'), true); + makeTextValidator($('#leftTextEntry'), true); + makeTextValidator($('#rightTextEntry'), true); + makeTextValidator($('#imageFileNameEntry'), true); + makeTextValidator($('#movieFileNameEntry'), true); + makeTextValidator($('#emailAddressesEntry'), true); + /* number validators */ makeNumberValidator($('#streamingPortEntry'), 1024, 65535, false, false, true); makeNumberValidator($('#snapshotIntervalEntry'), 1, 86400, false, false, true); @@ -90,21 +106,21 @@ function initUI() { makeTimeValidator($('#sundayTo')); /* ui elements that enable/disable other ui elements */ - $('#motionEyeSwitch').change(updateConfigUI); - $('#showAdvancedSwitch').change(updateConfigUI); - $('#storageDeviceSelect').change(updateConfigUI); - $('#autoBrightnessSwitch').change(updateConfigUI); - $('#leftTextSelect').change(updateConfigUI); - $('#rightTextSelect').change(updateConfigUI); - $('#captureModeSelect').change(updateConfigUI); - $('#autoNoiseDetectSwitch').change(updateConfigUI); - $('#videoDeviceSwitch').change(updateConfigUI); - $('#textOverlaySwitch').change(updateConfigUI); - $('#videoStreamingSwitch').change(updateConfigUI); - $('#stillImagesSwitch').change(updateConfigUI); - $('#motionMoviesSwitch').change(updateConfigUI); - $('#motionNotificationsSwitch').change(updateConfigUI); - $('#workingScheduleSwitch').change(updateConfigUI); + $('#motionEyeSwitch').change(updateConfigUi); + $('#showAdvancedSwitch').change(updateConfigUi); + $('#storageDeviceSelect').change(updateConfigUi); + $('#autoBrightnessSwitch').change(updateConfigUi); + $('#leftTextSelect').change(updateConfigUi); + $('#rightTextSelect').change(updateConfigUi); + $('#captureModeSelect').change(updateConfigUi); + $('#autoNoiseDetectSwitch').change(updateConfigUi); + $('#videoDeviceSwitch').change(updateConfigUi); + $('#textOverlaySwitch').change(updateConfigUi); + $('#videoStreamingSwitch').change(updateConfigUi); + $('#stillImagesSwitch').change(updateConfigUi); + $('#motionMoviesSwitch').change(updateConfigUi); + $('#motionNotificationsSwitch').change(updateConfigUi); + $('#workingScheduleSwitch').change(updateConfigUi); /* fetch & push handlers */ $('#videoDeviceSelect').change(fetchCameraConfig); @@ -118,11 +134,18 @@ function initUI() { 'input.motion-detection, select.motion-detection, ' + 'input.notifications, select.notifications, ' + 'input.working-schedule, select.working-schedule').change(pushCameraConfig); + + /* apply button */ + $('#applyButton').click(function () { + if ($(this).hasClass('progress')) { + return; /* in progress */ + } + + doApply(); + }); } -function updateConfigUI() { - noPushLock++; - +function updateConfigUi() { var objs = $('tr.settings-item, div.advanced-setting, table.advanced-setting, div.settings-section-title, table.settings'); function markHide() { @@ -223,7 +246,7 @@ function updateConfigUI() { }); /* re-validate all the input validators */ - $('div.settings').find('input.number-validator, input.time-validator').each(function () { + $('div.settings').find('input.text-validator, input.number-validator, input.time-validator').each(function () { this.validate(); }); @@ -238,34 +261,40 @@ function updateConfigUI() { this.selectedIndex = 0; } }); +} + +function configUiValid() { + var valid = true; + $('div.settings input, select').each(function () { + if (this.invalid) { + valid = false; + return false; + } + }); - noPushLock--; + return valid; } function mainUi2Dict() { return { - '@enabled': $('#motionEyeSwitch')[0].checked, - '@show_advanced': $('#showAdvancedSwitch')[0].checked, - '@admin_username': $('#adminUsernameEntry').val(), - '@admin_password': $('#adminPasswordEntry').val(), - '@normal_username': $('#normalUsernameEntry').val(), - '@normal_password': $('#normalPasswordEntry').val() + 'enabled': $('#motionEyeSwitch')[0].checked, + 'show_advanced': $('#showAdvancedSwitch')[0].checked, + 'admin_username': $('#adminUsernameEntry').val(), + 'admin_password': $('#adminPasswordEntry').val(), + 'normal_username': $('#normalUsernameEntry').val(), + 'normal_password': $('#normalPasswordEntry').val() }; } function dict2MainUi(dict) { - noPushLock++; - - $('#motionEyeSwitch')[0].checked = dict['@enabled']; - $('#showAdvancedSwitch')[0].checked = dict['@show_advanced']; - $('#adminUsernameEntry').val(dict['@admin_username']); - $('#adminPasswordEntry').val(dict['@admin_password']); - $('#normalUsernameEntry').val(dict['@normal_username']); - $('#normalPasswordEntry').val(dict['@normal_password']); - - updateConfigUI(); - - noPushLock--; + $('#motionEyeSwitch')[0].checked = dict['enabled']; + $('#showAdvancedSwitch')[0].checked = dict['show_advanced']; + $('#adminUsernameEntry').val(dict['admin_username']); + $('#adminPasswordEntry').val(dict['admin_password']); + $('#normalUsernameEntry').val(dict['normal_username']); + $('#normalPasswordEntry').val(dict['normal_password']); + + updateConfigUi(); } function cameraUi2Dict() { @@ -353,8 +382,6 @@ function cameraUi2Dict() { } function dict2CameraUi(dict) { - noPushLock++; - /* video device */ $('#videoDeviceSwitch')[0].checked = dict['enabled']; $('#deviceNameEntry').val(dict['name']); @@ -435,9 +462,98 @@ function dict2CameraUi(dict) { $('#sundayFrom').val(dict['sunday_from']); $('#sundayTo').val(dict['sunday_to']); - updateConfigUI(); + updateConfigUi(); +} + + + /* apply button */ + +function showApply() { + if (!$('div.settings-container').is(':visible')) { + return; /* settings panel is not open */ + } + + var applyButton = $('#applyButton'); + + applyButton.html('Apply'); + applyButton.css('display', 'inline-block'); + applyButton.animate({'opacity': '1'}, 100); + applyButton.removeClass('inactive'); +} + +function showProgress() { + if (!$('div.settings-container').is(':visible')) { + return; /* settings panel is not open */ + } + + var applyButton = $('#applyButton'); + + if (applyButton.hasClass('progress')) { + return; /* progress already visible */ + } - noPushLock--; + applyButton.html(''); + applyButton.css('display', 'inline-block'); + applyButton.animate({'opacity': '1'}, 100); + applyButton.addClass('progress'); +} + +function hideApply() { + if (!$('div.settings-container').is(':visible')) { + return; /* settings panel is not open */ + } + + var applyButton = $('#applyButton'); + + applyButton.animate({'opacity': '0'}, 200, function () { + applyButton.removeClass('progress'); + applyButton.css('display', 'none'); + }); +} + +function isProgress() { + var applyButton = $('#applyButton'); + + return applyButton.hasClass('progress'); +} + +function isApplyVisible() { + var applyButton = $('#applyButton'); + + return applyButton.is(':visible'); +} + +function doApply() { + var finishedCount = 0; + var configs = []; + + function testReady() { + if (finishedCount >= configs.length) { + hideApply(); + } + } + + for (var key in pushConfigs) { + if (pushConfigs.hasOwnProperty(key)) { + configs.push({key: key, config: pushConfigs[key]}); + } + } + + if (configs.length === 0) { + return; + } + + showProgress(); + + for (var i = 0; i < configs.length; i++) { + var config = configs[i]; + ajax('POST', '/config/' + config.key + '/set/', config.config, function () { + finishedCount++; + testReady(); + }); + } + + pushConfigs = {}; } function fetchCurrentConfig() { @@ -476,32 +592,22 @@ function fetchCameraConfig() { } function pushMainConfig() { - if (noPushLock) { - return; - } - - noPushLock++; - var mainConfig = mainUi2Dict(); - ajax('POST', '/config/main/set/', mainConfig, function () { - noPushLock--; - }); + pushConfigs['main'] = mainConfig; + if (!isApplyVisible()) { + showApply(); + } } function pushCameraConfig() { - if (noPushLock) { - return; - } - - noPushLock++; - var cameraConfig = cameraUi2Dict(); var cameraId = $('#videoDeviceSelect').val(); - - ajax('POST', '/config/' + cameraId + '/set/', cameraConfig, function () { - noPushLock--; - }); + + pushConfigs[cameraId] = cameraConfig; + if (!isApplyVisible()) { + showApply(); + } } $(document).ready(function () { @@ -516,6 +622,8 @@ $(document).ready(function () { $('div.settings').addClass('open'); $('div.page-container').addClass('stretched'); $('div.settings-top-bar').addClass('open'); + + updateConfigUi(); } }); @@ -531,6 +639,5 @@ $(document).ready(function () { }); initUI(); - updateConfigUI(); fetchCurrentConfig(); }); diff --git a/static/js/ui.js b/static/js/ui.js index f9b8747..463fbe5 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -223,6 +223,47 @@ function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decima return slider; } +function makeTextValidator($input, 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 false; + } + + return true; + } + + var msg = 'this field is required'; + + 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('text-validator'); + $input[0].validate = validate; +} + function makeNumberValidator($input, minVal, maxVal, floating, sign, required) { if (minVal == null) { minVal = -Infinity; @@ -241,6 +282,10 @@ function makeNumberValidator($input, minVal, maxVal, floating, sign, required) { } 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; } @@ -293,14 +338,17 @@ function makeNumberValidator($input, minVal, maxVal, floating, sign, required) { 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('number-validator'); @@ -315,18 +363,25 @@ function makeTimeValidator($input) { var msg = 'enter a valid time in the following format: HH:MM'; function validate() { + if (!$input.parents('tr:eq(0)').is(':visible')) { + return true; /* an invisible element is considered always valid */ + } + 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.timepicker({ closeOnWindowScroll: true, diff --git a/templates/base-site.html b/templates/base-site.html index 138c010..6ac6e03 100644 --- a/templates/base-site.html +++ b/templates/base-site.html @@ -17,8 +17,8 @@