]> www.vanbest.org Git - motioneye-debian/commitdiff
added an apply button
authorCalin Crisan <ccrisan@gmail.com>
Sat, 28 Sep 2013 14:47:23 +0000 (17:47 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Sat, 28 Sep 2013 14:47:23 +0000 (17:47 +0300)
doc/todo.txt
src/config.py
src/handlers.py
static/css/base-site.css
static/css/ui.css
static/img/progress.gif [new file with mode: 0644]
static/js/base-site.js
static/js/ui.js
templates/base-site.html

index 9fd65dc2fbaeb3a892f2360056953d211f7d843d..c5af23abee5d7218f9336f62d0d1cebc03a7608a 100644 (file)
@@ -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
index 9d67eab76d184a3c9a500ba78ab2a290cf4883bd..c5c192347623e3e2757ea1bf351377cabb50f83d 100644 (file)
@@ -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)
     
index b5097294de583f9ac8fd8068f4c05eb74285995c..2278593273f4eaf94cea90c26af94e7f4f586fdd 100644 (file)
@@ -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', '')
index ed8662c81c1ee12c2a75587b98daa804bc3f1ebf..92ffcb0ee9d4a78e22f1cb46ad4d6f2b509ace2c 100644 (file)
@@ -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 {
index 78bb79ff37607d5fc98819acf6b77b5a6909c8bc..97afe28827b75911605e75030e66901deb3a3dc3 100644 (file)
@@ -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 (file)
index 0000000..12ccd9b
Binary files /dev/null and b/static/img/progress.gif differ
index 58e55d06892bb3314295e9dc6eb206c578c7ceda..378a4cf9834facb2eec22fc5e2320dc3c2e89be5 100644 (file)
@@ -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('<img src="' + staticUrl + 'img/progress.gif">');
+    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();
 });
index f9b87475a96faf44c0e18e776b311fe41a042413..463fbe52921d756010efa5ac0c4fade7563ec186 100644 (file)
@@ -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,
index 138c010cb855e7914a2729baa3342d482d0798d4..6ac6e03dc755bde13f9b0215fed958c682974c3d 100644 (file)
@@ -17,8 +17,8 @@
         <div class="header-container">
             <div class="settings-top-bar">
                 <img class="settings-button" src="{{STATIC_URL}}img/settings.png" title="settings">
-                <select class="video-device styled" id="videoDeviceSelect">
-                </select>
+                <select class="video-device styled" id="videoDeviceSelect"></select>
+                <div class="button apply-button" id="applyButton">Apply</div>
             </div>
             <div class="logo">
                 <a href="/">
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Share Password</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled storage" id="networkPasswordEntry"></td>
+                        <td class="settings-item-value"><input type="password" class="styled storage" id="networkPasswordEntry"></td>
                         <td><span class="help-mark" title="the password required by the network share (leave empty if no password is required)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">