]> www.vanbest.org Git - motioneye-debian/commitdiff
generalized config UI and backend code
authorCalin Crisan <ccrisan@gmail.com>
Sun, 22 Feb 2015 17:51:06 +0000 (19:51 +0200)
committerCalin Crisan <ccrisan@gmail.com>
Sun, 22 Feb 2015 17:51:06 +0000 (19:51 +0200)
sendmail.py
src/config.py
src/handlers.py
src/ordereddict.py [new file with mode: 0644]
src/tzctl.py
src/utils.py
src/wifictl.py
static/js/main.js
static/js/ui.js
templates/main.html

index a720758f6de6509c0b4393d79d40a33994f65ae6..2539c06a8fd4f5ddeefefa5c7fe12dbb3c356b99 100755 (executable)
@@ -102,7 +102,7 @@ def make_message(subject, message, camera_id, moment, timespan, callback):
         }
         
         if settings.LOCAL_TIME_FILE:
-            format_dict['timezone'] = tzctl.get_time_zone()
+            format_dict['timezone'] = tzctl._get_time_zone()
         
         else:
             format_dict['timezone'] = 'local time'
index 44f7a71dd50c52e8d669cf72e40c405ef7b8a55d..f178857e92bf9cd5db57a67057e7655ce17c97e1 100644 (file)
@@ -21,17 +21,15 @@ import os.path
 import re
 import shlex
 
-from collections import OrderedDict
-
 import diskctl
 import motionctl
 import settings
 import smbctl
-import tzctl
 import update
 import utils
 import v4l2ctl
-import wifictl
+
+from utils import OrderedDict
 
 
 _CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf'
@@ -40,11 +38,26 @@ _MAIN_CONFIG_FILE_NAME = 'motion.conf'
 _main_config_cache = None
 _camera_config_cache = {}
 _camera_ids_cache = None
+_additional_section_funcs = []
+_additional_config_funcs = []
+_additional_structure_cache = {}
 
 # starting with r490 motion config directives have changed a bit 
 _LAST_OLD_CONFIG_VERSIONS = (490, '3.2.12')
 
 
+def additional_section(func):
+    _additional_section_funcs.append(func)
+
+
+def additional_config(func):
+    _additional_config_funcs.append(func)
+
+
+import wifictl  # @UnusedImport
+import tzctl  # @UnusedImport
+
+
 def get_main(as_lines=False):
     global _main_config_cache
     
@@ -91,12 +104,7 @@ def get_main(as_lines=False):
             list_names=['thread'],
             no_convert=['@admin_username', '@admin_password', '@normal_username', '@normal_password'])
     
-    if settings.WPA_SUPPLICANT_CONF:
-        _get_wifi_settings(main_config)
-        
-    if settings.LOCAL_TIME_FILE:
-        _get_localtime_settings(main_config)
-        
+    _get_additional_config(main_config, camera=False)
     _set_default_motion(main_config, old_motion=_is_old_motion())
     
     _main_config_cache = main_config
@@ -112,9 +120,8 @@ def set_main(main_config):
     _main_config_cache = main_config
     
     main_config = dict(main_config)
-    _set_wifi_settings(main_config)
-    _set_localtime_settings(main_config)
-    
+    _set_additional_config(main_config, camera=False)
+
     config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
     
     # read the actual configuration from file
@@ -480,38 +487,46 @@ def rem_camera(camera_id):
 
 
 def main_ui_to_dict(ui):
-    return {
+    data = {
         '@enabled': ui['enabled'],
         
         '@show_advanced': ui['show_advanced'],
         '@admin_username': ui['admin_username'],
         '@admin_password': ui['admin_password'],
         '@normal_username': ui['normal_username'],
-        '@normal_password': ui['normal_password'],
-        '@time_zone': ui['time_zone'],
-        
-        '@wifi_enabled': ui['wifi_enabled'],
-        '@wifi_name': ui['wifi_name'],
-        '@wifi_key': ui['wifi_key'],
+        '@normal_password': ui['normal_password']
     }
 
+    # additional configs
+    for name, value in ui.iteritems():
+        if not name.startswith('_'):
+            continue
+
+        data['@' + name] = value
+
+    return data
+
 
 def main_dict_to_ui(data):
-    return {
+    ui = {
         'enabled': data['@enabled'],
         
         'show_advanced': data['@show_advanced'],
         'admin_username': data['@admin_username'],
         'admin_password': data['@admin_password'],
         'normal_username': data['@normal_username'],
-        'normal_password': data['@normal_password'],
-        'time_zone': data['@time_zone'],
-    
-        'wifi_enabled': data['@wifi_enabled'],
-        'wifi_name': data['@wifi_name'],
-        'wifi_key': data['@wifi_key'],
+        'normal_password': data['@normal_password']
     }
 
+    # additional configs
+    for name, value in data.iteritems():
+        if not name.startswith('@_'):
+            continue
+        
+        ui[name[1:]] = value
+
+    return ui
+
 
 def camera_ui_to_dict(ui):
     data = {
@@ -767,6 +782,13 @@ def camera_ui_to_dict(ui):
     on_event_end = ['%(script)s stop %%t' % {'script': event_relay_path}]
     
     data['on_event_end'] = '; '.join(on_event_end)
+    
+    # additional configs
+    for name, value in ui.iteritems():
+        if not name.startswith('_'):
+            continue
+
+        data['@' + name] = value
 
     return data
 
@@ -1083,6 +1105,13 @@ def camera_dict_to_ui(data):
         ui['command_notifications_enabled'] = True
         ui['command_notifications_exec'] = '; '.join(command_notifications)
 
+    # additional configs
+    for name, value in data.iteritems():
+        if not name.startswith('@_'):
+            continue
+        
+        ui[name[1:]] = value
+
     return ui
 
 
@@ -1224,6 +1253,9 @@ def _dict_to_conf(lines, data, list_names=[]):
         conf_lines.append('') # add a blank line
     
     for (name, value) in remaining.iteritems():
+        if name.startswith('@_'):
+            continue # ignore additional configs
+        
         if name in list_names:
             for v in value:
                 line = name + ' ' + _python_to_value(v)
@@ -1276,12 +1308,7 @@ def _set_default_motion(data, old_motion):
     data.setdefault('@admin_password', '')
     data.setdefault('@normal_username', 'user')
     data.setdefault('@normal_password', '')
-    data.setdefault('@time_zone', 'UTC')
 
-    data.setdefault('@wifi_enabled', False)
-    data.setdefault('@wifi_name', '')
-    data.setdefault('@wifi_key', '')
-    
     if old_motion:
         data.setdefault('control_port', 7999)
     
@@ -1389,34 +1416,98 @@ def _set_default_motion_camera(camera_id, data, old_motion=False):
     data.setdefault('on_event_end', '')
 
 
-def _get_wifi_settings(data):
-    wifi_settings = wifictl.get_wifi_settings()
+def get_additional_structure(camera):
+    if _additional_structure_cache.get(camera) is None:
+        logging.debug('loading additional config structure for %s' % ('camera' if camera else 'main'))
 
-    data['@wifi_enabled'] = bool(wifi_settings['ssid'])
-    data['@wifi_name'] = wifi_settings['ssid']
-    data['@wifi_key'] = wifi_settings['psk']
+        # gather sections
+        sections = OrderedDict()
+        for func in _additional_section_funcs:
+            result = func()
+            if not result:
+                continue
+            
+            if result.get('reboot') and not settings.ENABLE_REBOOT:
+                continue
+            
+            if bool(result.get('camera')) != bool(camera):
+                continue
     
-
-def _set_wifi_settings(data):
-    wifi_enabled = data.pop('@wifi_enabled', False)
-    wifi_name = data.pop('@wifi_name', '')
-    wifi_key = data.pop('@wifi_key', '')
+            result['name'] = func.func_name
+            sections[func.func_name] = result
     
-    if settings.WPA_SUPPLICANT_CONF:
-        s = {
-            'ssid': wifi_enabled and wifi_name,
-            'psk': wifi_key
-        }
-        
-        wifictl.set_wifi_settings(s)
+        configs = OrderedDict()
+        for func in _additional_config_funcs:
+            result = func()
+            if not result:
+                continue
+            
+            if result.get('reboot') and not settings.ENABLE_REBOOT:
+                continue
+            
+            if bool(result.get('camera')) != bool(camera):
+                continue
+            
+            result['name'] = func.func_name
+            configs[func.func_name] = result
+    
+            section = sections.setdefault(result.get('section'), {})
+            section.setdefault('configs', []).append(result)
+
+        _additional_structure_cache[camera] = sections, configs
+
+    return _additional_structure_cache[camera]
 
 
-def _get_localtime_settings(data):
-    time_zone = tzctl.get_time_zone()
-    data['@time_zone'] = time_zone
+def _get_additional_config(data, camera):
+    (sections, configs) = get_additional_structure(camera)
+    get_funcs = set([c.get('get') for c in configs.itervalues() if c.get('get')])
+    get_func_values = dict((f, f()) for f in get_funcs)
 
+    for name, section in sections.iteritems():
+        if not section.get('get'):
+            continue
+
+        if section.get('get_set_dict'):
+            data['@_' + name] = get_func_values.get(section['get'], {}).get(name)
+            
+        else:
+            data['@_' + name] = get_func_values.get(section['get'])  
+
+    for name, config in configs.iteritems():
+        if not config.get('get'):
+            continue
+
+        if config.get('get_set_dict'):
+            data['@_' + name] = get_func_values.get(config['get'], {}).get(name)
+            
+        else:
+            data['@_' + name] = get_func_values.get(config['get']) 
+
+
+def _set_additional_config(data, camera):
+    (sections, configs) = get_additional_structure(camera)
+
+    set_func_values = {}
+    for name, section in sections.iteritems():
+        if not section.get('set'):
+            continue
+
+        if section.get('get_set_dict'):
+            set_func_values.setdefault(section['set'], {})[name] = data.get('@_' + name)
+
+        else:
+            set_func_values[section['set']] = data.get('@_' + name)
+
+    for name, config in configs.iteritems():
+        if not config.get('set'):
+            continue
+
+        if config.get('get_set_dict'):
+            set_func_values.setdefault(config['set'], {})[name] = data.get('@_' + name)
+            
+        else:
+            set_func_values[config['set']] = data.get('@_' + name)
 
-def _set_localtime_settings(data):
-    time_zone = data.pop('@time_zone')
-    if time_zone and settings.LOCAL_TIME_FILE:
-        tzctl.set_time_zone(time_zone)
+    for func, value in set_func_values.iteritems():
+        func(value)
index 9b75f992618a8d9793a457ab4047e8e0934efcd9..f39fcf090fa051fc964ee63a40b8bd53d8d9b556 100644 (file)
@@ -141,18 +141,17 @@ class MainHandler(BaseHandler):
     def get(self):
         import motioneye
         
-        timezones = []
-        if settings.LOCAL_TIME_FILE:
-            import pytz
-            timezones = pytz.common_timezones
+        # additional config
+        main_sections = config.get_additional_structure(camera=False)[0]
+        camera_sections = config.get_additional_structure(camera=True)[0]
 
         self.render('main.html',
                 frame=False,
                 version=motioneye.VERSION,
                 enable_update=bool(settings.REPO),
-                wpa_supplicant=settings.WPA_SUPPLICANT_CONF,
                 enable_reboot=settings.ENABLE_REBOOT,
-                timezones=timezones,
+                main_sections=main_sections,
+                camera_sections=camera_sections,
                 hostname=socket.gethostname(),
                 admin_username=config.get_main().get('@admin_username'))
 
@@ -293,12 +292,13 @@ class ConfigHandler(BaseHandler):
             main_config = config.main_ui_to_dict(ui_config)
             main_config.setdefault('thread', old_main_config.get('thread', [])) 
             admin_credentials = '%s:%s' % (main_config.get('@admin_username', ''), main_config.get('@admin_password', ''))
-            
-            wifi_changed = bool([k for k in ['@wifi_enabled', '@wifi_name', '@wifi_key'] if old_main_config.get(k) != main_config.get(k)])
-            
+
+            additional_configs = config.get_additional_structure(camera=False)[1]           
+            reboot_config_names = [('@_' + c['name']) for c in additional_configs.values() if c.get('reboot')]
+            reboot = bool([k for k in reboot_config_names if old_main_config.get(k) != main_config.get(k)])
+
             config.set_main(main_config)
             
-            reboot = False
             reload = False
             
             if admin_credentials != old_admin_credentials:
@@ -306,11 +306,11 @@ class ConfigHandler(BaseHandler):
                 
                 reload = True
             
-            if wifi_changed:
-                logging.debug('wifi settings changed, reboot needed')
+            if reboot:
+                logging.debug('system settings changed, reboot needed')
                 
                 reboot = True
-                
+
             return {'reload': reload, 'reboot': reboot}
         
         reload = False # indicates that browser should reload the page
@@ -322,8 +322,7 @@ class ConfigHandler(BaseHandler):
             if reboot[0]:
                 if settings.ENABLE_REBOOT:
                     def call_reboot():
-                        logging.info('rebooting')
-                        os.system('reboot')
+                        powerctl.reboot()
                     
                     ioloop = IOLoop.instance()
                     ioloop.add_timeout(datetime.timedelta(seconds=2), call_reboot)
@@ -625,7 +624,13 @@ class ConfigHandler(BaseHandler):
         event = self.get_argument('event')
         logging.debug('event %(event)s relayed for camera with id %(id)s' % {'event': event, 'id': camera_id})
         
-        camera_config = config.get_camera(camera_id)
+        try:
+            camera_config = config.get_camera(camera_id)
+        
+        except:
+            logging.warn('ignoring event for remote camera with id %s (probably removed)' % camera_id)
+            return self.finish_json()
+
         if not utils.local_camera(camera_config):
             logging.warn('ignoring event for remote camera with id %s' % camera_id)
             return self.finish_json()
diff --git a/src/ordereddict.py b/src/ordereddict.py
new file mode 100644 (file)
index 0000000..0874135
--- /dev/null
@@ -0,0 +1,258 @@
+# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
+# Passes Python2.7's test suite and incorporates all the latest updates.
+
+try:
+    from thread import get_ident as _get_ident
+except ImportError:
+    from dummy_thread import get_ident as _get_ident
+
+try:
+    from _abcoll import KeysView, ValuesView, ItemsView
+except ImportError:
+    pass
+
+
+class OrderedDict(dict):
+    'Dictionary that remembers insertion order'
+    # An inherited dict maps keys to values.
+    # The inherited dict provides __getitem__, __len__, __contains__, and get.
+    # The remaining methods are order-aware.
+    # Big-O running times for all methods are the same as for regular dictionaries.
+
+    # The internal self.__map dictionary maps keys to links in a doubly linked list.
+    # The circular doubly linked list starts and ends with a sentinel element.
+    # The sentinel element never gets deleted (this simplifies the algorithm).
+    # Each link is stored as a list of length three:  [PREV, NEXT, KEY].
+
+    def __init__(self, *args, **kwds):
+        '''Initialize an ordered dictionary.  Signature is the same as for
+        regular dictionaries, but keyword arguments are not recommended
+        because their insertion order is arbitrary.
+
+        '''
+        if len(args) > 1:
+            raise TypeError('expected at most 1 arguments, got %d' % len(args))
+        try:
+            self.__root
+        except AttributeError:
+            self.__root = root = []                     # sentinel node
+            root[:] = [root, root, None]
+            self.__map = {}
+        self.__update(*args, **kwds)
+
+    def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
+        'od.__setitem__(i, y) <==> od[i]=y'
+        # Setting a new item creates a new link which goes at the end of the linked
+        # list, and the inherited dictionary is updated with the new key/value pair.
+        if key not in self:
+            root = self.__root
+            last = root[0]
+            last[1] = root[0] = self.__map[key] = [last, root, key]
+        dict_setitem(self, key, value)
+
+    def __delitem__(self, key, dict_delitem=dict.__delitem__):
+        'od.__delitem__(y) <==> del od[y]'
+        # Deleting an existing item uses self.__map to find the link which is
+        # then removed by updating the links in the predecessor and successor nodes.
+        dict_delitem(self, key)
+        link_prev, link_next, key = self.__map.pop(key)
+        link_prev[1] = link_next
+        link_next[0] = link_prev
+
+    def __iter__(self):
+        'od.__iter__() <==> iter(od)'
+        root = self.__root
+        curr = root[1]
+        while curr is not root:
+            yield curr[2]
+            curr = curr[1]
+
+    def __reversed__(self):
+        'od.__reversed__() <==> reversed(od)'
+        root = self.__root
+        curr = root[0]
+        while curr is not root:
+            yield curr[2]
+            curr = curr[0]
+
+    def clear(self):
+        'od.clear() -> None.  Remove all items from od.'
+        try:
+            for node in self.__map.itervalues():
+                del node[:]
+            root = self.__root
+            root[:] = [root, root, None]
+            self.__map.clear()
+        except AttributeError:
+            pass
+        dict.clear(self)
+
+    def popitem(self, last=True):
+        '''od.popitem() -> (k, v), return and remove a (key, value) pair.
+        Pairs are returned in LIFO order if last is true or FIFO order if false.
+
+        '''
+        if not self:
+            raise KeyError('dictionary is empty')
+        root = self.__root
+        if last:
+            link = root[0]
+            link_prev = link[0]
+            link_prev[1] = root
+            root[0] = link_prev
+        else:
+            link = root[1]
+            link_next = link[1]
+            root[1] = link_next
+            link_next[0] = root
+        key = link[2]
+        del self.__map[key]
+        value = dict.pop(self, key)
+        return key, value
+
+    # -- the following methods do not depend on the internal structure --
+
+    def keys(self):
+        'od.keys() -> list of keys in od'
+        return list(self)
+
+    def values(self):
+        'od.values() -> list of values in od'
+        return [self[key] for key in self]
+
+    def items(self):
+        'od.items() -> list of (key, value) pairs in od'
+        return [(key, self[key]) for key in self]
+
+    def iterkeys(self):
+        'od.iterkeys() -> an iterator over the keys in od'
+        return iter(self)
+
+    def itervalues(self):
+        'od.itervalues -> an iterator over the values in od'
+        for k in self:
+            yield self[k]
+
+    def iteritems(self):
+        'od.iteritems -> an iterator over the (key, value) items in od'
+        for k in self:
+            yield (k, self[k])
+
+    def update(*args, **kwds): #@NoSelf
+        '''od.update(E, **F) -> None.  Update od from dict/iterable E and F.
+
+        If E is a dict instance, does:           for k in E: od[k] = E[k]
+        If E has a .keys() method, does:         for k in E.keys(): od[k] = E[k]
+        Or if E is an iterable of items, does:   for k, v in E: od[k] = v
+        In either case, this is followed by:     for k, v in F.items(): od[k] = v
+
+        '''
+        if len(args) > 2:
+            raise TypeError('update() takes at most 2 positional '
+                            'arguments (%d given)' % (len(args),))
+        elif not args:
+            raise TypeError('update() takes at least 1 argument (0 given)')
+        self = args[0]
+        # Make progressively weaker assumptions about "other"
+        other = ()
+        if len(args) == 2:
+            other = args[1]
+        if isinstance(other, dict):
+            for key in other:
+                self[key] = other[key]
+        elif hasattr(other, 'keys'):
+            for key in other.keys():
+                self[key] = other[key]
+        else:
+            for key, value in other:
+                self[key] = value
+        for key, value in kwds.items():
+            self[key] = value
+
+    __update = update  # let subclasses override update without breaking __init__
+
+    __marker = object()
+
+    def pop(self, key, default=__marker):
+        '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
+        If key is not found, d is returned if given, otherwise KeyError is raised.
+
+        '''
+        if key in self:
+            result = self[key]
+            del self[key]
+            return result
+        if default is self.__marker:
+            raise KeyError(key)
+        return default
+
+    def setdefault(self, key, default=None):
+        'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
+        if key in self:
+            return self[key]
+        self[key] = default
+        return default
+
+    def __repr__(self, _repr_running={}):
+        'od.__repr__() <==> repr(od)'
+        call_key = id(self), _get_ident()
+        if call_key in _repr_running:
+            return '...'
+        _repr_running[call_key] = 1
+        try:
+            if not self:
+                return '%s()' % (self.__class__.__name__,)
+            return '%s(%r)' % (self.__class__.__name__, self.items())
+        finally:
+            del _repr_running[call_key]
+
+    def __reduce__(self):
+        'Return state information for pickling'
+        items = [[k, self[k]] for k in self]
+        inst_dict = vars(self).copy()
+        for k in vars(OrderedDict()):
+            inst_dict.pop(k, None)
+        if inst_dict:
+            return (self.__class__, (items,), inst_dict)
+        return self.__class__, (items,)
+
+    def copy(self):
+        'od.copy() -> a shallow copy of od'
+        return self.__class__(self)
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
+        and values equal to v (which defaults to None).
+
+        '''
+        d = cls()
+        for key in iterable:
+            d[key] = value
+        return d
+
+    def __eq__(self, other):
+        '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive
+        while comparison to a regular mapping is order-insensitive.
+
+        '''
+        if isinstance(other, OrderedDict):
+            return len(self)==len(other) and self.items() == other.items()
+        return dict.__eq__(self, other)
+
+    def __ne__(self, other):
+        return not self == other
+
+    # -- the following methods are only used in Python 2.7 --
+
+    def viewkeys(self):
+        "od.viewkeys() -> a set-like object providing a view on od's keys"
+        return KeysView(self)
+
+    def viewvalues(self):
+        "od.viewvalues() -> an object providing a view on od's values"
+        return ValuesView(self)
+
+    def viewitems(self):
+        "od.viewitems() -> a set-like object providing a view on od's items"
+        return ItemsView(self)
index fbd04e070b07be965ee29e43b10587bf94274ef1..24e387d47aadc8a783a5ec1be0511cf7098dc4cb 100644 (file)
@@ -21,6 +21,11 @@ import os
 import settings
 import subprocess
 
+from config import additional_config
+
+
+LOCAL_TIME_FILE = settings.LOCAL_TIME_FILE  # @UndefinedVariable
+
 
 def _get_time_zone_symlink():
     file = settings.LOCAL_TIME_FILE
@@ -81,7 +86,7 @@ def _get_time_zone_md5():
     return time_zone
 
 
-def get_time_zone():
+def _get_time_zone():
     time_zone = _get_time_zone_symlink() or _get_time_zone_md5()
     if not time_zone:
         logging.error('could not find local time zone')
@@ -89,7 +94,7 @@ def get_time_zone():
     return time_zone
 
 
-def set_time_zone(time_zone):
+def _set_time_zone(time_zone):
     zoneinfo_file = '/usr/share/zoneinfo/' + time_zone
     if not os.path.exists(zoneinfo_file):
         logging.error('%s file does not exist' % zoneinfo_file)
@@ -113,3 +118,23 @@ def set_time_zone(time_zone):
         logging.error('failed to link "%s" to "%s": %s' % (settings.LOCAL_TIME_FILE, zoneinfo_file, e))
         
         return False
+
+
+@additional_config
+def timeZone():
+    if not LOCAL_TIME_FILE:
+        return
+
+    import pytz
+    timezones = pytz.common_timezones
+
+    return {
+        'label': 'Time Zone',
+        'description': 'selecting the right timezone assures a correct timestamp displayed on pictures and movies',
+        'type': 'choices',
+        'choices': [(t, t) for t in timezones],
+        'section': 'general',
+        'advanced': True,
+        'get': _get_time_zone,
+        'set': _set_time_zone
+    }
index eb120abbb3d15f263e3a2a7309221c6a7ed81b53..b79b906bb090b32fb651eb67a9ad681a5644ad18 100644 (file)
@@ -27,6 +27,13 @@ from tornado.httpclient import AsyncHTTPClient, HTTPRequest
 import settings
 
 
+try:
+    from collections import OrderedDict  # @UnusedImport
+
+except:
+    from ordereddict import OrderedDict  # @UnusedImport @Reimport
+
+
 def pretty_date_time(date_time, tzinfo=None, short=False):
     if date_time is None:
         return '('+  _('never') + ')'
index 8705a8eb0bf6503979b2a561c20abd41254c8726..dd7dd9f1ca0461b4f57b578eb6738f651cf0eda2 100644 (file)
@@ -19,22 +19,28 @@ import logging
 import re
 import settings
 
+from config import additional_config, additional_section
 
-def get_wifi_settings():
+
+WPA_SUPPLICANT_CONF = settings.WPA_SUPPLICANT_CONF  # @UndefinedVariable
+
+
+def _get_wifi_settings():
     # will return the first configured network
-    
-    logging.debug('reading wifi settings from %s' % settings.WPA_SUPPLICANT_CONF)
+
+    logging.debug('reading wifi settings from %s' % WPA_SUPPLICANT_CONF)
     
     try:
-        conf_file = open(settings.WPA_SUPPLICANT_CONF, 'r')
+        conf_file = open(WPA_SUPPLICANT_CONF, 'r')
     
     except Exception as e:
         logging.error('could open wifi settings file %(path)s: %(msg)s' % {
-                'path': settings.WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
         
         return {
-            'ssid': None,
-            'psk': None
+            'wifiEnabled': False,
+            'wifiNetworkName': '',
+            'wifiNetworkKey': ''
         }
     
     lines = conf_file.readlines()
@@ -70,26 +76,27 @@ def get_wifi_settings():
         logging.debug('wifi is disabled')
 
     return {
-        'ssid': ssid,
-        'psk': psk
+        'wifiEnabled': False,
+        'wifiNetworkName': ssid,
+        'wifiNetworkKey': psk
     }
 
 
-def set_wifi_settings(s):
+def _set_wifi_settings(s):
     # will update the first configured network
     
-    logging.debug('writing wifi settings to %s' % settings.WPA_SUPPLICANT_CONF)
+    logging.debug('writing wifi settings to %s' % WPA_SUPPLICANT_CONF)
     
-    enabled = bool(s['ssid'])
-    ssid = s['ssid']
-    psk = s['psk']
+    enabled = s['wifiEnabled']
+    ssid = s['wifiNetworkName']
+    psk = s['wifiNetworkKey']
     
     try:
-        conf_file = open(settings.WPA_SUPPLICANT_CONF, 'r')
+        conf_file = open(WPA_SUPPLICANT_CONF, 'r')
     
     except Exception as e:
         logging.error('could open wifi settings file %(path)s: %(msg)s' % {
-                'path': settings.WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
 
         return
     
@@ -150,11 +157,11 @@ def set_wifi_settings(s):
         lines.append('}\n\n')
 
     try:
-        conf_file = open(settings.WPA_SUPPLICANT_CONF, 'w')
+        conf_file = open(WPA_SUPPLICANT_CONF, 'w')
     
     except Exception as e:
         logging.error('could open wifi settings file %(path)s: %(msg)s' % {
-                'path': settings.WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
 
         return
     
@@ -162,3 +169,71 @@ def set_wifi_settings(s):
         conf_file.write(line)
 
     conf_file.close()
+
+
+@additional_section
+def network():
+    return {
+        'label': 'Network',
+        'description': 'configure the network connection',
+        'advanced': True
+    }
+
+
+
+@additional_config
+def wifiEnabled():
+    if not WPA_SUPPLICANT_CONF:
+        return
+
+    return {
+        'label': 'Wireless Network',
+        'description': 'enable this if you want to connect to a wireless network',
+        'type': 'bool',
+        'section': 'network',
+        'advanced': True,
+        'reboot': True,
+        'get': _get_wifi_settings,
+        'set': _set_wifi_settings,
+        'get_set_dict': True
+    }
+
+
+@additional_config
+def wifiNetworkName():
+    if not WPA_SUPPLICANT_CONF:
+        return
+
+    return {
+        'label': 'Wireless Network Name',
+        'description': 'the name (SSID) of your wireless network',
+        'type': 'str',
+        'section': 'network',
+        'advanced': True,
+        'required': True,
+        'reboot': True,
+        'depends': ['wifiEnabled'],
+        'get': _get_wifi_settings,
+        'set': _set_wifi_settings,
+        'get_set_dict': True
+    }
+
+
+@additional_config
+def wifiNetworkKey():
+    if not WPA_SUPPLICANT_CONF:
+        return
+
+    return {
+        'label': 'Wireless Network Key',
+        'description': 'the key (PSK) required to connect to your wireless network',
+        'type': 'pwd',
+        'section': 'network',
+        'advanced': True,
+        'required': True,
+        'reboot': True,
+        'depends': ['wifiEnabled'],
+        'get': _get_wifi_settings,
+        'set': _set_wifi_settings,
+        'get_set_dict': True
+    }
index 6d3cda11d4df34ad7bc85cb151517d5b12dd4c90..43c8979a9bdf0997db0ccc8581f914314c91bcfc 100644 (file)
@@ -303,6 +303,16 @@ Array.prototype.forEach = Array.prototype.forEach || function (callback, thisArg
     }
 };
 
+Array.prototype.every = Array.prototype.every || function (callback, thisArg) {
+    for (var i = 0; i < this.length; i++) {
+        if (!callback.call(thisArg, this[i], i, this)) {
+            return false;
+        }
+    }
+    
+    return true;
+};
+
 Array.prototype.unique = function (callback, thisArg) {
     var uniqueElements = [];
     this.forEach(function (element) {
@@ -351,6 +361,18 @@ Array.prototype.sortKey = function (keyFunc, reverse) {
     });
 };
 
+String.prototype.startsWith = String.prototype.startsWith || function (str) {
+    return (this.substr(0, str.length) === str);
+};
+
+String.prototype.endsWith = String.prototype.endsWith || function (str) {
+    return (this.substr(this.length - str.length) === str);
+};
+
+String.prototype.trim = String.prototype.trim || function () {
+    return this.replace(new RegExp('^\\s*'), '').replace(new RegExp('\\s*$'), '');
+};
+
 String.prototype.replaceAll = String.prototype.replaceAll || function (oldStr, newStr) {
     var p, s = this;
     while ((p = s.indexOf(oldStr)) >= 0) {
@@ -428,96 +450,48 @@ function doLogout() {
 
 function initUI() {
     /* checkboxes */
-    $('input[type=checkbox].styled').each(function () {
-        makeCheckBox($(this));
-    });
+    makeCheckBox($('input[type=checkbox].styled'));
 
     /* sliders */
-    makeSlider($('#brightnessSlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#contrastSlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#saturationSlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#hueSlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#framerateSlider'), 2, 30, 0, [
-        {value: 1, label: '1'},
-        {value: 5, label: '5'},
-        {value: 10, label: '10'},
-        {value: 15, label: '15'},
-        {value: 20, label: '20'},
-        {value: 25, label: '25'},
-        {value: 30, label: '30'}
-    ], null, 0);
-    makeSlider($('#streamingFramerateSlider'), 1, 30, 0, [
-        {value: 1, label: '1'},
-        {value: 5, label: '5'},
-        {value: 10, label: '10'},
-        {value: 15, label: '15'},
-        {value: 20, label: '20'},
-        {value: 25, label: '25'},
-        {value: 30, label: '30'}
-    ], null, 0);
-    makeSlider($('#streamingQualitySlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#streamingResolutionSlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#imageQualitySlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#movieQualitySlider'), 0, 100, 2, null, 5, 0, '%');
-    makeSlider($('#frameChangeThresholdSlider'), 0, 20, 0, null, 5, 1, '%');
-    
-    makeSlider($('#noiseLevelSlider'), 0, 25, 0, null, 6, 0, '%');
-    
+    $('input[type=text].range.styled').each(function () {
+        var $this = $(this);
+        var $tr = $this.parent().parent();
+        var ticks = null;
+        var ticksAttr = $tr.attr('ticks');
+        if (ticksAttr) {
+            ticks = ticksAttr.split('|').map(function (t) {
+                var parts = t.split(',');
+                if (parts.length < 2) {
+                    parts.push(parts[0]);
+                }
+                return {value: Number(parts[0]), label: parts[1]};
+            });
+        }
+        makeSlider($this, Number($tr.attr('min')), Number($tr.attr('max')),
+                Number($tr.attr('snap')), ticks, Number($tr.attr('ticksnum')), Number($tr.attr('decimals')), $tr.attr('unit'));
+    });
+
     /* progress bars */
-    makeProgressBar($('#diskUsageProgressBar'));
-    
+    makeProgressBar($('div.progress-bar'));
+
     /* text validators */
-    makeTextValidator($('#adminUsernameEntry'), true);
-    makeTextValidator($('#normalUsernameEntry'), true);
-    makeTextValidator($('#wifiNameEntry'), 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);
-    makeTextValidator($('#smtpServerEntry'), true);
-    makeTextValidator($('#smtpAccountEntry'), true);
-    makeTextValidator($('#smtpPasswordEntry'), true);
-    makeTextValidator($('#webHookUrlEntry'), true);
-    makeTextValidator($('#commandNotificationsEntry'), true);
-    
+    makeTextValidator($('tr[required=true] input[type=text]'), true);
+    makeTextValidator($('tr[required=true] input[type=password]'), true);
+
     /* number validators */
-    makeNumberValidator($('#streamingPortEntry'), 1024, 65535, false, false, true);
-    makeNumberValidator($('#snapshotIntervalEntry'), 1, 86400, false, false, true);
-    makeNumberValidator($('#picturesLifetimeEntry'), 1, 3650, false, false, true);
-    makeNumberValidator($('#moviesLifetimeEntry'), 1, 3650, false, false, true);
-    makeNumberValidator($('#eventGapEntry'), 1, 86400, false, false, true);
-    makeNumberValidator($('#preCaptureEntry'), 0, 100, false, false, true);
-    makeNumberValidator($('#postCaptureEntry'), 0, 100, false, false, true);
-    makeNumberValidator($('#minimumMotionFramesEntry'), 1, 1000, false, false, true);
-    makeNumberValidator($('#smtpPortEntry'), 1, 65535, false, false, true);
-    makeNumberValidator($('#emailPictureTimeSpanEntry'), 0, 60, false, false, true);
-    
+    $('input[type=text].number').each(function () {
+        var $this = $(this);
+        var $tr = $this.parent().parent();
+        makeTextValidator($this, Number($tr.attr('min')), Number($tr.attr('max')),
+                Boolean($tr.attr('floating')), Boolean($tr.attr('sign')), Boolean($tr.attr('required')));
+    });
+
     /* time validators */
-    makeTimeValidator($('#mondayFromEntry'));
-    makeTimeValidator($('#mondayToEntry'));
-    makeTimeValidator($('#tuesdayFromEntry'));
-    makeTimeValidator($('#tuesdayToEntry'));
-    makeTimeValidator($('#wednesdayFromEntry'));
-    makeTimeValidator($('#wednesdayToEntry'));
-    makeTimeValidator($('#thursdayFromEntry'));
-    makeTimeValidator($('#thursdayToEntry'));
-    makeTimeValidator($('#fridayFromEntry'));
-    makeTimeValidator($('#fridayToEntry'));
-    makeTimeValidator($('#saturdayFromEntry'));
-    makeTimeValidator($('#saturdayToEntry'));
-    makeTimeValidator($('#sundayFromEntry'));
-    makeTimeValidator($('#sundayToEntry'));
+    makeTimeValidator($('input[type=text].time'));
     
     /* custom validators */
     makeCustomValidator($('#rootDirectoryEntry'), function (value) {
-        if ($('#storageDeviceSelect').val() == 'custom-path' && $.trim(value) == '/') {
+        if ($('#storageDeviceSelect').val() == 'custom-path' && String(value).trim() == '/') {
             return 'files cannot be created directly on the root of your system';
         }
         
@@ -525,28 +499,12 @@ function initUI() {
     }, '');
     
     /* input value processors */
-    
-    makeStrippedInput($('#adminUsernameEntry'));
-    makeStrippedInput($('#adminPasswordEntry'));
-    makeStrippedInput($('#normalUsernameEntry'));
-    makeStrippedInput($('#normalPasswordEntry'));
-    makeStrippedInput($('#deviceNameEntry'));
-    makeStrippedInput($('#rootDirectoryEntry'));
-    makeStrippedInput($('#leftTextEntry'));
-    makeStrippedInput($('#rightTextEntry'));
-    makeStrippedInput($('#imageFileNameEntry'));
-    makeStrippedInput($('#movieFileNameEntry'));
-    makeStrippedInput($('#emailAddressesEntry'));
-    makeStrippedInput($('#smtpServerEntry'));
-    makeStrippedInput($('#smtpAccountEntry'));
-    makeStrippedInput($('#smtpPasswordEntry'));
-    makeStrippedInput($('#webHookUrlEntry'));
-    makeStrippedInput($('#commandNotificationsEntry'));
-    
+    makeStrippedInput($('tr[strip=true] input[type=text]'));
+    makeStrippedInput($('tr[strip=true] input[type=password]'));
+
     /* ui elements that enable/disable other ui elements */
     $('#motionEyeSwitch').change(updateConfigUi);
     $('#showAdvancedSwitch').change(updateConfigUi);
-    $('#wifiSwitch').change(updateConfigUi);
     $('#storageDeviceSelect').change(updateConfigUi);
     $('#resolutionSelect').change(updateConfigUi);
     $('#leftTextSelect').change(updateConfigUi);
@@ -574,13 +532,34 @@ function initUI() {
     $('#fridayEnabledSwitch').change(updateConfigUi);
     $('#saturdayEnabledSwitch').change(updateConfigUi);
     $('#sundayEnabledSwitch').change(updateConfigUi);
+
+    /* additional configs */
+    var seenDependNames = {};
+    $('tr[depends]').each(function () {
+        var $tr = $(this);
+        var depends = $tr.attr('depends').split(' ');
+        depends.forEach(function (depend) {
+            if (depend.charAt(0) == '!') {
+                depend = depend.substring(1);
+            }
+            
+            if (depend in seenDependNames) {
+                return;
+            }
+            
+            seenDependNames[depend] = true;
+
+            var control = $('#' + depend + 'Entry, #' + depend + 'Select, #' + depend + 'Slider, #' + depend + 'Switch');
+            control.change(updateConfigUi);
+        });
+    });
     
     $('#storageDeviceSelect').change(function () {
         $('#rootDirectoryEntry').val('/');
     });
     
     $('#rootDirectoryEntry').change(function () {
-        this.value = $.trim(this.value);
+        this.value = this.value.trim();
     });
     
     $('#rootDirectoryEntry').change(function () {
@@ -605,17 +584,8 @@ function initUI() {
             fetchCurrentCameraConfig(endProgress);
         }
     });
-    $('input.general, select.general').change(pushMainConfig);
-    $('input.wifi').change(pushMainConfig);
-    $('input.device, select.device, ' +
-      'input.storage, select.storage, ' +
-      'input.text-overlay, select.text-overlay, ' + 
-      'input.streaming, select.streaming, ' +
-      'input.still-images, select.still-images, ' +
-      'input.motion-detection, select.motion-detection, ' +
-      'input.motion-movies, select.motion-movies, ' +
-      'input.notifications, select.notifications, ' +
-      'input.working-schedule, select.working-schedule').change(pushCameraConfig);
+    $('input.main-config, select.main-config').change(pushMainConfig);
+    $('input.camera-config, select.camera-config').change(pushCameraConfig);
     
     /* preview controls */
     $('#brightnessSlider').change(function () {pushPreview('brightness');});
@@ -652,7 +622,7 @@ function initUI() {
     /* logout button */
     $('div.button.logout-button').click(doLogout);
     
-    /* read-only entries */
+    /* autoselect urls in read-only entries */
     $('#streamingSnapshotUrlEntry:text, #streamingMjpgUrlEntry:text, #streamingEmbedUrlEntry:text').click(function () {
         this.select();
     });
@@ -712,11 +682,6 @@ function updateConfigUi() {
         objs.not($('#motionEyeSwitch').parents('div').get(0)).each(markHide);
     }
     
-    /* wifi switch */
-    if (!$('#wifiSwitch').get(0).checked) {
-        $('#wifiSwitch').parent().next('table.settings').find('tr.settings-item').each(markHide);
-    }
-    
     if ($('#cameraSelect').find('option').length < 2) { /* no camera configured */
         $('#videoDeviceSwitch').parent().each(markHide);
         $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHide);
@@ -861,6 +826,48 @@ function updateConfigUi() {
         $('#workingScheduleSwitch').parent().next('table.settings').find('tr.settings-item').each(markHide);
     }
     
+    /* additional configs */
+    $('tr[depends]').each(function () {
+        var $tr = $(this);
+        var depends = $tr.attr('depends').split(' ');
+        var conditionOk = true;
+        depends.every(function (depend) {
+            var neg = false;
+            if (depend.charAt(0) == '!') {
+                neg = true;
+                depend = depend.substring(1);
+            }
+
+            var control = $('#' + depend + 'Entry, #' + depend + 'Select, #' + depend + 'Slider');
+            var val = false;
+            if (control.length) {
+                val = control.val();
+            }
+            else { /* maybe it's a checkbox */
+                control = $('#' + depend + 'Switch');
+                if (control.length) {
+                    val = control.get(0).checked;
+                }
+            }
+            
+            val = Boolean(val);
+            if (neg) {
+                val = !val;
+            }
+            
+            if (!val) {
+                conditionOk = false;
+                return false;
+            }
+            
+            return true;
+        });
+        
+        if (!conditionOk) {
+            $tr.each(markHide);
+        }
+    });
+
     var weekDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
     weekDays.forEach(function (weekDay) {
         var check = $('#' + weekDay + 'EnabledSwitch');
@@ -917,20 +924,53 @@ function configUiValid() {
 }
 
 function mainUi2Dict() {
-    return {
+    var dict = {
         '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(),
-        'time_zone': $('#timeZoneSelect').val(),
-        
-        'wifi_enabled': $('#wifiSwitch')[0].checked,
-        'wifi_name': $('#wifiNameEntry').val(),
-        'wifi_key': $('#wifiKeyEntry').val()
+        'normal_password': $('#normalPasswordEntry').val()
     };
+
+    /* additional sections */
+    $('input[type=checkbox].additional-section.main-config').each(function () {
+        dict['_' + this.id.substring(0, this.id.length - 6)] = this.checked;
+    });
+
+    /* additional configs */
+    $('tr.additional-config').each(function () {
+        var $this = $(this);
+        var control = $this.find('input, select');
+        
+        if (!control.hasClass('main-config')) {
+            return;
+        }
+        
+        var id = control.attr('id');
+        var name, value;
+        if (id.endsWith('Entry')) {
+            name = id.substring(0, id.length - 5);
+            value = control.val();
+        }
+        else if (id.endsWith('Select')) {
+            name = id.substring(0, id.length - 6);
+            value = control.val();
+        }
+        else if (id.endsWith('Slider')) {
+            name = id.substring(0, id.length - 6);
+            value = control.val();
+        }
+        else if (id.endsWith('Switch')) {
+            name = id.substring(0, id.length - 6);
+            value = control[0].checked;
+        }
+        
+        dict['_' + name] = value;
+    });
+
+    return dict;
 }
 
 function dict2MainUi(dict) {
@@ -941,12 +981,41 @@ function dict2MainUi(dict) {
     $('#adminPasswordEntry').val(dict['admin_password']);
     $('#normalUsernameEntry').val(dict['normal_username']);
     $('#normalPasswordEntry').val(dict['normal_password']);
-    $('#timeZoneSelect').val(dict['time_zone']);
-    
-    $('#wifiSwitch')[0].checked = dict['wifi_enabled'];
-    $('#wifiNameEntry').val(dict['wifi_name']);
-    $('#wifiKeyEntry').val(dict['wifi_key']);
-    
+
+    /* additional sections */
+    $('input[type=checkbox].additional-section.main-config').each(function () {
+        this.checked = dict['_' + this.id.substring(0, this.id.length - 6)];
+    });
+
+    /* additional configs */
+    $('tr.additional-config').each(function () {
+        var $this = $(this);
+        var control = $this.find('input, select');
+        
+        if (!control.hasClass('main-config')) {
+            return;
+        }
+
+        var id = control.attr('id');
+        var name;
+        if (id.endsWith('Entry')) {
+            name = id.substring(0, id.length - 5);
+            control.val(dict['_' + name]);
+        }
+        else if (id.endsWith('Select')) {
+            name = id.substring(0, id.length - 6);
+            control.val(dict['_' + name]);
+        }
+        else if (id.endsWith('Slider')) {
+            name = id.substring(0, id.length - 6);
+            control.val(dict['_' + name]);
+        }
+        else if (id.endsWith('Switch')) {
+            name = id.substring(0, id.length - 6);
+            control[0].checked = dict['_' + name];
+        }
+    });
+
     updateConfigUi();
 }
 
@@ -1075,6 +1144,42 @@ function cameraUi2Dict() {
         dict.hue = $('#hueSlider').val();
     }
     
+    /* additional sections */
+    $('input[type=checkbox].additional-section.camera-config').each(function () {
+        dict['_' + this.id.substring(0, this.id.length - 6)] = this.checked;
+    });
+
+    /* additional configs */
+    $('tr.additional-config').each(function () {
+        var $this = $(this);
+        var control = $this.find('input, select');
+        
+        if (!control.hasClass('camera-config')) {
+            return;
+        }
+        
+        var id = control.attr('id');
+        var name, value;
+        if (id.endsWith('Entry')) {
+            name = id.substring(0, id.length - 5);
+            value = control.val();
+        }
+        else if (id.endsWith('Select')) {
+            name = id.substring(0, id.length - 6);
+            value = control.val();
+        }
+        else if (id.endsWith('Slider')) {
+            name = id.substring(0, id.length - 6);
+            value = control.val();
+        }
+        else if (id.endsWith('Switch')) {
+            name = id.substring(0, id.length - 6);
+            value = control[0].checked;
+        }
+        
+        dict['_' + name] = value;
+    });
+
     return dict;
 }
 
@@ -1302,6 +1407,40 @@ function dict2CameraUi(dict) {
     $('#sundayToEntry').val(dict['sunday_to']);
     $('#workingScheduleTypeSelect').val(dict['working_schedule_type']);
     
+    /* additional sections */
+    $('input[type=checkbox].additional-section.main-config').each(function () {
+        this.checked = dict[this.id.substring(0, this.id.length - 6)];
+    });
+
+    /* additional configs */
+    $('tr.additional-config').each(function () {
+        var $this = $(this);
+        var control = $this.find('input, select');
+        
+        if (!control.hasClass('camera-config')) {
+            return;
+        }
+
+        var id = control.attr('id');
+        var name;
+        if (id.endsWith('Entry')) {
+            name = id.substring(0, id.length - 5);
+            control.val(dict['_' + name]);
+        }
+        else if (id.endsWith('Select')) {
+            name = id.substring(0, id.length - 6);
+            control.val(dict['_' + name]);
+        }
+        else if (id.endsWith('Slider')) {
+            name = id.substring(0, id.length - 6);
+            control.val(dict['_' + name]);
+        }
+        else if (id.endsWith('Switch')) {
+            name = id.substring(0, id.length - 6);
+            control[0].checked = dict['_' + name];
+        }
+    });
+
     updateConfigUi();
 }
 
index eec9f0ecd214c0852b7f61281f292055a8b7199a..94cf89685eb642eaf804042b513350fdb196eac8 100644 (file)
@@ -5,446 +5,433 @@ var _modalDialogContexts = [];
     /* UI widgets */
 
 function makeCheckBox($input) {
-    if (!$input.length) {
-        return;
-    }
-    
-    var mainDiv = $('<div class="check-box"></div>');
-    var buttonDiv = $('<div class="check-box-button"></div>');
-    var text = $('<span class="check-box-text"><span>');
-    
-    function setOn() {
-        text.html('ON');
-        mainDiv.addClass('on');
-    }
-    
-    function setOff() {
-        text.html('OFF');
-        mainDiv.removeClass('on');
-    }
-    
-    buttonDiv.append(text);
-    mainDiv.append(buttonDiv);
-    
-    /* transfer the CSS classes */
-    mainDiv[0].className += ' ' + $input[0].className;
-    
-    /* add the element */
-    $input.after(mainDiv);
-    
-    function update() {
-        if ($input[0].checked) {
-            setOn();
+    $input.each(function () {
+        var $this = $(this);
+
+        var mainDiv = $('<div class="check-box"></div>');
+        var buttonDiv = $('<div class="check-box-button"></div>');
+        var text = $('<span class="check-box-text"><span>');
+        
+        function setOn() {
+            text.html('ON');
+            mainDiv.addClass('on');
         }
-        else {
-            setOff();
+        
+        function setOff() {
+            text.html('OFF');
+            mainDiv.removeClass('on');
         }
-    }
-    
-    /* add event handers */
-    $input.change(update).change();
-    
-    mainDiv.click(function () {
-        $input[0].checked = !$input[0].checked;
-        $input.change();
-    });
-    
-    /* make the element focusable */
-    mainDiv[0].tabIndex = 0;
-    
-    /* handle the key events */
-    mainDiv.keydown(function (e) {
-        if (e.which === 13 || e.which === 32) {
-            $input[0].checked = !$input[0].checked;
-            $input.change();
-            
-            return false;
+        
+        buttonDiv.append(text);
+        mainDiv.append(buttonDiv);
+        
+        /* transfer the CSS classes */
+        mainDiv[0].className += ' ' + $this[0].className;
+        
+        /* add the element */
+        $this.after(mainDiv);
+        
+        function update() {
+            if ($this[0].checked) {
+                setOn();
+            }
+            else {
+                setOff();
+            }
         }
+        
+        /* add event handers */
+        $this.change(update).change();
+        
+        mainDiv.click(function () {
+            $this[0].checked = !$this[0].checked;
+            $this.change();
+        });
+        
+        /* make the element focusable */
+        mainDiv[0].tabIndex = 0;
+        
+        /* handle the key events */
+        mainDiv.keydown(function (e) {
+            if (e.which === 13 || e.which === 32) {
+                $this[0].checked = !$this[0].checked;
+                $this.change();
+                
+                return false;
+            }
+        });
+        
+        this.update = update;
     });
-    
-    $input[0].update = update;
-    
-    return mainDiv;
 }
 
 function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decimals, unit) {
-    if (!$input.length) {
-        return;
-    }
-
     unit = unit || '';
-    
-    var slider = $('<div class="slider"></div>');
-    
-    var labels = $('<div class="slider-labels"></div>');
-    slider.append(labels);
-    
-    var bar = $('<div class="slider-bar"></div>');
-    slider.append(bar);
-    
-    bar.append('<div class="slider-bar-inside"></div>');
-    
-    var cursor = $('<div class="slider-cursor"></div>');
-    bar.append(cursor);
-    
-    var cursorLabel = $('<div class="slider-cursor-label"></div>');
-    cursor.append(cursorLabel);
-    
-    function bestPos(pos) {
-        if (pos < 0) {
-            pos = 0;
-        }
-        if (pos > 100) {
-            pos = 100;
-        }
+
+    $input.each(function () {
+        var $this = $(this);
+        var slider = $('<div class="slider"></div>');
         
-        if (snapMode > 0) {
-            var minDif = Infinity;
-            var bestPos = null;
-            for (var i = 0; i < ticks.length; i++) {
-                var tick = ticks[i];
-                var p = valToPos(tick.value);
-                var dif = Math.abs(p - pos);
-                if ((dif < minDif) && (snapMode == 1 || dif < 5)) {
-                    minDif = dif;
-                    bestPos = p;
-                }
+        var labels = $('<div class="slider-labels"></div>');
+        slider.append(labels);
+        
+        var bar = $('<div class="slider-bar"></div>');
+        slider.append(bar);
+        
+        bar.append('<div class="slider-bar-inside"></div>');
+        
+        var cursor = $('<div class="slider-cursor"></div>');
+        bar.append(cursor);
+        
+        var cursorLabel = $('<div class="slider-cursor-label"></div>');
+        cursor.append(cursorLabel);
+        
+        function bestPos(pos) {
+            if (pos < 0) {
+                pos = 0;
+            }
+            if (pos > 100) {
+                pos = 100;
             }
             
-            if (bestPos != null) {
-                pos = bestPos;
+            if (snapMode > 0) {
+                var minDif = Infinity;
+                var bestPos = null;
+                for (var i = 0; i < ticks.length; i++) {
+                    var tick = ticks[i];
+                    var p = valToPos(tick.value);
+                    var dif = Math.abs(p - pos);
+                    if ((dif < minDif) && (snapMode == 1 || dif < 5)) {
+                        minDif = dif;
+                        bestPos = p;
+                    }
+                }
+                
+                if (bestPos != null) {
+                    pos = bestPos;
+                }
             }
-        }
-        
-        return pos;
-    }
-    
-    function getPos() {
-        return parseInt(cursor.position().left * 100 / bar.width());
-    }
-    
-    function valToPos(val) {
-        return (val - minVal) * 100 / (maxVal - minVal);
-    }
-    
-    function posToVal(pos) {
-        return minVal + pos * (maxVal - minVal) / 100;
-    }
-    
-    function sliderChange(val) {
-        $input.val(val.toFixed(decimals));
-        cursorLabel.html('' + val.toFixed(decimals) + unit);
-    }
-    
-    function bodyMouseMove(e) {
-        if (bar[0]._mouseDown) {
-            var offset = bar.offset();
-            var pos = e.pageX - offset.left - 5;
-            pos = pos / slider.width() * 100;
-            pos = bestPos(pos);
-            var val = posToVal(pos);
             
-            cursor.css('left', pos + '%');
-            sliderChange(val);
+            return pos;
         }
-    }
-    
-    function bodyMouseUp(e) {
-        bar[0]._mouseDown = false;
-
-        $('body').unbind('mousemove', bodyMouseMove);
-        $('body').unbind('mouseup', bodyMouseUp);
-        
-        cursorLabel.css('display', 'none');
         
-        $input.change();
-    }
-    
-    bar.mousedown(function (e) {
-        if (e.which > 1) {
-            return;
+        function getPos() {
+            return parseInt(cursor.position().left * 100 / bar.width());
         }
         
-        this._mouseDown = true;
-        bodyMouseMove(e);
-
-        $('body').mousemove(bodyMouseMove);
-        $('body').mouseup(bodyMouseUp);
+        function valToPos(val) {
+            return (val - minVal) * 100 / (maxVal - minVal);
+        }
         
-        slider.focus();
-        cursorLabel.css('display', 'inline-block');
+        function posToVal(pos) {
+            return minVal + pos * (maxVal - minVal) / 100;
+        }
         
-        return false;
-    });
-    
-    /* ticks */
-    var autoTicks = (ticks == null);
-    
-    function makeTicks() {
-        if (ticksNumber == null) {
-            ticksNumber = 11; 
+        function sliderChange(val) {
+            $this.val(val.toFixed(decimals));
+            cursorLabel.html('' + val.toFixed(decimals) + unit);
         }
-
-        labels.html('');
-        
-        if (autoTicks) {
-            ticks = [];
-            var i;
-            for (i = 0; i < ticksNumber; i++) {
-                var val = minVal + i * (maxVal - minVal) / (ticksNumber - 1);
-                var valStr;
-                if (Math.round(val) == val) {
-                    valStr = '' + val;
-                }
-                else {
-                    valStr = val.toFixed(decimals);
-                }
-                ticks.push({value: val, label: valStr + unit});
+        
+        function bodyMouseMove(e) {
+            if (bar[0]._mouseDown) {
+                var offset = bar.offset();
+                var pos = e.pageX - offset.left - 5;
+                pos = pos / slider.width() * 100;
+                pos = bestPos(pos);
+                var val = posToVal(pos);
+                
+                cursor.css('left', pos + '%');
+                sliderChange(val);
             }
         }
         
-        for (i = 0; i < ticks.length; i++) {
-            var tick = ticks[i];
-            var pos = valToPos(tick.value);
-            var span = $('<span class="slider-label" style="left: -9999px;">' + tick.label + '</span>');
+        function bodyMouseUp(e) {
+            bar[0]._mouseDown = false;
+    
+            $('body').unbind('mousemove', bodyMouseMove);
+            $('body').unbind('mouseup', bodyMouseUp);
+            
+            cursorLabel.css('display', 'none');
             
-            labels.append(span);
-            span.css('left', (pos - 10) + '%');
+            $this.change();
         }
         
-        return ticks;
-    }
+        bar.mousedown(function (e) {
+            if (e.which > 1) {
+                return;
+            }
+            
+            this._mouseDown = true;
+            bodyMouseMove(e);
     
-    makeTicks();
-
-    function input2slider() {
-        var value = parseFloat($input.val());
-        if (isNaN(value)) {
-            value = minVal;
-        }
+            $('body').mousemove(bodyMouseMove);
+            $('body').mouseup(bodyMouseUp);
+            
+            slider.focus();
+            cursorLabel.css('display', 'inline-block');
+            
+            return false;
+        });
         
-        var pos = valToPos(value);
-        pos = bestPos(pos);
-        cursor.css('left', pos + '%');
-        cursorLabel.html($input.val() + unit);
-    }
-    
-    /* transfer the CSS classes */
-    slider.addClass($input.attr('class'));
-    
-    /* handle input events */
-    $input.change(input2slider).change();
-    
-    /* add the slider to the parent of the input */
-    $input.after(slider);
-    
-    /* make the slider focusable */
-    slider.attr('tabIndex', 0);
+        /* ticks */
+        var autoTicks = (ticks == null);
+        
+        function makeTicks() {
+            if (ticksNumber == null) {
+                ticksNumber = 11; 
+            }
     
-    /* handle key events */
-    slider.keydown(function (e) {
-        switch (e.which) {
-            case 37: /* left */
-                if (snapMode == 1) { /* strict snapping */
-                    // TODO implement me
-                }
-                else {
-                    var step = (maxVal - minVal) / 200;
-                    var val = Math.max(minVal, parseFloat($input.val()) - step);
-                    if (decimals == 0) {
-                        val = Math.floor(val);
+            labels.html('');
+            
+            if (autoTicks) {
+                ticks = [];
+                var i;
+                for (i = 0; i < ticksNumber; i++) {
+                    var val = minVal + i * (maxVal - minVal) / (ticksNumber - 1);
+                    var valStr;
+                    if (Math.round(val) == val) {
+                        valStr = '' + val;
                     }
-                    
-                    var origSnapMode = snapMode;
-                    snapMode = 0;
-                    $input.val(val).change();
-                    snapMode = origSnapMode;
-                }
-                
-                break;
-                
-            case 39: /* right */
-                if (snapMode == 1) { /* strict snapping */
-                    // TODO implement me
-                }
-                else {
-                    var step = (maxVal - minVal) / 200;
-                    var val = Math.min(maxVal, parseFloat($input.val()) + step);
-                    if (decimals == 0) {
-                        val = Math.ceil(val);
+                    else {
+                        valStr = val.toFixed(decimals);
                     }
-
-                    var origSnapMode = snapMode;
-                    snapMode = 0;
-                    $input.val(val).change();
-                    snapMode = origSnapMode;
+                    ticks.push({value: val, label: valStr + unit});
                 }
+            }
+            
+            for (i = 0; i < ticks.length; i++) {
+                var tick = ticks[i];
+                var pos = valToPos(tick.value);
+                var span = $('<span class="slider-label" style="left: -9999px;">' + tick.label + '</span>');
                 
-                break;
+                labels.append(span);
+                span.css('left', (pos - 10) + '%');
+            }
+            
+            return ticks;
         }
-    });
+        
+        makeTicks();
     
-    $input.each(function () {
+        function input2slider() {
+            var value = parseFloat($this.val());
+            if (isNaN(value)) {
+                value = minVal;
+            }
+            
+            var pos = valToPos(value);
+            pos = bestPos(pos);
+            cursor.css('left', pos + '%');
+            cursorLabel.html($this.val() + unit);
+        }
+        
+        /* transfer the CSS classes */
+        slider.addClass($this.attr('class'));
+        
+        /* handle input events */
+        $this.change(input2slider).change();
+        
+        /* add the slider to the parent of the input */
+        $this.after(slider);
+        
+        /* make the slider focusable */
+        slider.attr('tabIndex', 0);
+        
+        /* handle key events */
+        slider.keydown(function (e) {
+            switch (e.which) {
+                case 37: /* left */
+                    if (snapMode == 1) { /* strict snapping */
+                        // TODO implement me
+                    }
+                    else {
+                        var step = (maxVal - minVal) / 200;
+                        var val = Math.max(minVal, parseFloat($this.val()) - step);
+                        if (decimals == 0) {
+                            val = Math.floor(val);
+                        }
+                        
+                        var origSnapMode = snapMode;
+                        snapMode = 0;
+                        $this.val(val).change();
+                        snapMode = origSnapMode;
+                    }
+                    
+                    break;
+                    
+                case 39: /* right */
+                    if (snapMode == 1) { /* strict snapping */
+                        // TODO implement me
+                    }
+                    else {
+                        var step = (maxVal - minVal) / 200;
+                        var val = Math.min(maxVal, parseFloat($this.val()) + step);
+                        if (decimals == 0) {
+                            val = Math.ceil(val);
+                        }
+    
+                        var origSnapMode = snapMode;
+                        snapMode = 0;
+                        $this.val(val).change();
+                        snapMode = origSnapMode;
+                    }
+                    
+                    break;
+            }
+        });
+        
         this.update = input2slider;
-    });
-    
-    slider[0].setMinVal = function (mv) {
-        minVal = mv;
-
-        makeTicks();
-    };
+        
+        slider[0].setMinVal = function (mv) {
+            minVal = mv;
     
-    slider[0].setMaxVal = function (mv) {
-        maxVal = mv;
-
-        makeTicks();
+            makeTicks();
+        };
         
-        input2slider();
-    };
+        slider[0].setMaxVal = function (mv) {
+            maxVal = mv;
     
-    return slider;
+            makeTicks();
+            
+            input2slider();
+        };
+    });
 }
 
 function makeProgressBar($div) {
-    if (!$div.length) {
-        return;
-    }
-    
-    $div.addClass('progress-bar-container');
-    var fillDiv = $('<div class="progress-bar-fill"></div>');
-    var textSpan = $('<span class="progress-bar-text"></span>');
-
-    $div.append(fillDiv);
-    $div.append(textSpan);
-    
-    $div[0].setProgress = function (progress) {
-        $div.progress = progress;
-        fillDiv.width(progress + '%');
-    };
+    $div.each(function () {
+        var $this = $(this);
+        
+        $this.addClass('progress-bar-container');
+        var fillDiv = $('<div class="progress-bar-fill"></div>');
+        var textSpan = $('<span class="progress-bar-text"></span>');
     
-    $div[0].setText = function (text) {
-        textSpan.html(text);
-    };
-
-    return $div;
+        $this.append(fillDiv);
+        $this.append(textSpan);
+        
+        this.setProgress = function (progress) {
+            $this.progress = progress;
+            fillDiv.width(progress + '%');
+        };
+        
+        this.setText = function (text) {
+            textSpan.html(text);
+        };
+    });
 }
 
 
     /* validators */
 
 function makeTextValidator($input, required) {
-    if (!$input.length) {
-        return;
-    }
-    
     if (required == null) {
         required = true;
     }
-    
-    function isValid(strVal) {
-        if (!$input.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';
+    $input.each(function () {
+        var $this = $(this);
+
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+            
+            if (strVal.length === 0 && required) {
+                return false;
+            }
     
-    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;
+            return true;
         }
-    }
-    
-    $input.keyup(validate);
-    $input.blur(validate);
-    $input.change(validate).change();
-    
-    $input.addClass('validator');
-    $input.addClass('text-validator');
-    $input.each(function () {
-        var oldValidate = this.validate;
-        this.validate = function () {
-            if (oldValidate) {
-                oldValidate.call(this);
+        
+        var msg = 'this field is required';
+        
+        function validate() {
+            var strVal = $this.val();
+            if (isValid(strVal)) {
+                $this.attr('title', '');
+                $this.removeClass('error');
+                $this[0].invalid = false;
+            }
+            else {
+                $this.attr('title', msg);
+                $this.addClass('error');
+                $this[0].invalid = true;
             }
-            validate();
         }
+        
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        
+        $this.addClass('validator');
+        $this.addClass('text-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
+            }
+        });
     });
 }
 
 function makeComboValidator($select, required) {
-    if (!$select.length) {
-        return;
-    }
-    
     if (required == null) {
         required = true;
     }
-    
-    function isValid(strVal) {
-        if (!$select.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';
+    $select.each(function () {
+        $this = $(this);
+
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+            
+            if (strVal.length === 0 && required) {
+                return false;
+            }
     
-    function validate() {
-        var strVal = $select.val() || '';
-        if (isValid(strVal)) {
-            $select.attr('title', '');
-            $select.removeClass('error');
-            $select[0].invalid = false;
-        }
-        else {
-            $select.attr('title', msg);
-            $select.addClass('error');
-            $select[0].invalid = true;
+            return true;
         }
-    }
-    
-    $select.keyup(validate);
-    $select.blur(validate);
-    $select.change(validate).change();
-    
-    $select.addClass('validator');
-    $select.addClass('combo-validator');
-    $select.each(function () {
-        var oldValidate = this.validate;
-        this.validate = function () {
-            if (oldValidate) {
-                oldValidate.call(this);
+        
+        var msg = 'this field is required';
+        
+        function validate() {
+            var strVal = $this.val() || '';
+            if (isValid(strVal)) {
+                $this.attr('title', '');
+                $this.removeClass('error');
+                $this[0].invalid = false;
+            }
+            else {
+                $this.attr('title', msg);
+                $this.addClass('error');
+                $this[0].invalid = true;
             }
-            validate();
         }
+        
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        
+        $this.addClass('validator');
+        $this.addClass('combo-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
+            }
+        });
     });
 }
 
 function makeNumberValidator($input, minVal, maxVal, floating, sign, required) {
-    if (!$input.length) {
-        return;
-    }
-    
     if (minVal == null) {
         minVal = -Infinity;
     }
@@ -460,233 +447,237 @@ function makeNumberValidator($input, minVal, maxVal, floating, sign, required) {
     if (required == null) {
         required = true;
     }
-    
-    function isValid(strVal) {
-        if (!$input.is(':visible')) {
-            return true; /* an invisible element is considered always valid */
-        }
 
-        if (strVal.length === 0 && !required) {
+    $input.each(function () {
+        var $this = $(this);
+        
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+    
+            if (strVal.length === 0 && !required) {
+                return true;
+            }
+            
+            var numVal = parseInt(strVal);
+            if ('' + numVal != strVal) {
+                return false;
+            }
+            
+            if (numVal < minVal || numVal > maxVal) {
+                return false;
+            }
+            
+            if (!sign && numVal < 0) {
+                return false;
+            }
+            
             return true;
         }
         
-        var numVal = parseInt(strVal);
-        if ('' + numVal != strVal) {
-            return false;
+        var msg = '';
+        if (!sign) {
+            msg = 'enter a positive';
         }
-        
-        if (numVal < minVal || numVal > maxVal) {
-            return false;
+        else {
+            msg = 'enter a';
         }
-        
-        if (!sign && numVal < 0) {
-            return false;
-        }
-        
-        return true;
-    }
-    
-    var msg = '';
-    if (!sign) {
-        msg = 'enter a positive';
-    }
-    else {
-        msg = 'enter a';
-    }
-    if (floating) {
-        msg += ' number';
-    }
-    else {
-        msg += ' integer number';
-    }
-    if (isFinite(minVal)) {
-        if (isFinite(maxVal)) {
-            msg += ' between ' + minVal + ' and ' + maxVal;
+        if (floating) {
+            msg += ' number';
         }
         else {
-            msg += ' greater than ' + minVal;
-        }
-    }
-    else {
-        if (isFinite(maxVal)) {
-            msg += ' smaller than ' + maxVal;
+            msg += ' integer number';
         }
-    }
-    
-    function validate() {
-        var strVal = $input.val();
-        if (isValid(strVal)) {
-            $input.attr('title', '');
-            $input.removeClass('error');
-            $input[0].invalid = false;
+        if (isFinite(minVal)) {
+            if (isFinite(maxVal)) {
+                msg += ' between ' + minVal + ' and ' + maxVal;
+            }
+            else {
+                msg += ' greater than ' + minVal;
+            }
         }
         else {
-            $input.attr('title', msg);
-            $input.addClass('error');
-            $input[0].invalid = true;
+            if (isFinite(maxVal)) {
+                msg += ' smaller than ' + maxVal;
+            }
         }
-    }
-    
-    $input.keyup(validate);
-    $input.blur(validate);
-    $input.change(validate).change();
-    
-    $input.addClass('validator');
-    $input.addClass('number-validator');
-    $input.each(function () {
-        var oldValidate = this.validate;
-        this.validate = function () {
-            if (oldValidate) {
-                oldValidate.call(this);
+        
+        function validate() {
+            var strVal = $this.val();
+            if (isValid(strVal)) {
+                $this.attr('title', '');
+                $this.removeClass('error');
+                $this[0].invalid = false;
+            }
+            else {
+                $this.attr('title', msg);
+                $this.addClass('error');
+                $this[0].invalid = true;
             }
-            validate();
         }
+        
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        
+        $this.addClass('validator');
+        $this.addClass('number-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
+            }
+        });
     });
     
     makeStrippedInput($input);
 }
 
 function makeTimeValidator($input) {
-    if (!$input.length) {
-        return;
-    }
-    
-    function isValid(strVal) {
-        if (!$input.is(':visible')) {
-            return true; /* an invisible element is considered always valid */
-        }
+    $input.each(function () {
+        var $this = $(this);
 
-        return strVal.match(new RegExp('^[0-2][0-9]:[0-5][0-9]$')) != null;
-    }
-    
-    var msg = 'enter a valid time in the following format: HH:MM';
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
     
-    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;
+            return strVal.match(new RegExp('^[0-2][0-9]:[0-5][0-9]$')) != null;
         }
-    }
-    
-    $input.keyup(validate);
-    $input.blur(validate);
-    $input.change(validate).change();
-    $input.timepicker({
-        closeOnWindowScroll: true,
-        selectOnBlur: true,
-        timeFormat: 'H:i',
-    });
-    
-    $input.addClass('validator');
-    $input.addClass('time-validator');
-    $input.each(function () {
-        var oldValidate = this.validate;
-        this.validate = function () {
-            if (oldValidate) {
-                oldValidate.call(this);
+        
+        var msg = 'enter a valid time in the following format: HH:MM';
+        
+        function validate() {
+            var strVal = $this.val();
+            if (isValid(strVal)) {
+                $this.attr('title', '');
+                $this.removeClass('error');
+                $this[0].invalid = false;
+            }
+            else {
+                $this.attr('title', msg);
+                $this.addClass('error');
+                $this[0].invalid = true;
             }
-            validate();
         }
+        
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        $this.timepicker({
+            closeOnWindowScroll: true,
+            selectOnBlur: true,
+            timeFormat: 'H:i',
+        });
+        
+        $this.addClass('validator');
+        $this.addClass('time-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
+            }
+        });
     });
-    
+
     makeStrippedInput($input);
 }
 
 function makeUrlValidator($input) {
-    if (!$input.length) {
-        return;
-    }
-    
-    function isValid(strVal) {
-        if (!$input.is(':visible')) {
-            return true; /* an invisible element is considered always valid */
-        }
+    $input.each(function () {
+        var $this = $(this);
 
-        return strVal.match(new RegExp('^([a-zA-Z]+)://([\\w\-.]+)(:\\d+)?(/.*)?$')) != null;
-    }
-    
-    var msg = 'enter a valid URL (e.g. http://example.com:8080/cams/)';
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
     
-    function validate() {
-        var strVal = $input.val();
-        if (isValid(strVal)) {
-            $input.attr('title', '');
-            $input.removeClass('error');
-            $input[0].invalid = false;
+            return strVal.match(new RegExp('^([a-zA-Z]+)://([\\w\-.]+)(:\\d+)?(/.*)?$')) != null;
         }
-        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('url-validator');
-    $input.each(function () {
-        var oldValidate = this.validate;
-        this.validate = function () {
-            if (oldValidate) {
-                oldValidate.call(this);
+        
+        var msg = 'enter a valid URL (e.g. http://example.com:8080/cams/)';
+        
+        function validate() {
+            var strVal = $this.val();
+            if (isValid(strVal)) {
+                $this.attr('title', '');
+                $this.removeClass('error');
+                $this[0].invalid = false;
+            }
+            else {
+                $this.attr('title', msg);
+                $this.addClass('error');
+                $this[0].invalid = true;
             }
-            validate();
         }
+        
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        
+        $this.addClass('validator');
+        $this.addClass('url-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
+            }
+        });
     });
 }
 
 function makeCustomValidator($input, isValidFunc) {
-    if (!$input.length) {
-        return;
-    }
-    
-    function isValid(strVal) {
-        if (!$input.is(':visible')) {
-            return true; /* an invisible element is considered always valid */
-        }
+    $input.each(function () {
+        var $this = $(this);
         
-        return isValidFunc(strVal);
-    }
-    
-    function validate() {
-        var strVal = $input.val();
-        var valid = isValid(strVal);
-        if (valid == true) {
-            $input.attr('title', '');
-            $input.removeClass('error');
-            $input[0].invalid = false;
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+            
+            return isValidFunc(strVal);
         }
-        else {
-            $input.attr('title', valid || 'enter a valid value');
-            $input.addClass('error');
-            $input[0].invalid = true;
+        
+        function validate() {
+            var strVal = $this.val();
+            var valid = isValid(strVal);
+            if (valid == true) {
+                $this.attr('title', '');
+                $this.removeClass('error');
+                $this[0].invalid = false;
+            }
+            else {
+                $this.attr('title', valid || 'enter a valid value');
+                $this.addClass('error');
+                $this[0].invalid = true;
+            }
         }
-    }
-
-    $input.keyup(validate);
-    $input.blur(validate);
-    $input.change(validate).change();
     
-    $input.addClass('validator');
-    $input.addClass('custom-validator');
-    $input.each(function () {
-        var oldValidate = this.validate;
-        this.validate = function () {
-            if (oldValidate) {
-                oldValidate.call(this);
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        
+        $this.addClass('validator');
+        $this.addClass('custom-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
             }
-            validate();
-        }
+        });
     });
 }
 
index 3716992f33f18840edbf4921ab3ce373df92dd21..0913623a206ffd69c066d73144312b7f38f7fa00 100644 (file)
@@ -1,5 +1,44 @@
 {% extends "base.html" %}
 
+{% macro config_item(config) -%}
+    <tr class="settings-item additional-config {% if config['advanced'] %}advanced-setting{% endif %}"
+            {% if config.get('reboot') %}reboot="true"{% endif %}
+            {% if config.get('required') %}required="true"{% endif %}
+            {% if config.get('strip') %}strip="true"{% endif %}
+            {% if config.get('depends') %}depends="{{' '.join(config['depends'])}}"{% endif %}
+            {% if config.get('min') is not none %}min="{{config['min']}}"{% endif %}
+            {% if config.get('max') is not none %}max="{{config['min']}}"{% endif %}
+            {% if config.get('floating') %}floating="true"{% endif %}
+            {% if config.get('sign') %}sign="true"{% endif %}
+            {% if config.get('snap') is not none %}snap="{{config['snap']}}"{% endif %}
+            {% if config.get('ticks') %}ticks="{{config['ticks']}}"{% endif %}
+            {% if config.get('ticksnum') is not none %}ticksnum="{{config['ticksnum']}}"{% endif %}
+            {% if config.get('decimals') is not none %}decimals="{{config['decimals']}}"{% endif %}
+            {% if config.get('unit') %}unit="{{config['unit']}}"{% endif %}>
+        <td class="settings-item-label"><span class="settings-item-label">{{config['label']}}</span></td>
+        <td class="settings-item-value">
+            {% if config['type'] == 'str' %}
+                <input type="text" class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Entry">
+            {% elif config['type'] == 'pwd' %}
+                <input type="password" class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Entry">
+            {% elif config['type'] == 'number' %}
+                <input type="text" class="number styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Entry">
+            {% elif config['type'] == 'range' %}
+                <input type="text" class="range styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Slider">
+            {% elif config['type'] == 'bool' %}
+                <input type="checkbox" class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Switch">
+            {% elif config['type'] == 'choices' %}
+                <select class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Select">
+                    {% for choice in config['choices'] %}
+                    <option value="{{choice[0]}}">{{choice[1]}}</option>
+                    {% endfor %}
+                </select>
+            {% endif %}
+        </td>
+        <td>{% if config.get('description') %}<span class="help-mark" title="{{config['description']}}">?</span>{% endif %}</td>
+    </tr>
+{%- endmacro %}
+
 {% block title %}{% if title %}{{title}}{% else %}motionEye{% endif %}{% endblock %}
 
 {% block style %}
     <div class="page">
         <div class="settings closed">
             <div class="settings-container">
-                <div class="settings-section-title"><input type="checkbox" class="styled section general" id="motionEyeSwitch">General Settings</div>
+                <div class="settings-section-title"><input type="checkbox" class="styled section general main-config" id="motionEyeSwitch">General Settings</div>
                 <table class="settings">
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Show Advanced Settings</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled general" id="showAdvancedSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled general main-config" id="showAdvancedSwitch"></td>
                         <td><span class="help-mark" title="enable this to be able to access all the advanced settings">?</span></td>
                     </tr>
-                    <tr class="settings-item">
+                    <tr class="settings-item" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Administrator Username</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled general" id="adminUsernameEntry" readonly="readonly"></td>
+                        <td class="settings-item-value"><input type="text" class="styled general main-config" id="adminUsernameEntry" readonly="readonly"></td>
                         <td><span class="help-mark" title="the username supplied to configure motionEye">?</span></td>
                     </tr>
-                    <tr class="settings-item">
+                    <tr class="settings-item" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Administrator Password</span></td>
-                        <td class="settings-item-value"><input type="password" class="styled general" id="adminPasswordEntry"></td>
+                        <td class="settings-item-value"><input type="password" class="styled general main-config" id="adminPasswordEntry"></td>
                         <td><span class="help-mark" title="administrator's password">?</span></td>
                     </tr>
-                    <tr class="settings-item">
+                    <tr class="settings-item" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Surveillance Username</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled general" id="normalUsernameEntry"></td>
+                        <td class="settings-item-value"><input type="text" class="styled general main-config" id="normalUsernameEntry"></td>
                         <td><span class="help-mark" title="the username supplied for video surveillance">?</span></td>
                     </tr>
-                    <tr class="settings-item">
+                    <tr class="settings-item" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Surveillance Password</span></td>
-                        <td class="settings-item-value"><input type="password" class="styled general" id="normalPasswordEntry"></td>
+                        <td class="settings-item-value"><input type="password" class="styled general main-config" id="normalPasswordEntry"></td>
                         <td><span class="help-mark" title="the password for the surveillance user (leave empty for passwordless surveillance)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting{% if not timezones %} hidden{% endif %}">
-                        <td class="settings-item-label"><span class="settings-item-label">Time Zone</span></td>
-                        <td class="settings-item-value">
-                            <select class="styled general" id="timeZoneSelect">
-                                {% for timezone in timezones %}
-                                <option value="{{timezone}}">{{timezone}}</option>
-                                {% endfor %}
-                            </select>
-                        </td>
-                        <td><span class="help-mark" title="selecting the right timezone assures a correct timestamp displayed on pictures and movies">?</span></td>
-                    </tr>
+                    {% for config in main_sections.get('general', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                     <tr class="settings-item advanced-setting">
                         <td colspan="100"><div class="settings-item-separator"></div></td>
                     </tr>
                     </tr>
                 </table>
                 
-                <div class="settings-section-title advanced-setting{% if not wpa_supplicant %} hidden{% endif %}"><input type="checkbox" class="styled section wifi" id="wifiSwitch">Wireless Network</div>
-                <table class="settings advanced-setting{% if not wpa_supplicant %} hidden{% endif %}">
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Network Name</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled wifi" id="wifiNameEntry" placeholder="name..."></td>
-                        <td><span class="help-mark" title="the name (SSID) of your wireless network">?</span></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Network Key</span></td>
-                        <td class="settings-item-value"><input type="password" class="styled wifi" id="wifiKeyEntry" placeholder="key..."></td>
-                        <td><span class="help-mark" title="the key (PSK) required to connect to your wireless network">?</span></td>
-                    </tr>
+                {% for section in main_sections.values() %}
+                {% if section.get('label') and section.get('configs') %}
+                <div class="settings-section-title {% if section['advanced'] %}advanced-setting{% endif %}" title="{{section.get('description') or ''}}">{% if section.get('onoff') %}
+                        <input type="checkbox" class="styled section additional-section {{section['name']}} main-config" id="{{section['name']}}Switch">{% endif %}{{section['label']}}</div>
+                <table class="settings">
+                    {% for config in section['configs'] %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
+                {% endif %}                    
+                {% endfor %}
 
-                <div class="settings-section-title"><input type="checkbox" class="styled section device" id="videoDeviceSwitch">Video Device</div>
+                <div class="settings-section-title"><input type="checkbox" class="styled section device camera-config" id="videoDeviceSwitch">Video Device</div>
                 <table class="settings">
-                    <tr class="settings-item">
+                    <tr class="settings-item" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Camera Name</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled device" id="deviceNameEntry" placeholder="camera name..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled device camera-config" id="deviceNameEntry" placeholder="camera name..."></td>
                         <td><span class="help-mark" title="an alias for this camera device">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Camera Device</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled device" id="deviceEntry" disabled="disabled"></td>
+                        <td class="settings-item-value"><input type="text" class="styled device camera-config" id="deviceEntry" disabled="disabled"></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td colspan="100"><div class="settings-item-separator"></div></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Light Switch Detection</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled device" id="lightSwitchDetectSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled device camera-config" id="lightSwitchDetectSwitch"></td>
                         <td><span class="help-mark" title="enable this if you want sudden changes in light to not be treated as motion">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Automatic Brightness</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled device" id="autoBrightnessSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled device camera-config" id="autoBrightnessSwitch"></td>
                         <td><span class="help-mark" title="enables software automatic brightness (only recommended for cameras without autobrightness)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Brightness</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled device" id="brightnessSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="brightnessSlider"></td>
                         <td><span class="help-mark" title="sets a desired brightness level for this camera">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Contrast</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled device" id="contrastSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="contrastSlider"></td>
                         <td><span class="help-mark" title="sets a desired contrast level for this camera">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Saturation</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled device" id="saturationSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="saturationSlider"></td>
                         <td><span class="help-mark" title="sets a desired saturation (color) level for this camera">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Hue</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled device" id="hueSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="hueSlider"></td>
                         <td><span class="help-mark" title="sets a desired hue (color) for this camera">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Video Resolution</span></td>
                         <td class="settings-item-value">
-                            <select class="video-resolution styled device" id="resolutionSelect">
+                            <select class="video-resolution styled device camera-config" id="resolutionSelect">
                             </select>
                         </td>
                         <td><span class="help-mark" title="the video resolution (larger values produce better quality but require larger storage space and bandwidth)">?</span></td>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Video Rotation</span></td>
                         <td class="settings-item-value">
-                            <select class="rotation styled device" id="rotationSelect">
+                            <select class="rotation styled device camera-config" id="rotationSelect">
                                 <option value="0">0&deg;</option>
                                 <option value="90">90&deg;</option>
                                 <option value="180">180&deg;</option>
                         </td>
                         <td><span class="help-mark" title="use this to rotate the captured image, if your camera is not positioned correctly">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="2" max="30" snap="0" ticks="2|5|10|15|20|25|30" decimals="0">
                         <td class="settings-item-label"><span class="settings-item-label">Frame Rate</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled device" id="framerateSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="framerateSlider"></td>
                         <td><span class="help-mark" title="sets the number of frames captured by the camera every second (higher values produce smoother videos but require larger storage space and bandwidth)">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('device', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
                 
                 <div class="settings-section-title advanced-setting">File Storage</div>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Storage Device</span></td>
                         <td class="settings-item-value">
-                            <select class="styled storage" id="storageDeviceSelect">
+                            <select class="styled storage camera-config" id="storageDeviceSelect">
                             </select>
                         </td>
                         <td><span class="help-mark" title="indicates the storage device where the image and video files will be saved">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Network Server</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled storage" id="networkServerEntry"></td>
+                        <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="networkServerEntry"></td>
                         <td><span class="help-mark" title="the address of the network server (IP address or hostname)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Share Name</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled storage" id="networkShareNameEntry"></td>
+                        <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="networkShareNameEntry"></td>
                         <td><span class="help-mark" title="the name of the network share">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Share Username</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled storage" id="networkUsernameEntry"></td>
+                        <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="networkUsernameEntry"></td>
                         <td><span class="help-mark" title="the username to be supplied when accessing the network share (leave empty if no username is required)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Share Password</span></td>
-                        <td class="settings-item-value"><input type="password" class="styled storage" id="networkPasswordEntry"></td>
+                        <td class="settings-item-value"><input type="password" class="styled storage camera-config" 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">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Root Directory</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled storage" id="rootDirectoryEntry"></td>
+                        <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="rootDirectoryEntry"></td>
                         <td><span class="help-mark" title="the root path (on the selected storage device) where the files will be saved">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Disk Usage</span></td>
                         <td class="settings-item-value">
-                            <div id="diskUsageProgressBar"></div>
+                            <div id="diskUsageProgressBar" class="progress-bar"></div>
                         </td>
                         <td><span class="help-mark" title="the used/total size of the disk where the root directory resides">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('storage', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
                 
-                <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section text-overlay" id="textOverlaySwitch">Text Overlay</div>
+                <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section text-overlay camera-config" id="textOverlaySwitch">Text Overlay</div>
                 <table class="settings advanced-setting">
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Left Text</span></td>
                         <td class="settings-item-value">
-                            <select class="styled text-overlay" id="leftTextSelect">
+                            <select class="styled text-overlay camera-config" id="leftTextSelect">
                                 <option value="camera-name">Camera Name</option>
                                 <option value="timestamp">Timestamp</option>
                                 <option value="custom-text">Custom Text</option>
                         </td>
                         <td><span class="help-mark" title="sets the text displayed on the movies and images, on the lower left corner">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"></td>
-                        <td class="settings-item-value"><input type="text" class="styled text-overlay" id="leftTextEntry" placeholder="custom text..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled text-overlay camera-config" id="leftTextEntry" placeholder="custom text..."></td>
                         <td><span class="help-mark" title="sets a custom left text; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %T = HH:MM:SS, %q = frame number, \n = new line">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Right Text</span></td>
                         <td class="settings-item-value">
-                            <select class="styled text-overlay" id="rightTextSelect">
+                            <select class="styled text-overlay camera-config" id="rightTextSelect">
                                 <option value="camera-name">Camera Name</option>
                                 <option value="timestamp">Timestamp</option>
                                 <option value="custom-text">Custom Text</option>
                         </td>
                         <td><span class="help-mark" title="sets the text displayed on the movies and images, on the lower right corner">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"></td>
-                        <td class="settings-item-value"><input type="text" class="styled text-overlay" id="rightTextEntry" placeholder="custom text..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled text-overlay camera-config" id="rightTextEntry" placeholder="custom text..."></td>
                         <td><span class="help-mark" title="sets a custom right text; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %T = HH:MM:SS, %q = frame number, \n = new line">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('text-overlay', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
 
-                <div class="settings-section-title"><input type="checkbox" class="styled section streaming" id="videoStreamingSwitch">Video Streaming</div>
+                <div class="settings-section-title"><input type="checkbox" class="styled section streaming camera-config" id="videoStreamingSwitch">Video Streaming</div>
                 <table class="settings">
-                    <tr class="settings-item advanced-setting local-streaming">
+                    <tr class="settings-item advanced-setting local-streaming" min="1" max="30" snap="0" ticks="1|5|10|15|20|25|30" decimals="0">
                         <td class="settings-item-label"><span class="settings-item-label">Streaming Frame Rate</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled streaming" id="streamingFramerateSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled streaming camera-config" id="streamingFramerateSlider"></td>
                         <td><span class="help-mark" title="sets the number of frames transmitted every second on the live streaming">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting local-streaming">
+                    <tr class="settings-item advanced-setting local-streaming" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Streaming Quality</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled streaming" id="streamingQualitySlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled streaming camera-config" id="streamingQualitySlider"></td>
                         <td><span class="help-mark" title="sets the live streaming quality (higher values produce a better video quality but require more bandwidth)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting local-streaming">
                         <td class="settings-item-label"><span class="settings-item-label">Streaming Image Resizing</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled streaming" id="streamingServerResizeSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled streaming camera-config" id="streamingServerResizeSwitch"></td>
                         <td><span class="help-mark" title="when this is enabled, the images are resized by motionEye before they are sent to the browser (disable when running motionEye on a slow CPU)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting local-streaming">
+                    <tr class="settings-item advanced-setting local-streaming" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Streaming Resolution</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled streaming" id="streamingResolutionSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled streaming camera-config" id="streamingResolutionSlider"></td>
                         <td><span class="help-mark" title="the streaming resolution given as percent of the video device resolution (higher values produce better video quality but require more bandwidth)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="1024" max="65535" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Streaming Port</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingPortEntry"></td>
+                        <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingPortEntry"></td>
                         <td><span class="help-mark" title="sets the TCP port on which the webcam streaming server listens">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Motion Optimization</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled streaming" id="streamingMotion"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled streaming camera-config" id="streamingMotion"></td>
                         <td><span class="help-mark" title="enable this if you want a lower frame rate for the live streaming when no motion is detected">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting local-streaming">
                         <td class="settings-item-label"><span class="settings-item-label">Snapshot URL</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingSnapshotUrlEntry" readonly="readonly"></td>
+                        <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingSnapshotUrlEntry" readonly="readonly"></td>
                         <td><span class="help-mark" title="a URL that provides a JPEG image with the most recent snapshot of the camera">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Streaming URL</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingMjpgUrlEntry" readonly="readonly"></td>
+                        <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingMjpgUrlEntry" readonly="readonly"></td>
                         <td><span class="help-mark" title="a URL that provides a MJPEG stream of the camera (there is no password protection for this URL!)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting local-streaming">
                         <td class="settings-item-label"><span class="settings-item-label">Embed URL</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingEmbedUrlEntry" readonly="readonly"></td>
+                        <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingEmbedUrlEntry" readonly="readonly"></td>
                         <td><span class="help-mark" title="a URL that provides a minimal HTML document containing the camera frame, ready to be embedded">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('streaming', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
                 
-                <div class="settings-section-title"><input type="checkbox" class="styled section still-images" id="stillImagesSwitch">Still Images</div>
+                <div class="settings-section-title"><input type="checkbox" class="styled section still-images camera-config" id="stillImagesSwitch">Still Images</div>
                 <table class="settings">
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Image File Name</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled still-images" id="imageFileNameEntry" placeholder="file name pattern..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled still-images camera-config" id="imageFileNameEntry" placeholder="file name pattern..."></td>
                         <td><span class="help-mark" title="sets the name pattern for the image (JPEG) files; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number, %v = event number / = subfolder">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Image Quality</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled still-images" id="imageQualitySlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled still-images camera-config" id="imageQualitySlider"></td>
                         <td><span class="help-mark" title="sets the JPEG image quality (higher values produce a better image quality but require more storage space)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Capture Mode</span></td>
                         <td class="settings-item-value">
-                            <select class="styled still-images" id="captureModeSelect">
+                            <select class="styled still-images camera-config" id="captureModeSelect">
                                 <option value="motion-triggered">Motion Triggered</option>
                                 <option value="interval-snapshots">Interval Snapshots</option>
                                 <option value="all-frames">All Frames</option>
                         </td>
                         <td><span class="help-mark" title="sets the image capture mode: Motion Triggered = an image captured whenever motion is detected, Automated Snapshots = an image captured every x seconds, All Frames = saves each frame into an image file">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="1" max="86400" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Snapshot Interval</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled number still-images" id="snapshotIntervalEntry"><span class="settings-item-unit">seconds</span></td>
+                        <td class="settings-item-value"><input type="text" class="styled number still-images camera-config" id="snapshotIntervalEntry"><span class="settings-item-unit">seconds</span></td>
                         <td><span class="help-mark" title="sets the interval (in seconds) for the automated snapshots">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Preserve Pictures</span></td>
                         <td class="settings-item-value">
-                            <select class="styled still-images" id="preservePicturesSelect">
+                            <select class="styled still-images camera-config" id="preservePicturesSelect">
                                 <option value="1">For One Day</option>
                                 <option value="7">For One Week</option>
                                 <option value="30">For One Month</option>
                         </td>
                         <td><span class="help-mark" title="images older than the specified duration are automatically deleted to free storage space">?</span></td>
                     </tr>
-                    <tr class="settings-item">
+                    <tr class="settings-item" min="1" max="3650" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Pictures Lifetime</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled number still-images" id="picturesLifetimeEntry"><span class="settings-item-unit">days</span></td>
+                        <td class="settings-item-value"><input type="text" class="styled number still-images camera-config" id="picturesLifetimeEntry"><span class="settings-item-unit">days</span></td>
                         <td><span class="help-mark" title="sets the number of days after which the pictures will be deleted automatically">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('still-images', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
                 
-                <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section motion-detection" id="motionDetectionSwitch">Motion Detection</div>
+                <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section motion-detection camera-config" id="motionDetectionSwitch">Motion Detection</div>
                 <table class="settings advanced-setting">
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Show Frame Changes</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled motion-detection" id="showFrameChangesSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled motion-detection camera-config" id="showFrameChangesSwitch"></td>
                         <td><span class="help-mark" title="if this is enabled, frame changes (number of pixels as well as the changed area) are shown on the video; temporarily enable this option to help adjust the motion detection parameters">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="20" snap="0" ticksnum="5" decimals="1" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Frame Change Threshold</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled motion-detection" id="frameChangeThresholdSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled motion-detection camera-config" id="frameChangeThresholdSlider"></td>
                         <td><span class="help-mark" title="indicates the minimal percent of the image that must change between two successive frames in order for motion to be detected (smaller values give a more sensitive detection, but are prone to false positives)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Auto Noise Detection</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled motion-detection" id="autoNoiseDetectSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled motion-detection camera-config" id="autoNoiseDetectSwitch"></td>
                         <td><span class="help-mark" title="enable this to automatically adjust the noise level">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="25" snap="0" ticksnum="6" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Noise Level</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled motion-detection" id="noiseLevelSlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled motion-detection camera-config" id="noiseLevelSlider"></td>
                         <td><span class="help-mark" title="manually sets the noise level to a fixed value">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td colspan="100"><div class="settings-item-separator"></div></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="1" max="86400" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Motion Gap</span></td>
-                        <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="eventGapEntry"><span class="settings-item-unit">seconds</span></td>
+                        <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="eventGapEntry"><span class="settings-item-unit">seconds</span></td>
                         <td><span class="help-mark" title="sets the number of seconds of silence (i.e. no motion) that mark the end of a motion event">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Captured Before</span></td>
-                        <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="preCaptureEntry"><span class="settings-item-unit">frames</span></td>
+                        <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="preCaptureEntry"><span class="settings-item-unit">frames</span></td>
                         <td><span class="help-mark" title="sets the number of frames to be captured (and included in the movie) before a motion event is detected">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Captured After</span></td>
-                        <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="postCaptureEntry"><span class="settings-item-unit">frames</span></td>
+                        <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="postCaptureEntry"><span class="settings-item-unit">frames</span></td>
                         <td><span class="help-mark" title="sets the number of frames to be captured (and included in the movie) after a motion event is detected">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="1" max="1000" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Minimum Motion Frames</span></td>
-                        <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="minimumMotionFramesEntry"><span class="settings-item-unit">frames</span></td>
+                        <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="minimumMotionFramesEntry"><span class="settings-item-unit">frames</span></td>
                         <td><span class="help-mark" title="sets the minimum number of successive motion frames required to start a motion event">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('motion-detection', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
                 
-                <div class="settings-section-title"><input type="checkbox" class="styled section motion-movies" id="motionMoviesSwitch">Motion Movies</div>
+                <div class="settings-section-title"><input type="checkbox" class="styled section motion-movies camera-config" id="motionMoviesSwitch">Motion Movies</div>
                 <table class="settings">
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Movie File Name</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled motion-movies" id="movieFileNameEntry" placeholder="file name pattern..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled motion-movies camera-config" id="movieFileNameEntry" placeholder="file name pattern..."></td>
                         <td><span class="help-mark" title="sets the name pattern for the movie (MPEG) files; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number, / = subfolder">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Movie Quality</span></td>
-                        <td class="settings-item-value"><input type="text" class="range styled motion-movies" id="movieQualitySlider"></td>
+                        <td class="settings-item-value"><input type="text" class="range styled motion-movies camera-config" id="movieQualitySlider"></td>
                         <td><span class="help-mark" title="sets the MPEG video quality (higher values produce a better video quality but require more storage space)">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Preserve Movies</span></td>
                         <td class="settings-item-value">
-                            <select class="styled motion-movies" id="preserveMoviesSelect">
+                            <select class="styled motion-movies camera-config" id="preserveMoviesSelect">
                                 <option value="1">For One Day</option>
                                 <option value="7">For One Week</option>
                                 <option value="30">For One Month</option>
                         </td>
                         <td><span class="help-mark" title="movies older than the specified duration are automatically deleted to free storage space">?</span></td>
                     </tr>
-                    <tr class="settings-item">
+                    <tr class="settings-item" min="1" max="3650" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Movies Lifetime</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled number still-images" id="moviesLifetimeEntry"><span class="settings-item-unit">days</span></td>
+                        <td class="settings-item-value"><input type="text" class="styled number motion-movies camera-config" id="moviesLifetimeEntry"><span class="settings-item-unit">days</span></td>
                         <td><span class="help-mark" title="sets the number of days after which the movies will be deleted automatically">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('motion-movies', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
                 
                 <div class="settings-section-title advanced-setting">Motion Notifications</div>
                 <table class="settings">
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Email Notifications</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="emailNotificationsSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="emailNotificationsSwitch"></td>
                         <td><span class="help-mark" title="enable this if you want to receive email notifications whenever a motion event is detected">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Email Addresses</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled notifications" id="emailAddressesEntry" placeholder="email addresses..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="emailAddressesEntry" placeholder="email addresses..."></td>
                         <td><span class="help-mark" title="email addresses (separated by comma) that are added here will receive notifications whenever a motion event is detected">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">SMTP Server</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled notifications" id="smtpServerEntry" placeholder="e.g. smtp.gmail.com"></td>
+                        <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="smtpServerEntry" placeholder="e.g. smtp.gmail.com"></td>
                         <td><span class="help-mark" title="enter the hostname or IP address of your SMTP server (for Gmail use smtp.gmail.com)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="1" max="65535" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">SMTP Port</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled number notifications" id="smtpPortEntry" placeholder="e.g. 587"></td>
+                        <td class="settings-item-value"><input type="text" class="styled number notifications camera-config" id="smtpPortEntry" placeholder="e.g. 587"></td>
                         <td><span class="help-mark" title="enter the port used by your SMTP server (usually 465 for non-TLS connections and 587 for TLS connections)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">SMTP Account</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled notifications" id="smtpAccountEntry" placeholder="account@gmail.com..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="smtpAccountEntry" placeholder="account@gmail.com..."></td>
                         <td><span class="help-mark" title="enter your SMTP account (normally your email address)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">SMTP Password</span></td>
-                        <td class="settings-item-value"><input type="password" class="styled notifications" id="smtpPasswordEntry"></td>
+                        <td class="settings-item-value"><input type="password" class="styled notifications camera-config" id="smtpPasswordEntry"></td>
                         <td><span class="help-mark" title="enter your SMTP account password (for Gmail use your Google password or an app-specific generated password)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Use TLS</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="smtpTlsSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="smtpTlsSwitch"></td>
                         <td><span class="help-mark" title="enable this if your SMTP server requires TLS (Gmail needs this to be enabled)">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" min="0" max="60" required="true">
                         <td class="settings-item-label"><span class="settings-item-label">Attached Pictures Time Span</span></td>
-                        <td class="settings-item-value"><input type="text" class="number styled notifications" id="emailPictureTimeSpanEntry"><span class="settings-item-unit">seconds</span></td>
+                        <td class="settings-item-value"><input type="text" class="number styled notifications camera-config" id="emailPictureTimeSpanEntry"><span class="settings-item-unit">seconds</span></td>
                         <td><span class="help-mark" title="defines the picture search time interval to use when creating email attachments (higher values generate emails with more pictures at the cost of an increased notification delay)">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Web Hook Notifications</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="webHookNotificationsSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="webHookNotificationsSwitch"></td>
                         <td><span class="help-mark" title="enable this if you want a URL to be requested whenever a motion event is detected">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Web Hook URL</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled notifications" id="webHookUrlEntry" placeholder="e.g. http://example.com/notify/"></td>
+                        <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="webHookUrlEntry" placeholder="e.g. http://example.com/notify/"></td>
                         <td><span class="help-mark" title="a URL to be requested when motion is detected; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">HTTP Method</span></td>
                         <td class="settings-item-value">
-                            <select class="styled notifications" id="webHookHttpMethodSelect">
+                            <select class="styled notifications camera-config" id="webHookHttpMethodSelect">
                                 <option value="GET">GET</option>
                                 <option value="POST">POST</option>
                             </select>
                     </tr>
                     <tr class="settings-item advanced-setting">
                         <td class="settings-item-label"><span class="settings-item-label">Run A Command</span></td>
-                        <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="commandNotificationsSwitch"></td>
+                        <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="commandNotificationsSwitch"></td>
                         <td><span class="help-mark" title="enable this if you want to execute a command whenever a motion event is detected">?</span></td>
                     </tr>
-                    <tr class="settings-item advanced-setting">
+                    <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Command</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled notifications" id="commandNotificationsEntry" placeholder="command..."></td>
+                        <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="commandNotificationsEntry" placeholder="command..."></td>
                         <td><span class="help-mark" title="a command to be executed when motion is detected; multiple commands can be separated by a semicolon; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('notifications', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
 
-                <div class="settings-section-title"><input type="checkbox" class="styled section working-schedule" id="workingScheduleSwitch">Working Schedule</div>
+                <div class="settings-section-title"><input type="checkbox" class="styled section working-schedule camera-config" id="workingScheduleSwitch">Working Schedule</div>
                 <table class="settings">
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Monday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="mondayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="mondayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="mondayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="mondayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="mondayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="mondayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Mondays">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Tuesday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="tuesdayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="tuesdayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="tuesdayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="tuesdayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="tuesdayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="tuesdayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Tuesdays">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Wednesday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="wednesdayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="wednesdayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="wednesdayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="wednesdayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="wednesdayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="wednesdayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Wednesdays">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Thursday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="thursdayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="thursdayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="thursdayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="thursdayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="thursdayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="thursdayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Thursdays">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Friday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="fridayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="fridayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="fridayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="fridayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="fridayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="fridayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Friday">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Saturday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="saturdayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="saturdayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="saturdayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="saturdayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="saturdayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="saturdayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Saturday">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Sunday</span></td>
                         <td class="settings-item-value">
-                            <input type="checkbox" class="styled working-schedule" id="sundayEnabledSwitch">
-                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="sundayFromEntry">
-                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="sundayToEntry">
+                            <input type="checkbox" class="styled working-schedule camera-config" id="sundayEnabledSwitch">
+                            <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="sundayFromEntry">
+                            <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="sundayToEntry">
                         </td>
                         <td><span class="help-mark" title="sets the working schedule time interval for Sunday">?</span></td>
                     </tr>
                     <tr class="settings-item">
                         <td class="settings-item-label"><span class="settings-item-label">Detect Motion</span></td>
                         <td class="settings-item-value">
-                            <select class="styled working-schedule" id="workingScheduleTypeSelect">
+                            <select class="styled working-schedule camera-config" id="workingScheduleTypeSelect">
                                 <option value="during">During Working Schedule</option>
                                 <option value="outside">Outside Working Schedule</option>
                             </select>
                         </td>
                         <td><span class="help-mark" title="sets whether motion detection should be active during or outside the working schedule">?</span></td>
                     </tr>
+                    {% for config in camera_sections.get('working-schedule', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
+                </table>
+
+                {% for section in camera_sections.values() %}
+                {% if section.get('label') and section.get('configs') %}
+                <div class="settings-section-title {% if section['advanced'] %}advanced-setting{% endif %}" title="{{section.get('description') or ''}}">{% if section.get('onoff') %}
+                        <input type="checkbox" class="styled section additional-section {{section['name']}} camera-config" id="{{section['name']}}Switch">{% endif %}{{section['label']}}</div>
+                <table class="settings">
+                    {% for config in section['configs'] %}
+                        {{config_item(config)}}
+                    {% endfor %}
                 </table>
+                {% endif %}                    
+                {% endfor %}
 
                 <div class="settings-progress"></div>
             </div>