]> www.vanbest.org Git - motioneye-debian/commitdiff
initial work on migrating to setuptools
authorCalin Crisan <ccrisan@gmail.com>
Wed, 19 Aug 2015 14:52:43 +0000 (17:52 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Wed, 19 Aug 2015 14:52:43 +0000 (17:52 +0300)
150 files changed:
.gitignore
MANIFEST.in [new file with mode: 0644]
README.md
eventrelay.py [deleted file]
motioneye.py [deleted file]
motioneye/__init__.py [new file with mode: 0644]
motioneye/cleanup.py [new file with mode: 0644]
motioneye/config.py [new file with mode: 0644]
motioneye/diskctl.py [new file with mode: 0644]
motioneye/eventrelay.py [new file with mode: 0755]
motioneye/handlers.py [new file with mode: 0644]
motioneye/mediafiles.py [new file with mode: 0644]
motioneye/mjpgclient.py [new file with mode: 0644]
motioneye/motionctl.py [new file with mode: 0644]
motioneye/ordereddict.py [new file with mode: 0644]
motioneye/powerctl.py [new file with mode: 0644]
motioneye/remote.py [new file with mode: 0644]
motioneye/sendmail.py [new file with mode: 0755]
motioneye/server.py [new file with mode: 0644]
motioneye/settings.py [new file with mode: 0644]
motioneye/smbctl.py [new file with mode: 0644]
motioneye/static/css/frame.css [new file with mode: 0644]
motioneye/static/css/jquery.timepicker.css [new file with mode: 0755]
motioneye/static/css/main.css [new file with mode: 0644]
motioneye/static/css/ui.css [new file with mode: 0644]
motioneye/static/favicon.ico [new file with mode: 0644]
motioneye/static/fnt/mavenpro-black-webfont.eot [new file with mode: 0644]
motioneye/static/fnt/mavenpro-black-webfont.svg [new file with mode: 0644]
motioneye/static/fnt/mavenpro-black-webfont.ttf [new file with mode: 0644]
motioneye/static/fnt/mavenpro-black-webfont.woff [new file with mode: 0644]
motioneye/static/fnt/mavenpro-bold-webfont.eot [new file with mode: 0644]
motioneye/static/fnt/mavenpro-bold-webfont.svg [new file with mode: 0644]
motioneye/static/fnt/mavenpro-bold-webfont.ttf [new file with mode: 0644]
motioneye/static/fnt/mavenpro-bold-webfont.woff [new file with mode: 0644]
motioneye/static/fnt/mavenpro-medium-webfont.eot [new file with mode: 0644]
motioneye/static/fnt/mavenpro-medium-webfont.svg [new file with mode: 0644]
motioneye/static/fnt/mavenpro-medium-webfont.ttf [new file with mode: 0644]
motioneye/static/fnt/mavenpro-medium-webfont.woff [new file with mode: 0644]
motioneye/static/fnt/mavenpro-regular-webfont.eot [new file with mode: 0644]
motioneye/static/fnt/mavenpro-regular-webfont.svg [new file with mode: 0644]
motioneye/static/fnt/mavenpro-regular-webfont.ttf [new file with mode: 0644]
motioneye/static/fnt/mavenpro-regular-webfont.woff [new file with mode: 0644]
motioneye/static/img/apply-progress.gif [new file with mode: 0644]
motioneye/static/img/arrows.svg [new file with mode: 0644]
motioneye/static/img/camera-progress.gif [new file with mode: 0644]
motioneye/static/img/combo-box-arrow.svg [new file with mode: 0644]
motioneye/static/img/logout.svg [new file with mode: 0644]
motioneye/static/img/main-loading-progress.gif [new file with mode: 0644]
motioneye/static/img/modal-progress.gif [new file with mode: 0644]
motioneye/static/img/motioneye-icon.svg [new file with mode: 0644]
motioneye/static/img/motioneye-logo.svg [new file with mode: 0644]
motioneye/static/img/no-camera.svg [new file with mode: 0644]
motioneye/static/img/no-preview.svg [new file with mode: 0644]
motioneye/static/img/settings.svg [new file with mode: 0644]
motioneye/static/img/slider-arrow.svg [new file with mode: 0644]
motioneye/static/img/small-progress.gif [new file with mode: 0644]
motioneye/static/img/top-bar-buttons.svg [new file with mode: 0644]
motioneye/static/img/validation-error.svg [new file with mode: 0644]
motioneye/static/js/css-browser-selector.js [new file with mode: 0644]
motioneye/static/js/frame.js [new file with mode: 0644]
motioneye/static/js/jquery.min.js [new file with mode: 0644]
motioneye/static/js/jquery.mousewheel.js [new file with mode: 0644]
motioneye/static/js/jquery.timepicker.min.js [new file with mode: 0644]
motioneye/static/js/main.js [new file with mode: 0644]
motioneye/static/js/ui.js [new file with mode: 0644]
motioneye/static/js/version.js [new file with mode: 0644]
motioneye/template.py [new file with mode: 0644]
motioneye/templates/base.html [new file with mode: 0644]
motioneye/templates/main.html [new file with mode: 0644]
motioneye/templates/version.html [new file with mode: 0644]
motioneye/thumbnailer.py [new file with mode: 0644]
motioneye/tzctl.py [new file with mode: 0644]
motioneye/update.py [new file with mode: 0644]
motioneye/utils.py [new file with mode: 0644]
motioneye/v4l2ctl.py [new file with mode: 0644]
motioneye/webhook.py [new file with mode: 0755]
motioneye/wifictl.py [new file with mode: 0644]
motioneye/wsswitch.py [new file with mode: 0644]
sendmail.py [deleted file]
settings_default.py [deleted file]
setup.py [new file with mode: 0644]
src/cleanup.py [deleted file]
src/config.py [deleted file]
src/diskctl.py [deleted file]
src/handlers.py [deleted file]
src/mediafiles.py [deleted file]
src/mjpgclient.py [deleted file]
src/motionctl.py [deleted file]
src/ordereddict.py [deleted file]
src/powerctl.py [deleted file]
src/remote.py [deleted file]
src/server.py [deleted file]
src/smbctl.py [deleted file]
src/template.py [deleted file]
src/thumbnailer.py [deleted file]
src/tzctl.py [deleted file]
src/update.py [deleted file]
src/utils.py [deleted file]
src/v4l2ctl.py [deleted file]
src/wifictl.py [deleted file]
src/wsswitch.py [deleted file]
static/css/frame.css [deleted file]
static/css/jquery.timepicker.css [deleted file]
static/css/main.css [deleted file]
static/css/ui.css [deleted file]
static/favicon.ico [deleted file]
static/fnt/mavenpro-black-webfont.eot [deleted file]
static/fnt/mavenpro-black-webfont.svg [deleted file]
static/fnt/mavenpro-black-webfont.ttf [deleted file]
static/fnt/mavenpro-black-webfont.woff [deleted file]
static/fnt/mavenpro-bold-webfont.eot [deleted file]
static/fnt/mavenpro-bold-webfont.svg [deleted file]
static/fnt/mavenpro-bold-webfont.ttf [deleted file]
static/fnt/mavenpro-bold-webfont.woff [deleted file]
static/fnt/mavenpro-medium-webfont.eot [deleted file]
static/fnt/mavenpro-medium-webfont.svg [deleted file]
static/fnt/mavenpro-medium-webfont.ttf [deleted file]
static/fnt/mavenpro-medium-webfont.woff [deleted file]
static/fnt/mavenpro-regular-webfont.eot [deleted file]
static/fnt/mavenpro-regular-webfont.svg [deleted file]
static/fnt/mavenpro-regular-webfont.ttf [deleted file]
static/fnt/mavenpro-regular-webfont.woff [deleted file]
static/img/apply-progress.gif [deleted file]
static/img/arrows.svg [deleted file]
static/img/camera-progress.gif [deleted file]
static/img/combo-box-arrow.svg [deleted file]
static/img/logout.svg [deleted file]
static/img/main-loading-progress.gif [deleted file]
static/img/modal-progress.gif [deleted file]
static/img/motioneye-icon.svg [deleted file]
static/img/motioneye-logo.svg [deleted file]
static/img/no-camera.svg [deleted file]
static/img/no-preview.svg [deleted file]
static/img/settings.svg [deleted file]
static/img/slider-arrow.svg [deleted file]
static/img/small-progress.gif [deleted file]
static/img/top-bar-buttons.svg [deleted file]
static/img/validation-error.svg [deleted file]
static/js/css-browser-selector.js [deleted file]
static/js/frame.js [deleted file]
static/js/jquery.min.js [deleted file]
static/js/jquery.mousewheel.js [deleted file]
static/js/jquery.timepicker.min.js [deleted file]
static/js/main.js [deleted file]
static/js/ui.js [deleted file]
static/js/version.js [deleted file]
templates/base.html [deleted file]
templates/main.html [deleted file]
templates/version.html [deleted file]
webhook.py [deleted file]

index 3be82e44ad4e428dac5fb85f75bb055917fb7cf5..806f37ae5597fffcdce82dbad24721f139622d7d 100644 (file)
@@ -7,8 +7,7 @@
 *.conf
 *.json
 .settings
-settings.py
 conf
 run
 media
-log
\ No newline at end of file
+log
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..1c2510d
--- /dev/null
@@ -0,0 +1,2 @@
+include README.md
+
index dd5865dc721230fb540bbf12a2af33fb8521c157..de116c171b2bc7f54a848d66e1a4736d171a6250 100644 (file)
--- a/README.md
+++ b/README.md
  * ffmpeg\r
  * v4l-utils\r
 \r
-On a debian-based system you could run (as root):\r
-\r
-    apt-get install motion ffmpeg v4l-utils python-pip\r
-    pip install python-imaging jinja2 pycurl tornado\r
-\r
-## Browser Compatibility ##\r
-\r
-motionEye works fine with most modern browsers, including IE9+.\r
-Being designed with responsiveness in mind, it will also work nicely on mobile devices and tablets.\r
-\r
 ## Installation ##\r
 \r
  1. download the latest version from [bitbucket](https://bitbucket.org/ccrisan/motioneye/downloads) (use the *Tags* tab).\r
@@ -42,4 +32,4 @@ Being designed with responsiveness in mind, it will also work nicely on mobile d
  \r
         ./motioneye.py\r
  \r
- 5. point your favourite browser to <http://localhost:8765>
\ No newline at end of file
+ 5. point your favourite browser to <http://localhost:8765>\r
diff --git a/eventrelay.py b/eventrelay.py
deleted file mode 100755 (executable)
index 42c02f3..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import errno
-import json
-import logging
-import os.path
-import sys
-import urllib
-
-sys.path.append(os.path.join(os.path.dirname(sys.argv[0]),'src'))
-
-import settings
-import utils
-
-from motioneye import _configure_settings, _configure_logging
-
-
-_configure_settings()
-_configure_logging(module='eventrelay')
-
-
-def print_usage():
-    print 'Usage: eventrelay.py <event> <thread_id>'
-
-
-def get_admin_credentials():
-    # this shortcut function is a bit faster than using the config module functions
-    config_file_path = os.path.join(settings.CONF_PATH, 'motion.conf')
-    
-    logging.debug('reading main config from file %(path)s...' % {'path': config_file_path}) 
-
-    lines = None
-    try:
-        file = open(config_file_path, 'r')
-    
-    except IOError as e:
-        if e.errno == errno.ENOENT:  # file does not exist
-            logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
-            
-            lines = []
-        
-        else:
-            logging.error('could not open main config file %(path)s: %(msg)s' % {
-                    'path': config_file_path, 'msg': unicode(e)})
-            
-            raise
-
-    if lines is None:
-        try:
-            lines = [l[:-1] for l in file.readlines()]
-        
-        except Exception as e:
-            logging.error('could not read main config file %(path)s: %(msg)s' % {
-                    'path': config_file_path, 'msg': unicode(e)})
-            
-            raise
-        
-        finally:
-            file.close()
-    
-    admin_username = 'admin'
-    admin_password = ''
-    for line in lines:
-        line = line.strip()
-        if not line.startswith('#'):
-            continue
-        
-        line = line[1:].strip()
-        if line.startswith('@admin_username'):
-            parts = line.split(' ', 1)
-            admin_username = parts[1] if len(parts) > 1 else ''
-            
-            continue
-        
-        if line.startswith('@admin_password'):
-            parts = line.split(' ', 1)
-            admin_password = parts[1] if len(parts) > 1 else ''
-
-            continue
-    
-    return admin_username, admin_password
-
-
-# def compute_signature(method, uri, body, key):
-#     parts = list(urlparse.urlsplit(uri))
-#     query = [q for q in urlparse.parse_qsl(parts[3]) if (q[0] != 'signature')]
-#     query.sort(key=lambda q: q[0])
-#     query = urllib.urlencode(query)
-#     parts[0] = parts[1] = ''
-#     parts[3] = query
-#     uri = urlparse.urlunsplit(parts)
-#     
-#     return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower()
-
-
-if __name__ == '__main__':
-    if len(sys.argv) < 3:
-        print_usage()
-        sys.exit(-1)
-    
-    event = sys.argv[1] 
-    thread_id = sys.argv[2]
-
-    logging.debug('hello!')
-    logging.debug('event = %s' % event)
-    logging.debug('thread_id = %s' % thread_id)
-    
-    admin_username, admin_password = get_admin_credentials()
-
-    uri = '/_relay_event/?event=%(event)s&thread_id=%(thread_id)s&_username=%(username)s' % {
-            'username': admin_username,
-            'thread_id': thread_id,
-            'event': event}
-    
-    signature = utils.compute_signature('POST', uri, '', admin_password)
-    
-    url = 'http://127.0.0.1:%(port)s' + uri + '&_signature=' + signature
-    url = url % {'port': settings.PORT}
-    
-    try:
-        response = urllib.urlopen(url, data='')
-        response = json.load(response)
-        if response.get('error'):
-            raise Exception(response['error'])
-        
-        logging.debug('event successfully relayed')
-    
-    except Exception as e:
-        logging.error('failed to relay event: %s' % e)
-
-    logging.debug('bye!')
diff --git a/motioneye.py b/motioneye.py
deleted file mode 100755 (executable)
index 5f54e4b..0000000
+++ /dev/null
@@ -1,419 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import imp
-import inspect
-import logging
-import multiprocessing
-import os.path
-import re
-import signal
-import sys
-
-from tornado.httpclient import AsyncHTTPClient
-
-# test if a --settings directive has been supplied
-for i in xrange(1, len(sys.argv) - 1):
-    if sys.argv[i] == '--settings':
-        settings_module = sys.argv[i + 1]
-        imp.load_source('settings', settings_module)
-
-sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), 'src'))
-
-import settings
-import update
-
-VERSION = '0.25.2'
-
-
-def _configure_settings():
-    def set_default_setting(name, value):
-        if not hasattr(settings, name):
-            setattr(settings, name, value)
-    
-    set_default_setting('PROJECT_PATH', os.path.dirname(sys.argv[0]))
-    set_default_setting('TEMPLATE_PATH', os.path.join(settings.PROJECT_PATH, 'templates'))
-    set_default_setting('STATIC_PATH', os.path.join(settings.PROJECT_PATH, 'static'))
-    set_default_setting('STATIC_URL', '/static/')
-    set_default_setting('CONF_PATH', os.path.join(settings.PROJECT_PATH, 'conf'))
-    set_default_setting('RUN_PATH', os.path.join(settings.PROJECT_PATH, 'run'))
-    set_default_setting('LOG_PATH', os.path.join(settings.PROJECT_PATH, 'log'))
-    set_default_setting('MEDIA_PATH', os.path.join(settings.PROJECT_PATH, 'media'))
-    set_default_setting('MOTION_BINARY', None)
-    set_default_setting('LOG_LEVEL', logging.INFO)
-    set_default_setting('LISTEN', '0.0.0.0')
-    set_default_setting('PORT', 8765)
-    set_default_setting('MOUNT_CHECK_INTERVAL', 300)
-    set_default_setting('MOTION_CHECK_INTERVAL', 10)
-    set_default_setting('CLEANUP_INTERVAL', 43200)
-    set_default_setting('THUMBNAILER_INTERVAL', 60)
-    set_default_setting('REMOTE_REQUEST_TIMEOUT', 10)
-    set_default_setting('MJPG_CLIENT_TIMEOUT', 10)
-    set_default_setting('MJPG_CLIENT_IDLE_TIMEOUT', 10)
-    set_default_setting('SMB_SHARES', False)
-    set_default_setting('SMB_MOUNT_ROOT', '/media')
-    set_default_setting('WPA_SUPPLICANT_CONF', None)
-    set_default_setting('LOCAL_TIME_FILE', None)
-    set_default_setting('ENABLE_REBOOT', False)
-    set_default_setting('SMTP_TIMEOUT', 60)
-    set_default_setting('ZIP_TIMEOUT', 500)
-    set_default_setting('ADD_REMOVE_CAMERAS', True)
-
-    length = len(sys.argv) - 1
-    for i in xrange(length):
-        arg = sys.argv[i + 1]
-        
-        if not arg.startswith('--'):
-            continue
-        
-        next_arg = None
-        if i < length - 1:
-            next_arg = sys.argv[i + 2]
-        
-        name = arg[2:].upper().replace('-', '_')
-        
-        if name == 'HELP':
-            _print_help()
-            sys.exit(0)
-        
-        if hasattr(settings, name):
-            curr_value = getattr(settings, name)
-            
-            if next_arg.lower() == 'debug':
-                next_arg = logging.DEBUG
-            
-            elif next_arg.lower() == 'info':
-                next_arg = logging.INFO
-            
-            elif next_arg.lower() == 'warn':
-                next_arg = logging.WARN
-            
-            elif next_arg.lower() == 'error':
-                next_arg = logging.ERROR
-            
-            elif next_arg.lower() == 'fatal':
-                next_arg = logging.FATAL
-            
-            elif next_arg.lower() == 'true':
-                next_arg = True
-            
-            elif next_arg.lower() == 'false':
-                next_arg = False
-            
-            elif isinstance(curr_value, int):
-                next_arg = int(next_arg)
-            
-            elif isinstance(curr_value, float):
-                next_arg = float(next_arg)
-
-            setattr(settings, name, next_arg)
-        
-        else:
-            return arg[2:]
-    
-    try:
-        os.makedirs(settings.CONF_PATH)
-        
-    except:
-        pass
-    
-    try:
-        os.makedirs(settings.RUN_PATH)
-
-    except:
-        pass
-
-    try:
-        os.makedirs(settings.LOG_PATH)
-
-    except:
-        pass
-
-    try:
-        os.makedirs(settings.MEDIA_PATH)
-
-    except:
-        pass
-
-
-def _test_requirements():
-    if os.geteuid() != 0:
-        if settings.SMB_SHARES:
-            print('SMB_SHARES require root privileges')
-            return False
-
-        if settings.ENABLE_REBOOT:
-            print('reboot requires root privileges')
-            return False
-
-    try:
-        import tornado  # @UnusedImport
-        has_tornado = True
-
-    except ImportError:
-        has_tornado = False
-
-    if update.compare_versions(tornado.version, '3.1') < 0:
-        has_tornado = False
-
-    try:
-        import jinja2  # @UnusedImport
-        has_jinja2 = True
-    
-    except ImportError:
-        has_jinja2 = False
-
-    try:
-        import PIL.Image  # @UnusedImport
-        has_pil = True
-    
-    except ImportError:
-        has_pil = False
-
-    try:
-        import pycurl  # @UnusedImport
-        has_pycurl = True
-    
-    except ImportError:
-        has_pycurl = False
-
-    import mediafiles
-    has_ffmpeg = mediafiles.find_ffmpeg() is not None
-    
-    import motionctl
-    has_motion = motionctl.find_motion() is not None
-    
-    import v4l2ctl
-    has_v4lutils = v4l2ctl.find_v4l2_ctl() is not None
-    
-    import smbctl
-    has_mount_cifs = smbctl.find_mount_cifs() is not None
-    
-    ok = True
-    if not has_tornado:
-        print('please install tornado (python-tornado), version 3.1 or greater')
-        ok = False
-    
-    if not has_jinja2:
-        print('please install jinja2 (python-jinja2)')
-        ok = False
-
-    if not has_pil:
-        print('please install PIL (python-imaging)')
-        ok = False
-
-    if not has_pycurl:
-        print('please install pycurl (python-pycurl)')
-        ok = False
-    
-    if not has_ffmpeg:
-        print('please install ffmpeg')
-        ok = False
-
-    if not has_motion:
-        print('please install motion')
-        ok = False
-
-    if not has_v4lutils:
-        print('please install v4l-utils')
-        ok = False
-
-    if settings.SMB_SHARES and not has_mount_cifs:
-        print('please install cifs-utils')
-        ok = False
-
-    return ok
-
-        
-def _configure_signals():
-    def bye_handler(signal, frame):
-        import tornado.ioloop
-        
-        logging.info('interrupt signal received, shutting down...')
-
-        # shut down the IO loop if it has been started
-        ioloop = tornado.ioloop.IOLoop.instance()
-        ioloop.stop()
-        
-    def child_handler(signal, frame):
-        # this is required for the multiprocessing mechanism to work
-        multiprocessing.active_children()
-
-    signal.signal(signal.SIGINT, bye_handler)
-    signal.signal(signal.SIGTERM, bye_handler)
-    signal.signal(signal.SIGCHLD, child_handler)
-
-
-def _configure_logging(module=None):
-    if module:
-        format = '%(asctime)s: [{module}] %(levelname)s: %(message)s'.format(module=module)
-        
-    else:
-        format = '%(asctime)s: %(levelname)s: %(message)s'
-
-    logging.basicConfig(filename=None, level=settings.LOG_LEVEL,
-            format=format, datefmt='%Y-%m-%d %H:%M:%S')
-    
-    logging.getLogger('tornado').setLevel(logging.WARN)
-
-
-def _configure_tornado():
-    AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient', max_clients=16)
-
-
-def _print_help():
-    print('Usage: ' + sys.argv[0] + ' [option1 value1] ...')
-    print('available options: ')
-    
-    options = list(inspect.getmembers(settings))
-    
-    print('    --settings <module>')
-    
-    for (name, value) in sorted(options):
-        if name.upper() != name:
-            continue
-        
-        if not re.match('^[A-Z0-9_]+$', name):
-            continue
-        
-        name = '--' + name.lower().replace('_', '-')
-        if value is not None:
-            value = type(value).__name__
-        
-        line = '    ' + name
-        if value:
-            line += ' <' + value + '>'
-        print(line)
-    
-    print('')
-
-
-def _run_server():
-    import cleanup
-    import motionctl
-    import thumbnailer
-    import tornado.ioloop
-    import server
-    import smbctl
-
-    server.application.listen(settings.PORT, settings.LISTEN)
-    logging.info('server started')
-    
-    tornado.ioloop.IOLoop.instance().start()
-
-    logging.info('server stopped')
-    
-    if thumbnailer.running():
-        thumbnailer.stop()
-        logging.info('thumbnailer stopped')
-
-    if cleanup.running():
-        cleanup.stop()
-        logging.info('cleanup stopped')
-
-    if motionctl.running():
-        motionctl.stop()
-        logging.info('motion stopped')
-    
-    if settings.SMB_SHARES:
-        smbctl.umount_all()
-        logging.info('SMB shares unmounted')
-
-
-def _start_motion():
-    import tornado.ioloop
-    import config
-    import motionctl
-
-    ioloop = tornado.ioloop.IOLoop.instance()
-    
-    # add a motion running checker
-    def checker():
-        if ioloop._stopped:
-            return
-            
-        if not motionctl.running() and motionctl.started() and config.get_enabled_local_motion_cameras():
-            try:
-                logging.error('motion not running, starting it')
-                motionctl.start()
-            
-            except Exception as e:
-                logging.error('failed to start motion: %(msg)s' % {
-                        'msg': unicode(e)}, exc_info=True)
-
-        ioloop.add_timeout(datetime.timedelta(seconds=settings.MOTION_CHECK_INTERVAL), checker)
-    
-    motionctl.start()
-        
-    ioloop.add_timeout(datetime.timedelta(seconds=settings.MOTION_CHECK_INTERVAL), checker)
-
-
-def _start_cleanup():
-    import cleanup
-
-    cleanup.start()
-    logging.info('cleanup started')
-
-
-def _start_wsswitch():
-    import wsswitch
-
-    wsswitch.start()
-    logging.info('wsswitch started')
-
-
-def _start_thumbnailer():
-    import thumbnailer
-
-    thumbnailer.start()
-    logging.info('thumbnailer started')
-
-
-if __name__ == '__main__':
-    cmd = _configure_settings()
-
-    _configure_signals()
-    _configure_logging()
-    
-    if not _test_requirements():
-        sys.exit(-1)
-
-    _configure_tornado()
-    
-    logging.info('hello! this is motionEye %s' % VERSION)
-    
-    if settings.SMB_SHARES:
-        import smbctl
-
-        stop, start = smbctl.update_mounts()
-        if start:
-            _start_motion()
-
-    else:
-        _start_motion()
-        
-    _start_cleanup()
-    _start_wsswitch()
-    
-    if settings.THUMBNAILER_INTERVAL:
-        _start_thumbnailer()
-
-    _run_server()
-
-    logging.info('bye!')
\ No newline at end of file
diff --git a/motioneye/__init__.py b/motioneye/__init__.py
new file mode 100644 (file)
index 0000000..ad23e4c
--- /dev/null
@@ -0,0 +1,2 @@
+
+VERSION = '0.26'
diff --git a/motioneye/cleanup.py b/motioneye/cleanup.py
new file mode 100644 (file)
index 0000000..a032ef1
--- /dev/null
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import logging
+import multiprocessing
+import os
+import signal
+import tornado
+
+import mediafiles
+import settings
+import thumbnailer
+
+
+_process = None
+
+
+def start():
+    # schedule the first call a bit later to improve performance at startup
+    ioloop = tornado.ioloop.IOLoop.instance()
+    ioloop.add_timeout(datetime.timedelta(seconds=60), _run_process)
+
+
+def stop():
+    global _process
+    
+    if not running():
+        _process = None
+        return
+    
+    if _process.is_alive():
+        _process.join(timeout=10)
+    
+    if _process.is_alive():
+        logging.error('cleanup process did not finish in time, killing it...')
+        os.kill(_process.pid, signal.SIGKILL)
+    
+    _process = None
+
+
+def running():
+    return _process is not None and _process.is_alive()
+
+
+def _run_process():
+    global _process
+    
+    ioloop = tornado.ioloop.IOLoop.instance()
+    
+    if thumbnailer.running():
+        # postpone if thumbnailer is currently running
+        ioloop.add_timeout(datetime.timedelta(seconds=60), _run_process)
+        
+        return
+        
+    else:
+        # schedule the next call
+        ioloop.add_timeout(datetime.timedelta(seconds=settings.CLEANUP_INTERVAL), _run_process)
+
+    if not running(): # check that the previous process has finished
+        logging.debug('running cleanup process...')
+
+        _process = multiprocessing.Process(target=_do_cleanup)
+        _process.start()
+
+
+def _do_cleanup():
+    # this will be executed in a separate subprocess
+    
+    # ignore the terminate and interrupt signals in this subprocess
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+    signal.signal(signal.SIGTERM, signal.SIG_IGN)
+    
+    try:
+        mediafiles.cleanup_media('picture')
+        mediafiles.cleanup_media('movie')
+        logging.debug('cleanup done')
+         
+    except Exception as e:
+        logging.error('failed to cleanup media files: %(msg)s' % {
+                'msg': unicode(e)}, exc_info=True)
diff --git a/motioneye/config.py b/motioneye/config.py
new file mode 100644 (file)
index 0000000..235d763
--- /dev/null
@@ -0,0 +1,1789 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import collections
+import datetime
+import errno
+import logging
+import os.path
+import re
+import shlex
+import subprocess
+import urlparse
+
+from tornado.ioloop import IOLoop
+
+import diskctl
+import powerctl
+import settings
+import update
+import utils
+import v4l2ctl
+
+from utils import OrderedDict
+
+
+_CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf'
+_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')
+
+_KNOWN_MOTION_OPTIONS = set([
+    'auto_brightness', 'brightness', 'contrast', 'emulate_motion', 'event_gap', 'ffmpeg_bps', 'ffmpeg_output_movies', 'ffmpeg_variable_bitrate', 'ffmpeg_video_codec',
+    'framerate', 'height', 'hue', 'lightswitch', 'locate_motion_mode', 'locate_motion_style', 'minimum_motion_frames', 'movie_filename', 'max_movie_time', 'max_mpeg_time',
+    'noise_level', 'noise_tune', 'on_event_end', 'on_event_start', 'output_pictures', 'picture_filename', 'post_capture', 'pre_capture', 'quality', 'rotate', 'saturation',
+    'snapshot_filename', 'snapshot_interval', 'stream_auth_method', 'stream_authentication', 'stream_localhost', 'stream_maxrate', 'stream_motion', 'stream_port', 'stream_quality',
+    'target_dir', 'text_changes', 'text_double', 'text_left', 'text_right', 'threshold', 'videodevice', 'width',
+    'webcam_localhost', 'webcam_port', 'webcam_maxrate', 'webcam_quality', 'webcam_motion', 'ffmpeg_cap_new', 'output_normal', 'output_motion', 'jpeg_filename', 'output_all', 'gap', 'locate',
+    'netcam_url', 'netcam_userpass', 'netcam_http', 'netcam_tolerant_check', 'netcam_keepalive', 'rtsp_uses_tcp'
+])
+
+
+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
+    
+    if not as_lines and _main_config_cache is not None:
+        return _main_config_cache
+    
+    config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
+    
+    logging.debug('reading main config from file %(path)s...' % {'path': config_file_path})
+    
+    lines = None
+    try:
+        file = open(config_file_path, 'r')
+    
+    except IOError as e:
+        if e.errno == errno.ENOENT:  # file does not exist
+            logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
+            
+            lines = []
+        
+        else:
+            logging.error('could not open main config file %(path)s: %(msg)s' % {
+                    'path': config_file_path, 'msg': unicode(e)})
+            
+            raise
+    
+    if lines is None:
+        try:
+            lines = [l[:-1] for l in file.readlines()]
+        
+        except Exception as e:
+            logging.error('could not read main config file %(path)s: %(msg)s' % {
+                    'path': config_file_path, 'msg': unicode(e)})
+            
+            raise
+        
+        finally:
+            file.close()
+    
+    if as_lines:
+        return lines
+    
+    main_config = _conf_to_dict(lines,
+            list_names=['thread'],
+            no_convert=['@admin_username', '@admin_password', '@normal_username', '@normal_password'])
+    
+    _get_additional_config(main_config)
+    _set_default_motion(main_config, old_motion=is_old_motion())
+    
+    _main_config_cache = main_config
+    
+    return main_config
+
+
+def set_main(main_config):
+    global _main_config_cache
+    
+    main_config = dict(main_config)
+    _set_default_motion(main_config, old_motion=is_old_motion())
+    for n, v in _main_config_cache.iteritems():
+        main_config.setdefault(n, v)
+    _main_config_cache = main_config
+    
+    main_config = dict(main_config)
+    _set_additional_config(main_config)
+
+    config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
+    
+    # read the actual configuration from file
+    lines = get_main(as_lines=True)
+    
+    # write the configuration to file
+    logging.debug('writing main config to %(path)s...' % {'path': config_file_path})
+    
+    try:
+        file = open(config_file_path, 'w')
+    
+    except Exception as e:
+        logging.error('could not open main config file %(path)s for writing: %(msg)s' % {
+                'path': config_file_path, 'msg': unicode(e)})
+        
+        raise
+    
+    lines = _dict_to_conf(lines, main_config, list_names=['thread'])
+    
+    try:
+        file.writelines([utils.make_str(l) + '\n' for l in lines])
+    
+    except Exception as e:
+        logging.error('could not write main config file %(path)s: %(msg)s' % {
+                'path': config_file_path, 'msg': unicode(e)})
+        
+        raise
+    
+    finally:
+        file.close()
+
+
+def get_camera_ids(filter_valid=True):
+    global _camera_ids_cache
+    
+    if _camera_ids_cache is not None:
+        return _camera_ids_cache
+
+    config_path = settings.CONF_PATH
+    
+    logging.debug('listing config dir %(path)s...' % {'path': config_path})
+    
+    try:
+        ls = os.listdir(config_path)
+    
+    except Exception as e:
+        logging.error('failed to list config dir %(path)s: %(msg)s', {
+                'path': config_path, 'msg': unicode(e)})
+        
+        raise
+    
+    camera_ids = []
+    
+    pattern = '^' + _CAMERA_CONFIG_FILE_NAME.replace('%(id)s', '(\d+)') + '$'
+    for name in ls:
+        match = re.match(pattern, name)
+        if match:
+            camera_id = int(match.groups()[0])
+            logging.debug('found camera with id %(id)s' % {
+                    'id': camera_id})
+            
+            camera_ids.append(camera_id)
+        
+    camera_ids.sort()
+    
+    if not filter_valid:
+        return camera_ids
+
+    filtered_camera_ids = []
+    for camera_id in camera_ids:
+        if get_camera(camera_id):
+            filtered_camera_ids.append(camera_id)
+    
+    _camera_ids_cache = filtered_camera_ids
+    
+    return filtered_camera_ids
+
+
+def get_enabled_local_motion_cameras():
+    if not get_main().get('@enabled'):
+        return []
+    
+    camera_ids = get_camera_ids()
+    cameras = [get_camera(camera_id) for camera_id in camera_ids]
+    return [c for c in cameras if c.get('@enabled') and utils.local_motion_camera(c)]
+
+
+def get_network_shares():
+    if not get_main().get('@enabled'):
+        return []
+
+    camera_ids = get_camera_ids()
+    cameras = [get_camera(camera_id) for camera_id in camera_ids]
+    
+    mounts = []
+    for camera in cameras:
+        if camera.get('@storage_device') != 'network-share':
+            continue
+        
+        mounts.append({
+            'server': camera['@network_server'],
+            'share': camera['@network_share_name'],
+            'username': camera['@network_username'],
+            'password': camera['@network_password'],
+        })
+        
+    return mounts
+
+
+def get_camera(camera_id, as_lines=False):
+    global _camera_config_cache
+    
+    if not as_lines and camera_id in _camera_config_cache:
+        return _camera_config_cache[camera_id]
+    
+    camera_config_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
+    
+    logging.debug('reading camera config from %(path)s...' % {'path': camera_config_path})
+    
+    try:
+        file = open(camera_config_path, 'r')
+    
+    except Exception as e:
+        logging.error('could not open camera config file: %(msg)s' % {'msg': unicode(e)})
+        
+        raise
+    
+    try:
+        lines = [l.strip() for l in file.readlines()]
+    
+    except Exception as e:
+        logging.error('could not read camera config file %(path)s: %(msg)s' % {
+                'path': camera_config_path, 'msg': unicode(e)})
+        
+        raise
+    
+    finally:
+        file.close()
+    
+    if as_lines:
+        return lines
+        
+    camera_config = _conf_to_dict(lines,
+            no_convert=['@name', '@network_share_name', '@network_server',
+                        '@network_username', '@network_password', '@storage_device'])
+    
+    if utils.local_motion_camera(camera_config):
+        # determine the enabled status
+        main_config = get_main()
+        threads = main_config.get('thread', [])
+        camera_config['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads
+        camera_config['@id'] = camera_id
+        
+        old_motion = is_old_motion()
+        
+        # adapt directives from old configuration, if needed
+        if old_motion:
+            logging.debug('using old motion config directives')
+            
+            if 'output_normal' in camera_config:
+                camera_config['output_pictures'] = camera_config.pop('output_normal')
+            if 'output_all' in camera_config:
+                camera_config['emulate_motion'] = camera_config.pop('output_all')
+            if 'ffmpeg_cap_new' in camera_config:
+                camera_config['ffmpeg_output_movies'] = camera_config.pop('ffmpeg_cap_new')
+            if 'locate' in camera_config:
+                camera_config['locate_motion_mode'] = camera_config.pop('locate')
+            if 'jpeg_filename' in camera_config:
+                camera_config['picture_filename'] = camera_config.pop('jpeg_filename')
+            if 'max_mpeg_time' in camera_config:
+                camera_config['max_movie_time'] = camera_config.pop('max_mpeg_time')
+            if 'webcam_port' in camera_config:
+                camera_config['stream_port'] = camera_config.pop('webcam_port')
+            if 'webcam_quality' in camera_config:
+                camera_config['stream_quality'] = camera_config.pop('webcam_quality')
+            if 'webcam_motion' in camera_config:
+                camera_config['stream_motion'] = camera_config.pop('webcam_motion')
+            if 'webcam_maxrate' in camera_config:
+                camera_config['stream_maxrate'] = camera_config.pop('webcam_maxrate')
+            if 'webcam_localhost' in camera_config:
+                camera_config['stream_localhost'] = camera_config.pop('webcam_localhost')
+            if 'gap' in camera_config:
+                camera_config['event_gap'] = camera_config.pop('gap')
+            if 'netcam_http' in camera_config:
+                camera_config['netcam_keepalive'] = camera_config.pop('netcam_http') in ['1.1', 'keepalive']
+
+        _get_additional_config(camera_config, camera_id=camera_id)
+        _set_default_motion_camera(camera_id, camera_config)
+    
+    elif utils.remote_camera(camera_config):
+        pass
+    
+    elif utils.simple_mjpeg_camera(camera_config):
+        _get_additional_config(camera_config, camera_id=camera_id)
+    
+    else: # incomplete configuration
+        logging.warn('camera config file at %s is incomplete, ignoring' % camera_config_path)
+        
+        return None
+    
+    _camera_config_cache[camera_id] = dict(camera_config)
+    
+    return camera_config
+
+
+def set_camera(camera_id, camera_config):
+    global _camera_config_cache
+
+    camera_config['@id'] = camera_id
+    _camera_config_cache[camera_id] = camera_config
+
+    camera_config = dict(camera_config)
+    
+    if utils.local_motion_camera(camera_config):
+        old_motion = is_old_motion()
+        
+        # adapt directives to old configuration, if needed
+        if old_motion:
+            logging.debug('using old motion config directives')
+            
+            if 'output_pictures' in camera_config:
+                camera_config['output_normal'] = camera_config.pop('output_pictures')
+            if 'emulate_motion' in camera_config:
+                camera_config['output_all'] = camera_config.pop('emulate_motion')
+            if 'ffmpeg_output_movies' in camera_config:
+                camera_config['ffmpeg_cap_new'] = camera_config.pop('ffmpeg_output_movies')
+            if 'locate_motion_mode' in camera_config:
+                camera_config['locate'] = camera_config.pop('locate_motion_mode')
+            if 'picture_filename' in camera_config:
+                camera_config['jpeg_filename'] = camera_config.pop('picture_filename')
+            if 'max_movie_time' in camera_config:
+                camera_config['max_mpeg_time'] = camera_config.pop('max_movie_time')
+            if 'stream_port' in camera_config:
+                camera_config['webcam_port'] = camera_config.pop('stream_port')
+            if 'stream_quality' in camera_config:
+                camera_config['webcam_quality'] = camera_config.pop('stream_quality')
+            if 'stream_motion' in camera_config:
+                camera_config['webcam_motion'] = camera_config.pop('stream_motion')
+            if 'stream_maxrate' in camera_config:
+                camera_config['webcam_maxrate'] = camera_config.pop('stream_maxrate')
+            if 'stream_localhost' in camera_config:
+                camera_config['webcam_localhost'] = camera_config.pop('stream_localhost')
+            if 'stream_auth_method' in camera_config:
+                camera_config.pop('stream_auth_method')
+            if 'stream_authentication' in camera_config:
+                camera_config.pop('stream_authentication')
+            if 'event_gap' in camera_config:
+                camera_config['gap'] = camera_config.pop('event_gap')
+            if 'netcam_keepalive' in camera_config:
+                camera_config['netcam_http'] = '1.1' if camera_config.pop('netcam_keepalive') else '1.0'
+                
+        _set_default_motion_camera(camera_id, camera_config, old_motion)
+        
+        # set the enabled status in main config
+        main_config = get_main()
+        threads = main_config.setdefault('thread', [])
+        config_file_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
+        if camera_config['@enabled'] and config_file_name not in threads:
+            threads.append(config_file_name)
+                
+        elif not camera_config['@enabled']:
+            threads = [t for t in threads if t != config_file_name]
+
+        main_config['thread'] = threads
+        
+        set_main(main_config)
+        _set_additional_config(camera_config, camera_id=camera_id)
+
+    elif utils.remote_camera(camera_config):
+        pass
+    
+    elif utils.simple_mjpeg_camera(camera_config):
+        _set_additional_config(camera_config, camera_id=camera_id)
+
+    # read the actual configuration from file
+    config_file_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
+    if os.path.isfile(config_file_path):
+        lines = get_camera(camera_id, as_lines=True)
+    
+    else:
+        lines = []
+    
+    # write the configuration to file
+    camera_config_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
+    logging.debug('writing camera config to %(path)s...' % {'path': camera_config_path})
+    
+    try:
+        file = open(camera_config_path, 'w')
+    
+    except Exception as e:
+        logging.error('could not open camera config file %(path)s for writing: %(msg)s' % {
+                'path': camera_config_path, 'msg': unicode(e)})
+        
+        raise
+    
+    lines = _dict_to_conf(lines, camera_config)
+    
+    try:
+        file.writelines([utils.make_str(l) + '\n' for l in lines])
+    
+    except Exception as e:
+        logging.error('could not write camera config file %(path)s: %(msg)s' % {
+                'path': camera_config_path, 'msg': unicode(e)})
+        
+        raise
+    
+    finally:
+        file.close()
+        
+
+def add_camera(device_details):
+    global _camera_ids_cache
+    global _camera_config_cache
+    
+    proto = device_details['proto']
+    if proto in ['netcam', 'mjpeg']:
+        host = device_details['host']
+        if device_details['port']:
+            host += ':' + str(device_details['port'])
+
+        if device_details['username'] and proto == 'mjpeg':
+            if device_details['password']:
+                host = device_details['username'] + ':' + device_details['password'] + '@' + host
+                
+            else:
+                host = device_details['username'] + '@' + host
+
+        device_details['url'] = urlparse.urlunparse((device_details['scheme'], host, device_details['uri'], '', '', ''))
+
+    # determine the last camera id
+    camera_ids = get_camera_ids()
+
+    camera_id = 1
+    while camera_id in camera_ids:
+        camera_id += 1
+    
+    logging.info('adding new camera with id %(id)s...' % {'id': camera_id})
+    
+    # prepare a default camera config
+    camera_config = {'@enabled': True}
+    if proto == 'v4l2':
+        # find a suitable resolution
+        for (w, h) in v4l2ctl.list_resolutions(device_details['uri']):
+            if w > 300:
+                camera_config['width'] = w
+                camera_config['height'] = h
+                break
+
+        camera_config['videodevice'] = device_details['uri']
+        _set_default_motion_camera(camera_id, camera_config)
+    
+    elif proto == 'motioneye':
+        camera_config['@proto'] = 'motioneye'
+        camera_config['@scheme'] = device_details['scheme']
+        camera_config['@host'] = device_details['host']
+        camera_config['@port'] = device_details['port']
+        camera_config['@uri'] = device_details['uri']
+        camera_config['@username'] = device_details['username']
+        camera_config['@password'] = device_details['password']
+        camera_config['@remote_camera_id'] = device_details['remote_camera_id']
+
+    elif proto == 'netcam':
+        camera_config['netcam_url'] = device_details['url']
+        camera_config['text_double'] = True
+        
+        if device_details['username']:
+            camera_config['netcam_userpass'] = device_details['username'] + ':' + device_details['password']
+        
+        camera_config['netcam_keepalive'] = device_details.get('keep_alive')
+        camera_config['netcam_tolerant_check'] = True
+
+        if device_details.get('camera_index') == 'udp':
+            camera_config['rtsp_uses_tcp'] = False
+        
+        if camera_config['netcam_url'].startswith('rtsp'):
+            camera_config['width'] = 640
+            camera_config['height'] = 480
+
+        _set_default_motion_camera(camera_id, camera_config)
+
+    else: # assuming mjpeg
+        camera_config['@proto'] = 'mjpeg'
+        camera_config['@url'] = device_details['url']
+        _set_default_simple_mjpeg_camera(camera_id, camera_config)
+
+    # write the configuration to file
+    set_camera(camera_id, camera_config)
+    
+    _camera_ids_cache = None
+    _camera_config_cache = {}
+    
+    camera_config = get_camera(camera_id)
+    
+    return camera_config
+
+
+def rem_camera(camera_id):
+    global _camera_ids_cache
+    global _camera_config_cache
+    
+    camera_config_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
+    camera_config_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
+    
+    # remove the camera from the main config
+    main_config = get_main()
+    threads = main_config.setdefault('thread', [])
+    threads = [t for t in threads if t != camera_config_name]
+    
+    main_config['thread'] = threads
+
+    set_main(main_config)
+    
+    logging.info('removing camera config file %(path)s...' % {'path': camera_config_path})
+    
+    _camera_ids_cache = None
+    _camera_config_cache = {}
+    
+    try:
+        os.remove(camera_config_path)
+    
+    except Exception as e:
+        logging.error('could not remove camera config file %(path)s: %(msg)s' % {
+                'path': camera_config_path, 'msg': unicode(e)})
+        
+        raise
+
+
+def main_ui_to_dict(ui):
+    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']
+    }
+
+    # additional configs
+    for name, value in ui.iteritems():
+        if not name.startswith('_'):
+            continue
+
+        data['@' + name] = value
+
+    return data
+
+
+def main_dict_to_ui(data):
+    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']
+    }
+
+    # additional configs
+    for name, value in data.iteritems():
+        if not name.startswith('@_'):
+            continue
+        
+        ui[name[1:]] = value
+
+    return ui
+
+
+def motion_camera_ui_to_dict(ui, old_config=None):
+    import smbctl
+    
+    old_config = dict(old_config or {})
+    main_config = get_main() # needed for surveillance password
+
+    data = {
+        # device
+        '@name': ui['name'],
+        '@enabled': ui['enabled'],
+        'lightswitch': int(ui['light_switch_detect']) * 50,
+        'auto_brightness': ui['auto_brightness'],
+        'framerate': int(ui['framerate']),
+        'rotate': int(ui['rotation']),
+        
+        # file storage
+        '@storage_device': ui['storage_device'],
+        '@network_server': ui['network_server'],
+        '@network_share_name': ui['network_share_name'],
+        '@network_username': ui['network_username'],
+        '@network_password': ui['network_password'],
+        
+        # text overlay
+        'text_left': '',
+        'text_right': '',
+        'text_double': False,
+        
+        # streaming
+        'stream_localhost': not ui['video_streaming'],
+        'stream_port': int(ui['streaming_port']),
+        'stream_maxrate': int(ui['streaming_framerate']),
+        'stream_quality': max(1, int(ui['streaming_quality'])),
+        '@webcam_resolution': max(1, int(ui['streaming_resolution'])),
+        '@webcam_server_resize': ui['streaming_server_resize'],
+        'stream_motion': ui['streaming_motion'],
+        'stream_auth_method': {'disabled': 0, 'basic': 1, 'digest': 2}.get(ui['streaming_auth_mode'], 0),
+        'stream_authentication': main_config['@normal_username'] + ':' + main_config['@normal_password'],
+
+        # still images
+        'output_pictures': False,
+        'emulate_motion': False,
+        'snapshot_interval': 0,
+        'picture_filename': '',
+        'snapshot_filename': '',
+        '@preserve_pictures': int(ui['preserve_pictures']),
+        
+        # motion detection
+        '@motion_detection': ui['motion_detection'],
+        'text_changes': ui['show_frame_changes'],
+        'locate_motion_mode': ui['show_frame_changes'],
+        'noise_tune': ui['auto_noise_detect'],
+        'noise_level': max(1, int(round(int(ui['noise_level']) * 2.55))),
+        'event_gap': int(ui['event_gap']),
+        'pre_capture': int(ui['pre_capture']),
+        'post_capture': int(ui['post_capture']),
+        'minimum_motion_frames': int(ui['minimum_motion_frames']),
+        
+        # movies
+        'ffmpeg_output_movies': ui['motion_movies'],
+        'movie_filename': ui['movie_file_name'],
+        'ffmpeg_bps': 44000, # a quality of about 85% for 320x240x2fps
+        'max_movie_time': ui['max_movie_length'],
+        '@preserve_movies': int(ui['preserve_movies']),
+    
+        # working schedule
+        '@working_schedule': '',
+    
+        # events
+        'on_event_start': '',
+        'on_event_end': ''
+    }
+    
+    if utils.v4l2_camera(old_config):
+        proto = 'v4l2'
+        
+    else:
+        proto = 'netcam'
+    
+    if proto == 'v4l2':
+        # leave videodevice unchanged
+        
+        # resolution
+        if not ui['resolution']:
+            ui['resolution'] = '320x240'
+
+        width = int(ui['resolution'].split('x')[0])
+        height = int(ui['resolution'].split('x')[1])
+        data['width'] = width
+        data['height'] = height
+        
+        threshold = int(float(ui['frame_change_threshold']) * width * height / 100)
+
+        if 'brightness' in ui:
+            if int(ui['brightness']) == 50:
+                data['brightness'] = 0
+                
+            else:
+                data['brightness'] = max(1, int(round(int(ui['brightness']) * 2.55)))
+        
+        if 'contrast' in ui:
+            if int(ui['contrast']) == 50:
+                data['contrast'] = 0
+                
+            else:
+                data['contrast'] = max(1, int(round(int(ui['contrast']) * 2.55)))
+        
+        if 'saturation' in ui:
+            if int(ui['saturation']) == 50:
+                data['saturation'] = 0
+                
+            else:
+                data['saturation'] = max(1, int(round(int(ui['saturation']) * 2.55)))
+            
+        if 'hue' in ui:
+            if int(ui['hue']) == 50:
+                data['hue'] = 0
+                
+            else:
+                data['hue'] = max(1, int(round(int(ui['hue']) * 2.55)))
+    
+    else: # assuming netcam
+        if data.get('netcam_url', old_config.get('netcam_url', '')).startswith('rtsp'):
+            # motion uses the configured width and height for RTSP cameras
+            width = int(ui['resolution'].split('x')[0])
+            height = int(ui['resolution'].split('x')[1])
+            data['width'] = width
+            data['height'] = height
+            
+            threshold = int(float(ui['frame_change_threshold']) * width * height / 100)
+        
+        else: # width & height are not available for other netcams
+            threshold = int(float(ui['frame_change_threshold']) * 640 * 480 / 100)
+
+    data['threshold'] = threshold
+
+    if (ui['storage_device'] == 'network-share') and settings.SMB_SHARES:
+        mount_point = smbctl.make_mount_point(ui['network_server'], ui['network_share_name'], ui['network_username'])
+        if ui['root_directory'].startswith('/'):
+            ui['root_directory'] = ui['root_directory'][1:]
+        data['target_dir'] = os.path.normpath(os.path.join(mount_point, ui['root_directory']))
+    
+    elif ui['storage_device'].startswith('local-disk'):
+        target_dev = ui['storage_device'][10:].replace('-', '/')
+        mounted_partitions = diskctl.list_mounted_partitions()
+        partition = mounted_partitions[target_dev]
+        mount_point = partition['mount_point']
+        
+        if ui['root_directory'].startswith('/'):
+            ui['root_directory'] = ui['root_directory'][1:]
+        data['target_dir'] = os.path.normpath(os.path.join(mount_point, ui['root_directory']))
+
+    else:
+        data['target_dir'] = ui['root_directory']
+
+    if ui['text_overlay']:
+        left_text = ui['left_text']
+        if left_text == 'camera-name':
+            data['text_left'] = ui['name']
+            
+        elif left_text == 'timestamp':
+            data['text_left'] = '%Y-%m-%d\\n%T'
+            
+        elif left_text == 'disabled':
+            data['text_left'] = ''
+            
+        else:
+            data['text_left'] = ui['custom_left_text']
+        
+        right_text = ui['right_text']
+        if right_text == 'camera-name':
+            data['text_right'] = ui['name']
+            
+        elif right_text == 'timestamp':
+            data['text_right'] = '%Y-%m-%d\\n%T'
+            
+        elif right_text == 'disabled':
+            data['text_right'] = ''
+            
+        else:
+            data['text_right'] = ui['custom_right_text']
+        
+        if proto == 'netcam' or data['width'] > 320:
+            data['text_double'] = True
+    
+    if ui['still_images']:
+        capture_mode = ui['capture_mode']
+        if capture_mode == 'motion-triggered':
+            data['output_pictures'] = True
+            data['picture_filename'] = ui['image_file_name']  
+            
+        elif capture_mode == 'interval-snapshots':
+            data['snapshot_interval'] = int(ui['snapshot_interval'])
+            data['snapshot_filename'] = ui['image_file_name']
+            
+        elif capture_mode == 'all-frames':
+            data['output_pictures'] = True
+            data['emulate_motion'] = True
+            data['picture_filename'] = ui['image_file_name']
+            
+        data['quality'] = max(1, int(ui['image_quality']))
+    
+    if proto == 'v4l2':
+        max_val = data['width'] * data['height'] * data['framerate'] / 3
+    
+    else: # always assume a netcam image size of 640x480, since we have no means to know it at this point
+        max_val = 640 * 480 * data['framerate'] / 3
+        
+    max_val = min(max_val, 9999999)
+    
+    data['ffmpeg_bps'] = int(ui['movie_quality']) * max_val / 100
+    
+    # working schedule
+    if ui['working_schedule']:
+        data['@working_schedule'] = (
+                ui['monday_from'] + '-' + ui['monday_to'] + '|' + 
+                ui['tuesday_from'] + '-' + ui['tuesday_to'] + '|' + 
+                ui['wednesday_from'] + '-' + ui['wednesday_to'] + '|' + 
+                ui['thursday_from'] + '-' + ui['thursday_to'] + '|' + 
+                ui['friday_from'] + '-' + ui['friday_to'] + '|' + 
+                ui['saturday_from'] + '-' + ui['saturday_to'] + '|' + 
+                ui['sunday_from'] + '-' + ui['sunday_to'])
+        
+        data['@working_schedule_type'] = ui['working_schedule_type']
+    
+    # event start
+    event_relay_path = os.path.join(settings.PROJECT_PATH, 'eventrelay.py')
+    event_relay_path = os.path.abspath(event_relay_path)
+        
+    on_event_start = ['%(script)s start %%t' % {'script': event_relay_path}]
+    if ui['email_notifications_enabled']:
+        send_mail_path = os.path.join(settings.PROJECT_PATH, 'sendmail.py')
+        send_mail_path = os.path.abspath(send_mail_path)
+        emails = re.sub('\\s', '', ui['email_notifications_addresses'])
+        
+        on_event_start.append("%(script)s '%(server)s' '%(port)s' '%(account)s' '%(password)s' '%(tls)s' '%(to)s' 'motion_start' '%%t' '%%Y-%%m-%%dT%%H:%%M:%%S' '%(timespan)s'" % {
+                'script': send_mail_path,
+                'server': ui['email_notifications_smtp_server'],
+                'port': ui['email_notifications_smtp_port'],
+                'account': ui['email_notifications_smtp_account'],
+                'password': ui['email_notifications_smtp_password'],
+                'tls': ui['email_notifications_smtp_tls'],
+                'to': emails,
+                'timespan': ui['email_notifications_picture_time_span']})
+
+    if ui['web_hook_notifications_enabled']:
+        web_hook_path = os.path.join(settings.PROJECT_PATH, 'webhook.py')
+        web_hook_path = os.path.abspath(web_hook_path)
+        url = re.sub('\\s', '+', ui['web_hook_notifications_url'])
+
+        on_event_start.append("%(script)s '%(method)s' '%(url)s'" % {
+                'script': web_hook_path,
+                'method': ui['web_hook_notifications_http_method'],
+                'url': url})
+
+    if ui['command_notifications_enabled']:
+        commands = ui['command_notifications_exec'].split(';')
+        on_event_start += [c.strip() for c in commands]
+
+    data['on_event_start'] = '; '.join(on_event_start)
+
+    # event end
+    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
+        
+    # extra motion options
+    for name in old_config.keys():
+        if name not in _KNOWN_MOTION_OPTIONS and not name.startswith('@'):
+            old_config.pop(name)
+
+    extra_options = ui.get('extra_options', [])
+    for name, value in extra_options:
+        data[name] = value or ''
+
+    old_config.update(data)
+
+    return old_config
+
+
+def motion_camera_dict_to_ui(data):
+    import smbctl
+    
+    ui = {
+        # device
+        'name': data['@name'],
+        'enabled': data['@enabled'],
+        'id': data['@id'],
+        'light_switch_detect': data['lightswitch'] > 0,
+        'auto_brightness': data['auto_brightness'],
+        'framerate': int(data['framerate']),
+        'rotation': int(data['rotate']),
+        
+        # file storage
+        'smb_shares': settings.SMB_SHARES,
+        'storage_device': data['@storage_device'],
+        'network_server': data['@network_server'],
+        'network_share_name': data['@network_share_name'],
+        'network_username': data['@network_username'],
+        'network_password': data['@network_password'],
+        'disk_used': 0,
+        'disk_total': 0,
+        'available_disks': diskctl.list_mounted_disks(),
+
+        # text overlay
+        'text_overlay': False,
+        'left_text': 'camera-name',
+        'right_text': 'timestamp',
+        'custom_left_text': '',
+        'custom_right_text': '',
+        
+        # streaming
+        'video_streaming': not data['stream_localhost'],
+        'streaming_framerate': int(data['stream_maxrate']),
+        'streaming_quality': int(data['stream_quality']),
+        'streaming_resolution': int(data['@webcam_resolution']),
+        'streaming_server_resize': data['@webcam_server_resize'],
+        'streaming_port': int(data['stream_port']),
+        'streaming_auth_mode': {0: 'disabled', 1: 'basic', 2: 'digest'}.get(data.get('stream_auth_method'), 'disabled'),
+        'streaming_motion': int(data['stream_motion']),
+        
+        # still images
+        'still_images': False,
+        'capture_mode': 'motion-triggered',
+        'image_file_name': '%Y-%m-%d/%H-%M-%S',
+        'image_quality': 85,
+        'snapshot_interval': 0,
+        'preserve_pictures': data['@preserve_pictures'],
+        
+        # motion detection
+        'motion_detection': data['@motion_detection'],
+        'show_frame_changes': data['text_changes'] or data['locate_motion_mode'],
+        'auto_noise_detect': data['noise_tune'],
+        'noise_level': int(int(data['noise_level']) / 2.55),
+        'event_gap': int(data['event_gap']),
+        'pre_capture': int(data['pre_capture']),
+        'post_capture': int(data['post_capture']),
+        'minimum_motion_frames': int(data['minimum_motion_frames']),
+        
+        # motion movies
+        'motion_movies': data['ffmpeg_output_movies'],
+        'movie_file_name': data['movie_filename'],
+        'max_movie_length': data['max_movie_time'],
+        'preserve_movies': data['@preserve_movies'],
+
+        # motion notifications
+        'email_notifications_enabled': False,
+        'web_hook_notifications_enabled': False,
+        'command_notifications_enabled': False,
+        
+        # working schedule
+        'working_schedule': False,
+        'working_schedule_type': 'during',
+        'monday_from': '', 'monday_to': '',
+        'tuesday_from': '', 'tuesday_to': '',
+        'wednesday_from': '', 'wednesday_to': '',
+        'thursday_from': '', 'thursday_to': '',
+        'friday_from': '', 'friday_to': '',
+        'saturday_from': '', 'saturday_to': '',
+        'sunday_from': '', 'sunday_to': ''
+    }
+    
+    if utils.net_camera(data):
+        ui['device_url'] = data['netcam_url']
+        ui['proto'] = 'netcam'
+
+        # resolutions
+        if data['netcam_url'].startswith('rtsp'):
+            # motion uses the configured width and height for RTSP cameras
+            resolutions = utils.COMMON_RESOLUTIONS
+            ui['available_resolutions'] = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
+            ui['resolution'] = str(data['width']) + 'x' + str(data['height'])
+
+            threshold = data['threshold'] * 100.0 / (data['width'] * data['height'])
+
+        else: # width & height are not available for other netcams
+            # we have no other choice but use something like 640x480 as reference
+            threshold = data['threshold'] * 100.0 / (640 * 480)
+
+    else: # assuming v4l2
+        ui['device_url'] = data['videodevice']
+        ui['proto'] = 'v4l2'
+
+        # resolutions
+        resolutions = v4l2ctl.list_resolutions(data['videodevice'])
+        ui['available_resolutions'] = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
+        ui['resolution'] = str(data['width']) + 'x' + str(data['height'])
+    
+        # the brightness & co. keys in the ui dictionary
+        # indicate the presence of these controls
+        # we must call v4l2ctl functions to determine the available controls    
+        brightness = v4l2ctl.get_brightness(data['videodevice'])
+        if brightness is not None: # has brightness control
+            if data.get('brightness', 0) != 0:
+                ui['brightness'] = brightness
+                    
+            else:
+                ui['brightness'] = 50
+
+        contrast = v4l2ctl.get_contrast(data['videodevice'])
+        if contrast is not None: # has contrast control
+            if data.get('contrast', 0) != 0:
+                ui['contrast'] = contrast
+            
+            else:
+                ui['contrast'] = 50
+            
+        saturation = v4l2ctl.get_saturation(data['videodevice'])
+        if saturation is not None: # has saturation control
+            if data.get('saturation', 0) != 0:
+                ui['saturation'] = saturation
+            
+            else:
+                ui['saturation'] = 50
+            
+        hue = v4l2ctl.get_hue(data['videodevice'])
+        if hue is not None: # has hue control
+            if data.get('hue', 0) != 0:
+                ui['hue'] = hue
+            
+            else:
+                ui['hue'] = 50
+        
+        threshold = data['threshold'] * 100.0 / (data['width'] * data['height'])
+        
+    ui['frame_change_threshold'] = threshold
+    
+    if (data['@storage_device'] == 'network-share') and settings.SMB_SHARES:
+        mount_point = smbctl.make_mount_point(data['@network_server'], data['@network_share_name'], data['@network_username'])
+        ui['root_directory'] = data['target_dir'][len(mount_point):] or '/'
+    
+    elif data['@storage_device'].startswith('local-disk'):
+        target_dev = data['@storage_device'][10:].replace('-', '/')
+        mounted_partitions = diskctl.list_mounted_partitions()
+        for partition in mounted_partitions.values():
+            if partition['target'] == target_dev and data['target_dir'].startswith(partition['mount_point']):
+                ui['root_directory'] = data['target_dir'][len(partition['mount_point']):] or '/'
+                break
+
+        else: # not found for some reason
+            logging.error('could not find mounted partition for device "%s" and target dir "%s"' % (target_dev, data['target_dir']))
+            ui['root_directory'] = data['target_dir']
+
+    else:
+        ui['root_directory'] = data['target_dir']
+
+    # disk usage
+    usage = utils.get_disk_usage(data['target_dir'])
+    if usage:
+        ui['disk_used'], ui['disk_total'] = usage
+    
+    text_left = data['text_left']
+    text_right = data['text_right'] 
+    if text_left or text_right:
+        ui['text_overlay'] = True
+        
+        if text_left == data['@name']:
+            ui['left_text'] = 'camera-name'
+            
+        elif text_left == '%Y-%m-%d\\n%T':
+            ui['left_text'] = 'timestamp'
+
+        elif text_left == '':
+            ui['left_text'] = 'disabled'
+            
+        else:
+            ui['left_text'] = 'custom-text'
+            ui['custom_left_text'] = text_left
+
+        if text_right == data['@name']:
+            ui['right_text'] = 'camera-name'
+            
+        elif text_right == '%Y-%m-%d\\n%T':
+            ui['right_text'] = 'timestamp'
+            
+        elif text_right == '':
+            ui['right_text'] = 'disabled'
+
+        else:
+            ui['right_text'] = 'custom-text'
+            ui['custom_right_text'] = text_right
+
+    emulate_motion = data['emulate_motion']
+    output_pictures = data['output_pictures']
+    picture_filename = data['picture_filename']
+    snapshot_interval = data['snapshot_interval']
+    snapshot_filename = data['snapshot_filename']
+    
+    if (((emulate_motion or output_pictures) and picture_filename) or
+        (snapshot_interval and snapshot_filename)):
+        
+        ui['still_images'] = True
+        
+        if emulate_motion:
+            ui['capture_mode'] = 'all-frames'
+            ui['image_file_name'] = picture_filename
+            
+        elif snapshot_interval:
+            ui['capture_mode'] = 'interval-snapshots'
+            ui['image_file_name'] = snapshot_filename
+            ui['snapshot_interval'] = snapshot_interval
+            
+        elif output_pictures:
+            ui['capture_mode'] = 'motion-triggered'
+            ui['image_file_name'] = picture_filename  
+            
+        ui['image_quality'] = data['quality']
+
+    ffmpeg_bps = data['ffmpeg_bps']
+    if ffmpeg_bps is not None:
+        if utils.v4l2_camera(data):
+            max_val = data['width'] * data['height'] * data['framerate'] / 3
+        
+        else: # net camera
+            max_val = 640 * 480 * data['framerate'] / 3
+            
+        max_val = min(max_val, 9999999)
+        
+        ui['movie_quality'] = min(100, int(round(ffmpeg_bps * 100.0 / max_val)))
+
+    # working schedule
+    working_schedule = data['@working_schedule']
+    if working_schedule:
+        days = working_schedule.split('|')
+        ui['working_schedule'] = True
+        ui['monday_from'], ui['monday_to'] = days[0].split('-')
+        ui['tuesday_from'], ui['tuesday_to'] = days[1].split('-')
+        ui['wednesday_from'], ui['wednesday_to'] = days[2].split('-')
+        ui['thursday_from'], ui['thursday_to'] = days[3].split('-')
+        ui['friday_from'], ui['friday_to'] = days[4].split('-')
+        ui['saturday_from'], ui['saturday_to'] = days[5].split('-')
+        ui['sunday_from'], ui['sunday_to'] = days[6].split('-')
+        ui['working_schedule_type'] = data['@working_schedule_type']
+    
+    # event start    
+    on_event_start = data.get('on_event_start') or []
+    if on_event_start:
+        on_event_start = [e.strip() for e in on_event_start.split(';')]
+
+    ui['email_notifications_picture_time_span'] = 0
+    command_notifications = []
+    for e in on_event_start:
+        if e.count('sendmail.py') and e.count('motion_start'):
+            e = shlex.split(e)
+            if len(e) < 10:
+                continue
+
+            ui['email_notifications_enabled'] = True 
+            ui['email_notifications_smtp_server'] = e[1]
+            ui['email_notifications_smtp_port'] = e[2]
+            ui['email_notifications_smtp_account'] = e[3]
+            ui['email_notifications_smtp_password'] = e[4]
+            ui['email_notifications_smtp_tls'] = e[5].lower() == 'true'
+            ui['email_notifications_addresses'] = e[6]
+            try:
+                ui['email_notifications_picture_time_span'] = int(e[10])
+                
+            except:
+                ui['email_notifications_picture_time_span'] = 0
+
+        elif e.count('webhook.py'):
+            e = shlex.split(e)
+            if len(e) != 3:
+                continue
+
+            ui['web_hook_notifications_enabled'] = True 
+            ui['web_hook_notifications_http_method'] = e[1]
+            ui['web_hook_notifications_url'] = e[2]
+        
+        elif e.count('eventrelay.py'):
+            continue # ignore internal relay script
+
+        else: # custom command
+            command_notifications.append(e)
+    
+    if command_notifications: 
+        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
+    
+    # extra motion options
+    extra_options = []
+    for name, value in data.iteritems():
+        if name not in _KNOWN_MOTION_OPTIONS and not name.startswith('@'):
+            extra_options.append((name, value))
+
+    ui['extra_options'] = extra_options
+
+    return ui
+
+
+def simple_mjpeg_camera_ui_to_dict(ui, old_config=None):
+    old_config = dict(old_config or {})
+
+    data = {
+        # device
+        '@name': ui['name'],
+        '@enabled': ui['enabled'],
+    }
+    
+    # additional configs
+    for name, value in ui.iteritems():
+        if not name.startswith('_'):
+            continue
+
+        data['@' + name] = value
+        
+    old_config.update(data)
+
+    return old_config
+
+
+def simple_mjpeg_camera_dict_to_ui(data):
+    ui = {
+        'name': data['@name'],
+        'enabled': data['@enabled'],
+        'id': data['@id'],
+        'proto': 'mjpeg',
+        'url': data['@url']
+    }
+    
+    # additional configs
+    for name, value in data.iteritems():
+        if not name.startswith('@_'):
+            continue
+        
+        ui[name[1:]] = value
+    
+    return ui
+
+
+def backup():
+    logging.debug('generating config backup file')
+
+    if len(os.listdir(settings.CONF_PATH)) > 100:
+        logging.debug('config path "%s" appears to be a system-wide config directory, performing a selective backup' % settings.CONF_PATH)
+        cmd = 'cd "%s" && tar zc motion.conf thread-*.conf' % settings.CONF_PATH
+        try:
+            content = subprocess.check_output(cmd, shell=True)
+            logging.debug('backup file created (%s bytes)' % len(content))
+            
+            return content
+            
+        except Exception as e:
+            logging.error('backup failed: %s' % e, exc_info=True)
+            
+            return None
+
+    else:
+        logging.debug('config path "%s" appears to be a motion-specific config directory, performing a full backup' % settings.CONF_PATH)
+
+        cmd = 'cd "%s" && tar zc .' % settings.CONF_PATH
+        try:
+            content = subprocess.check_output(cmd, shell=True)
+            logging.debug('backup file created (%s bytes)' % len(content))
+            
+            return content
+            
+        except Exception as e:
+            logging.error('backup failed: %s' % e, exc_info=True)
+            
+            return None
+
+
+def restore(content):
+    global _main_config_cache
+    global _camera_config_cache
+    global _camera_ids_cache
+    global _additional_structure_cache
+    
+    logging.info('restoring config from backup file')
+
+    cmd = 'tar zxC "%s" || true' % settings.CONF_PATH
+
+    try:
+        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        msg = p.communicate(content)[0]
+        if msg:
+            logging.error('failed to restore configuration: %s' % msg)
+            return False
+
+        logging.debug('configuration restored successfully')
+
+        if settings.ENABLE_REBOOT:
+            def later():
+                powerctl.reboot()
+
+            IOLoop.instance().add_timeout(datetime.timedelta(seconds=2), later)
+
+        else:
+            logging.info('invalidating config cache')
+            invalidate()
+
+        return {'reboot': settings.ENABLE_REBOOT}
+
+    except Exception as e:
+        logging.error('failed to restore configuration: %s' % e, exc_info=True)
+
+        return None
+
+
+def is_old_motion():
+    import motionctl
+    
+    try:
+        binary, version = motionctl.find_motion()  # @UnusedVariable
+        
+        if version.startswith('trunkREV'): # e.g. trunkREV599
+            version = int(version[8:])
+            return version <= _LAST_OLD_CONFIG_VERSIONS[0]
+        
+        elif version.count('Git'): # e.g. Unofficial-Git-a5b5f13
+            return False # all git versions are assumed to be new
+        
+        else: # stable release, should be in the format x.y.z
+            return update.compare_versions(version, _LAST_OLD_CONFIG_VERSIONS[1]) <= 0
+
+    except:
+        return False
+
+
+def motion_rtsp_support():
+    import motionctl
+
+    try:
+        binary, version = motionctl.find_motion()  # @UnusedVariable
+        
+        if version.startswith('trunkREV'): # e.g. trunkREV599
+            version = int(version[8:])
+            if version > _LAST_OLD_CONFIG_VERSIONS[0]:
+                return ['tcp']
+        
+        elif version.count('Git'): # e.g. Unofficial-Git-a5b5f13
+            return ['tcp', 'udp'] # all git versions are assumed to support both transport protocols
+        
+        else: # stable release, should be in the format x.y.z
+            return []
+
+    except:
+        return []
+
+
+def invalidate():
+    global _main_config_cache
+    global _camera_config_cache
+    global _camera_ids_cache
+    global _additional_structure_cache
+
+    logging.debug('invalidating config cache')    
+    _main_config_cache = None
+    _camera_config_cache = {}
+    _camera_ids_cache = None
+    _additional_structure_cache = {}
+
+
+def _value_to_python(value):
+    value_lower = value.lower()
+    if value_lower == 'off':
+        return False
+    
+    elif value_lower == 'on':
+        return True
+    
+    try:
+        return int(value)
+    
+    except ValueError:
+        try:
+            return float(value)
+        
+        except ValueError:
+            return value
+
+
+def _python_to_value(value):
+    if value is True:
+        return 'on'
+    
+    elif value is False:
+        return 'off'
+    
+    elif isinstance(value, (int, float)):
+        return str(value)
+    
+    else:
+        return value
+
+
+def _conf_to_dict(lines, list_names=[], no_convert=[]):
+    data = OrderedDict()
+    
+    for line in lines:
+        line = line.strip()
+        if len(line) == 0:  # empty line
+            continue
+        
+        if line.startswith(';'):  # comment line
+            continue
+        
+        match = re.match('^\#\s*(\@\w+)\s*(.*)', line)
+        if match:
+            name, value = match.groups()[:2]
+        
+        elif line.startswith('#') or line.startswith(';'): # comment line
+            continue
+
+        else:
+            parts = line.split(None, 1)
+            if len(parts) == 1: # empty value
+                parts.append('')
+
+            (name, value) = parts
+            value = value.strip()
+        
+        if name not in no_convert:
+            value = _value_to_python(value)
+        
+        if name in list_names:
+            data.setdefault(name, []).append(value)
+        
+        else:
+            data[name] = value
+
+    return data
+
+
+def _dict_to_conf(lines, data, list_names=[]):
+    conf_lines = []
+    remaining = OrderedDict(data)
+    processed = set()
+    
+    # parse existing lines and replace the values
+    
+    for line in lines:
+        line = line.strip()
+        if len(line) == 0:  # empty line
+            conf_lines.append(line)
+            continue
+
+        if line.startswith(';'):  # simple comment line
+            conf_lines.append(line)
+            continue
+        
+        match = re.match('^\#\s*(\@\w+)\s*(.*)', line)
+        if match: # @line
+            (name, value) = match.groups()[:2]
+        
+        elif line.startswith('#'):  # simple comment line
+            conf_lines.append(line)
+            continue
+        
+        else:
+            parts = line.split(None, 1)
+            if len(parts) == 2:
+                (name, value) = parts
+            
+            else:
+                (name, value) = parts[0], ''
+        
+        if name in processed:
+            continue # name already processed
+        
+        processed.add(name)
+        
+        if name in list_names:
+            new_value = data.get(name)
+            if new_value is not None:
+                for v in new_value:
+                    if v is None:
+                        continue
+
+                    line = name + ' ' + _python_to_value(v)
+                    conf_lines.append(line)
+            
+            else:
+                line = name + ' ' + value
+                conf_lines.append(line)
+
+        else:
+            new_value = data.get(name)
+            if new_value is not None:
+                value = _python_to_value(new_value)
+                line = name + ' ' + value
+                conf_lines.append(line)
+
+        remaining.pop(name, None)
+    
+    # add the remaining config values not covered by existing lines
+    
+    if len(remaining) and len(lines):
+        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:
+                if v is None:
+                    continue
+
+                line = name + ' ' + _python_to_value(v)
+                conf_lines.append(line)
+
+        else:
+            line = name + ' ' + _python_to_value(value)
+            conf_lines.append(line)
+            
+    # build the final config lines
+    conf_lines.sort(key=lambda l: not l.startswith('@'))
+    
+    lines = []
+    for i, line in enumerate(conf_lines):
+        # squeeze successive blank lines
+        if i > 0 and len(line.strip()) == 0 and len(conf_lines[i - 1].strip()) == 0:
+            continue
+        
+        if line.startswith('@'):
+            line = '# ' + line
+        
+        elif i > 0 and conf_lines[i - 1].startswith('@'):
+            lines.append('') # add a blank line between @lines and the rest
+        
+        lines.append(line)
+        
+    return lines
+
+
+def _set_default_motion(data, old_motion):
+    data.setdefault('@enabled', True)
+
+    data.setdefault('@show_advanced', False)
+    data.setdefault('@admin_username', 'admin')
+    data.setdefault('@admin_password', '')
+    data.setdefault('@normal_username', 'user')
+    data.setdefault('@normal_password', '')
+
+    if old_motion:
+        data.setdefault('control_port', 7999)
+    
+    else:
+        data.setdefault('webcontrol_port', 7999)
+
+
+def _set_default_motion_camera(camera_id, data, old_motion=False):
+    data.setdefault('@name', 'Camera' + str(camera_id))
+    data.setdefault('@enabled', False)
+    data.setdefault('@id', camera_id)
+    
+    if not utils.net_camera(data):
+        data.setdefault('videodevice', '/dev/video0')
+        data.setdefault('brightness', 0)
+        data.setdefault('contrast', 0)
+        data.setdefault('saturation', 0)
+        data.setdefault('hue', 0)
+        data.setdefault('width', 352)
+        data.setdefault('height', 288)
+
+    data.setdefault('lightswitch', 50)
+    data.setdefault('auto_brightness', False)
+    data.setdefault('framerate', 2)
+    data.setdefault('rotate', 0)
+    
+    data.setdefault('@storage_device', 'custom-path')
+    data.setdefault('@network_server', '')
+    data.setdefault('@network_share_name', '')
+    data.setdefault('@network_username', '')
+    data.setdefault('@network_password', '')
+    data.setdefault('target_dir', settings.MEDIA_PATH)
+    
+    if old_motion:
+        data.setdefault('webcam_localhost', False)
+        data.setdefault('webcam_port', int('808' + str(camera_id)))
+        data.setdefault('webcam_maxrate', 5)
+        data.setdefault('webcam_quality', 85)
+        data.setdefault('webcam_motion', False)
+    
+    else:
+        data.setdefault('stream_localhost', False)
+        data.setdefault('stream_port', int('808' + str(camera_id)))
+        data.setdefault('stream_maxrate', 5)
+        data.setdefault('stream_quality', 85)
+        data.setdefault('stream_motion', False)
+        data.setdefault('stream_auth_method', 0)
+
+    data.setdefault('@webcam_resolution', 100)
+    data.setdefault('@webcam_server_resize', False)
+    
+    data.setdefault('text_left', data['@name'])
+    data.setdefault('text_right', '%Y-%m-%d\\n%T')
+    data.setdefault('text_double', False)
+
+    data.setdefault('@motion_detection', True)
+    data.setdefault('text_changes', False)
+    if old_motion:
+        data.setdefault('locate', False)
+    
+    else:
+        data.setdefault('locate_motion_mode', False)
+        data.setdefault('locate_motion_style', 'redbox')
+    
+    data.setdefault('threshold', 2000)
+    data.setdefault('noise_tune', True)
+    data.setdefault('noise_level', 32)
+    data.setdefault('minimum_motion_frames', 1)
+    
+    data.setdefault('pre_capture', 2)
+    data.setdefault('post_capture', 4)
+    data.setdefault('minimum_motion_frames', 1)
+    
+    if old_motion:
+        data.setdefault('output_normal', False)
+        data.setdefault('jpeg_filename', '')
+        data.setdefault('output_all', False)
+        data.setdefault('gap', 10)
+
+    else:
+        data.setdefault('output_pictures', False)
+        data.setdefault('picture_filename', '')
+        data.setdefault('emulate_motion', False)
+        data.setdefault('event_gap', 10)
+    
+    data.setdefault('snapshot_interval', 0)
+    data.setdefault('snapshot_filename', '')
+    data.setdefault('quality', 85)
+    data.setdefault('@preserve_pictures', 0)
+    
+    data.setdefault('ffmpeg_variable_bitrate', 0)
+    data.setdefault('ffmpeg_bps', 44000) # a quality of about 85% 
+    data.setdefault('movie_filename', '%Y-%m-%d/%H-%M-%S')
+    if old_motion:
+        data.setdefault('max_mpeg_time', 0)
+        data.setdefault('ffmpeg_cap_new', False)
+    
+    else:
+        data.setdefault('max_movie_time', 0)
+        data.setdefault('ffmpeg_output_movies', False)
+    data.setdefault('ffmpeg_video_codec', 'msmpeg4')
+    data.setdefault('@preserve_movies', 0)
+    
+    data.setdefault('@working_schedule', '')
+    data.setdefault('@working_schedule_type', 'outside')
+
+    data.setdefault('on_event_start', '')
+    data.setdefault('on_event_end', '')
+
+
+def _set_default_simple_mjpeg_camera(camera_id, data):
+    data.setdefault('@name', 'Camera' + str(camera_id))
+    data.setdefault('@enabled', False)
+    data.setdefault('@id', camera_id)
+
+    
+def get_additional_structure(camera, separators=False):
+    if _additional_structure_cache.get((camera, separators)) is None:
+        logging.debug('loading additional config structure for %s, %s separators' % (
+                'camera' if camera else 'main',
+                'with' if separators else 'without'))
+
+        # 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
+            
+            result['name'] = func.func_name
+            sections[func.func_name] = result
+            
+            logging.debug('additional config section: %s' % result['name'])
+    
+        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
+
+            if result['type'] == 'separator' and not separators:
+                continue
+
+            result['name'] = func.func_name
+            configs[func.func_name] = result
+    
+            section = sections.setdefault(result.get('section'), {})
+            section.setdefault('configs', []).append(result)
+            
+            logging.debug('additional config item: %s' % result['name'])
+
+        _additional_structure_cache[(camera, separators)] = sections, configs
+
+    return _additional_structure_cache[(camera, separators)]
+
+
+def _get_additional_config(data, camera_id=None):
+    args = [camera_id] if camera_id else []
+    
+    (sections, configs) = get_additional_structure(camera=bool(camera_id))
+    get_funcs = set([c.get('get') for c in configs.itervalues() if c.get('get')])
+    get_func_values = collections.OrderedDict((f, f(*args)) 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_id=None):
+    args = [camera_id] if camera_id else []
+
+    (sections, configs) = get_additional_structure(camera=bool(camera_id))
+    
+    set_func_values = collections.OrderedDict()
+    for name, section in sections.iteritems():
+        if not section.get('set'):
+            continue
+        
+        if ('@_' + name) not in data:
+            continue
+
+        if section.get('get_set_dict'):
+            set_func_values.setdefault(section['set'], {})[name] = data['@_' + name]
+
+        else:
+            set_func_values[section['set']] = data['@_' + name]
+
+    for name, config in configs.iteritems():
+        if not config.get('set'):
+            continue
+
+        if ('@_' + name) not in data:
+            continue
+
+        if config.get('get_set_dict'):
+            set_func_values.setdefault(config['set'], {})[name] = data['@_' + name]
+            
+        else:
+            set_func_values[config['set']] = data['@_' + name]
+
+    for func, value in set_func_values.iteritems():
+        func(*(args + [value]))
diff --git a/motioneye/diskctl.py b/motioneye/diskctl.py
new file mode 100644 (file)
index 0000000..b4cd738
--- /dev/null
@@ -0,0 +1,259 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import logging
+import os
+import re
+import subprocess
+
+
+def _list_mounts():
+    logging.debug('listing mounts...')
+    
+    seen_targets = set()
+    
+    mounts = []
+    with open('/proc/mounts', 'r') as f:
+        for line in f:
+            line = line.strip()
+            if not line:
+                continue
+            parts = line.split()
+            if len(parts) < 4:
+                continue
+            
+            target = parts[0]
+            mount_point = parts[1]
+            fstype = parts[2]
+            opts = parts[3]
+            
+            if not os.access(mount_point, os.W_OK):
+                continue
+            
+            if target in seen_targets:
+                continue # probably a bind mount
+            
+            seen_targets.add(target)
+
+            if fstype == 'fuseblk':
+                fstype = 'ntfs' # most likely
+            
+            logging.debug('found mount "%s" at "%s"' % (target, mount_point))
+            
+            mounts.append({
+                'target': target,
+                'mount_point': mount_point,
+                'fstype': fstype,
+                'opts': opts,
+            })
+
+    return mounts
+
+
+def _list_disks():
+    if os.path.exists('/dev/disk/by-id/'):
+        return _list_disks_dev_by_id()
+    
+    else:  # fall back to fdisk -l
+        return _list_disks_fdisk()
+
+
+def _list_disks_dev_by_id():
+    logging.debug('listing disks using /dev/disk/by-id/')
+    
+    disks_by_dev = {}
+    partitions_by_dev = {}
+
+    for entry in os.listdir('/dev/disk/by-id/'):
+        parts = entry.split('-', 1)
+        if len(parts) < 2:
+            continue
+        
+        target = os.path.realpath(os.path.join('/dev/disk/by-id/', entry))
+        
+        bus, entry = parts
+        m = re.search('-part(\d+)$', entry)
+        if m:
+            part_no = int(m.group(1))
+            entry = re.sub('-part\d+$', '', entry)
+        
+        else:
+            part_no = None
+
+        parts = entry.split('_')
+        if len(parts) < 2:
+            vendor = parts[0]
+            model = ''
+        
+        else:
+            vendor, model = parts[:2]
+
+        if part_no is not None:
+            logging.debug('found partition "%s" at "%s" on bus "%s": "%s %s"' % (part_no, target, bus, vendor, model))
+        
+            partitions_by_dev[target] = {
+                'target': target,
+                'bus': bus,
+                'vendor': vendor,
+                'model': model,
+                'part_no': part_no,
+                'unmatched': True
+            }
+            
+        else:
+            logging.debug('found disk at "%s" on bus "%s": "%s %s"' % (target, bus, vendor, model))
+
+            disks_by_dev[target] = {
+                'target': target,
+                'bus': bus,
+                'vendor': vendor,
+                'model': model,
+                'partitions': []
+            }
+        
+    # group partitions by disk
+    for dev, partition in partitions_by_dev.items():
+        for disk_dev, disk in disks_by_dev.items():
+            if dev.startswith(disk_dev):
+                disk['partitions'].append(partition)
+                partition.pop('unmatched')
+            
+    # add separate partitions that did not match any disk
+    for partition in partitions_by_dev.values():
+        if partition.pop('unmatched', False):
+            disks_by_dev[partition['target']] = partition
+            partition['partitions'] = [dict(partition)]
+
+    # prepare flat list of disks
+    disks = disks_by_dev.values()
+    disks.sort(key=lambda d: d['vendor'])
+    
+    for disk in disks:
+        disk['partitions'].sort(key=lambda p: p['part_no'])
+
+    return disks
+
+
+def _list_disks_fdisk():
+    try:
+        output = subprocess.check_output('fdisk -l 2>/dev/null', shell=True)
+    
+    except Exception as e:
+        logging.error('failed to list disks using "fdisk -l": %s' % e, exc_info=True)
+        
+        return []
+
+    disks = []
+    disk = None
+    
+    def add_disk(disk):
+        logging.debug('found disk at "%s" on bus "%s": "%s %s"' %
+                (disk['target'], disk['bus'], disk['vendor'], disk['model']))
+
+        for part in disk['partitions']:
+            logging.debug('found partition "%s" at "%s" on bus "%s": "%s %s"' %
+                    (part['part_no'], part['target'], part['bus'], part['vendor'], part['model']))
+
+        disks.append(disk)
+
+    for line in output.split('\n'):
+        line = line.replace('*', '')
+        line = re.sub('\s+', ' ', line.strip())
+        if not line:
+            continue
+
+        if line.startswith('Disk /dev/'):
+            if disk and disk['partitions']:
+                add_disk(disk)
+
+            parts = line.split()
+
+            disk = {
+                'target': parts[1].strip(':'),
+                'bus': '',
+                'vendor': '',
+                'model': parts[2] + ' ' + parts[3].strip(','),
+                'partitions': []
+            }
+            
+        elif line.startswith('/dev/') and disk:
+            parts = line.split()
+            part_no = re.findall('\d+$', parts[0])
+            partition = {
+                'part_no': int(part_no[0]) if part_no else None,
+                'target': parts[0],
+                'bus': '',
+                'vendor': '',
+                'model': parts[4] + ' ' + ' '.join(parts[6:]),
+            }
+            
+            disk['partitions'].append(partition)
+
+    if disk and disk['partitions']:
+        add_disk(disk)
+
+    disks.sort(key=lambda d: d['target'])
+
+    for disk in disks:
+        disk['partitions'].sort(key=lambda p: p['part_no'])
+
+    return disks
+
+
+def list_mounted_disks():
+    mounted_disks = []
+    
+    try:
+        disks = _list_disks()
+        mounts_by_target = dict((m['target'], m) for m in _list_mounts())
+        
+        for disk in disks:
+            for partition in disk['partitions']:
+                mount = mounts_by_target.get(partition['target'])
+                if mount:
+                    partition.update(mount) 
+        
+            # filter out unmounted partitions
+            disk['partitions'] = [p for p in disk['partitions'] if p.get('mount_point')]
+        
+        # filter out unmounted disks
+        mounted_disks = [d for d in disks if d['partitions']]
+
+    except Exception as e:
+        logging.error('failed to list mounted disks: %s' % e, exc_info=True)
+        
+    return mounted_disks
+
+
+def list_mounted_partitions():
+    mounted_partitions = {}
+
+    try:
+        disks = _list_disks()
+        mounts_by_target = dict((m['target'], m) for m in _list_mounts())
+        
+        for disk in disks:
+            for partition in disk['partitions']:
+                mount = mounts_by_target.get(partition['target'])
+                if mount:
+                    partition.update(mount)
+                    mounted_partitions[partition['target']] = partition 
+        
+    except Exception as e:
+        logging.error('failed to list mounted partitions: %s' % e, exc_info=True)
+        
+    return mounted_partitions
diff --git a/motioneye/eventrelay.py b/motioneye/eventrelay.py
new file mode 100755 (executable)
index 0000000..42c02f3
--- /dev/null
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import errno
+import json
+import logging
+import os.path
+import sys
+import urllib
+
+sys.path.append(os.path.join(os.path.dirname(sys.argv[0]),'src'))
+
+import settings
+import utils
+
+from motioneye import _configure_settings, _configure_logging
+
+
+_configure_settings()
+_configure_logging(module='eventrelay')
+
+
+def print_usage():
+    print 'Usage: eventrelay.py <event> <thread_id>'
+
+
+def get_admin_credentials():
+    # this shortcut function is a bit faster than using the config module functions
+    config_file_path = os.path.join(settings.CONF_PATH, 'motion.conf')
+    
+    logging.debug('reading main config from file %(path)s...' % {'path': config_file_path}) 
+
+    lines = None
+    try:
+        file = open(config_file_path, 'r')
+    
+    except IOError as e:
+        if e.errno == errno.ENOENT:  # file does not exist
+            logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
+            
+            lines = []
+        
+        else:
+            logging.error('could not open main config file %(path)s: %(msg)s' % {
+                    'path': config_file_path, 'msg': unicode(e)})
+            
+            raise
+
+    if lines is None:
+        try:
+            lines = [l[:-1] for l in file.readlines()]
+        
+        except Exception as e:
+            logging.error('could not read main config file %(path)s: %(msg)s' % {
+                    'path': config_file_path, 'msg': unicode(e)})
+            
+            raise
+        
+        finally:
+            file.close()
+    
+    admin_username = 'admin'
+    admin_password = ''
+    for line in lines:
+        line = line.strip()
+        if not line.startswith('#'):
+            continue
+        
+        line = line[1:].strip()
+        if line.startswith('@admin_username'):
+            parts = line.split(' ', 1)
+            admin_username = parts[1] if len(parts) > 1 else ''
+            
+            continue
+        
+        if line.startswith('@admin_password'):
+            parts = line.split(' ', 1)
+            admin_password = parts[1] if len(parts) > 1 else ''
+
+            continue
+    
+    return admin_username, admin_password
+
+
+# def compute_signature(method, uri, body, key):
+#     parts = list(urlparse.urlsplit(uri))
+#     query = [q for q in urlparse.parse_qsl(parts[3]) if (q[0] != 'signature')]
+#     query.sort(key=lambda q: q[0])
+#     query = urllib.urlencode(query)
+#     parts[0] = parts[1] = ''
+#     parts[3] = query
+#     uri = urlparse.urlunsplit(parts)
+#     
+#     return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower()
+
+
+if __name__ == '__main__':
+    if len(sys.argv) < 3:
+        print_usage()
+        sys.exit(-1)
+    
+    event = sys.argv[1] 
+    thread_id = sys.argv[2]
+
+    logging.debug('hello!')
+    logging.debug('event = %s' % event)
+    logging.debug('thread_id = %s' % thread_id)
+    
+    admin_username, admin_password = get_admin_credentials()
+
+    uri = '/_relay_event/?event=%(event)s&thread_id=%(thread_id)s&_username=%(username)s' % {
+            'username': admin_username,
+            'thread_id': thread_id,
+            'event': event}
+    
+    signature = utils.compute_signature('POST', uri, '', admin_password)
+    
+    url = 'http://127.0.0.1:%(port)s' + uri + '&_signature=' + signature
+    url = url % {'port': settings.PORT}
+    
+    try:
+        response = urllib.urlopen(url, data='')
+        response = json.load(response)
+        if response.get('error'):
+            raise Exception(response['error'])
+        
+        logging.debug('event successfully relayed')
+    
+    except Exception as e:
+        logging.error('failed to relay event: %s' % e)
+
+    logging.debug('bye!')
diff --git a/motioneye/handlers.py b/motioneye/handlers.py
new file mode 100644 (file)
index 0000000..120d481
--- /dev/null
@@ -0,0 +1,1466 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import json
+import logging
+import os
+import re
+import socket
+import subprocess
+
+from tornado.web import RequestHandler, HTTPError, asynchronous
+from tornado.ioloop import IOLoop
+
+import config
+import mediafiles
+import motionctl
+import powerctl
+import remote
+import settings
+import smbctl
+import template
+import update
+import utils
+import v4l2ctl
+
+
+class BaseHandler(RequestHandler):
+    def get_data(self):
+        keys = self.request.arguments.keys()
+        data = dict([(key, self.get_argument(key)) for key in keys])
+
+        for key in self.request.files:
+            files = self.request.files[key]
+            if len(files) > 1:
+                data[key] = files
+
+            elif len(files) > 0:
+                data[key] = files[0]
+
+            else:
+                continue
+
+        return data
+    
+    def render(self, template_name, content_type='text/html', **context):
+        self.set_header('Content-Type', content_type)
+        
+        content = template.render(template_name, **context)
+        self.finish(content)
+    
+    def finish_json(self, data={}):
+        self.set_header('Content-Type', 'application/json')
+        self.finish(json.dumps(data))
+
+    def get_current_user(self):
+        main_config = config.get_main()
+        
+        username = self.get_argument('_username', None)
+        signature = self.get_argument('_signature', None)
+        login = self.get_argument('_login', None) == 'true'
+        if (username == main_config.get('@admin_username') and
+            signature == utils.compute_signature(self.request.method, self.request.uri, self.request.body, main_config.get('@admin_password'))):
+            
+            return 'admin'
+        
+        elif not username and not main_config.get('@normal_password'): # no authentication required for normal user
+            return 'normal'
+        
+        elif (username == main_config.get('@normal_username') and
+            signature == utils.compute_signature(self.request.method, self.request.uri, self.request.body, main_config.get('@normal_password'))):
+            
+            return 'normal'
+
+        elif username and username != '_' and login:
+            logging.error('authentication failed for user %(user)s' % {'user': username})
+
+        return None
+        
+    def _handle_request_exception(self, exception):
+        try:
+            if isinstance(exception, HTTPError):
+                logging.error(str(exception))
+                self.set_status(exception.status_code)
+                self.finish_json({'error': exception.log_message or getattr(exception, 'reason', None) or str(exception)})
+            
+            else:
+                logging.error(str(exception), exc_info=True)
+                self.set_status(500)
+                self.finish_json({'error':  'internal server error'})
+                
+        except RuntimeError:
+            pass # nevermind
+        
+    @staticmethod
+    def auth(admin=False, prompt=True):
+        def decorator(func):
+            def wrapper(self, *args, **kwargs):
+                _admin = self.get_argument('_admin', None) == 'true'
+                
+                user = self.current_user
+                if (user is None) or (user != 'admin' and (admin or _admin)):
+                    self.set_header('Content-Type', 'application/json')
+
+                    return self.finish_json({'error': 'unauthorized', 'prompt': prompt})
+
+                return func(self, *args, **kwargs)
+            
+            return wrapper
+        
+        return decorator
+
+    def get(self, *args, **kwargs):
+        raise HTTPError(400, 'method not allowed')
+
+    def post(self, *args, **kwargs):
+        raise HTTPError(400, 'method not allowed')
+
+
+class NotFoundHandler(BaseHandler):
+    def get(self):
+        raise HTTPError(404, 'not found')
+
+    def post(self):
+        raise HTTPError(404, 'not found')
+
+
+class MainHandler(BaseHandler):
+    def get(self):
+        import motioneye
+        
+        # additional config
+        main_sections = config.get_additional_structure(camera=False, separators=True)[0]
+        camera_sections = config.get_additional_structure(camera=True, separators=True)[0]
+
+        self.render('main.html',
+                frame=False,
+                version=motioneye.VERSION,
+                enable_update=False,
+                enable_reboot=settings.ENABLE_REBOOT,
+                add_remove_cameras=settings.ADD_REMOVE_CAMERAS,
+                main_sections=main_sections,
+                camera_sections=camera_sections,
+                hostname=socket.gethostname(),
+                title=self.get_argument('title', None),
+                admin_username=config.get_main().get('@admin_username'),
+                old_motion=config.is_old_motion())
+
+
+class ConfigHandler(BaseHandler):
+    @asynchronous
+    def get(self, camera_id=None, op=None):
+        if camera_id is not None:
+            camera_id = int(camera_id)
+        
+        if op == 'get':
+            self.get_config(camera_id)
+            
+        elif op == 'list':
+            self.list()
+        
+        elif op == 'backup':
+            self.backup()
+        
+        else:
+            raise HTTPError(400, 'unknown operation')
+    
+    @asynchronous
+    def post(self, camera_id=None, op=None):
+        if camera_id is not None:
+            camera_id = int(camera_id)
+        
+        if op == 'set':
+            self.set_config(camera_id)
+        
+        elif op == 'set_preview':
+            self.set_preview(camera_id)
+        
+        elif op == 'add':
+            self.add_camera()
+        
+        elif op == 'rem':
+            self.rem_camera(camera_id)
+            
+        elif op == 'restore':
+            self.restore()
+        
+        else:
+            raise HTTPError(400, 'unknown operation')
+    
+    @BaseHandler.auth(admin=True)
+    def get_config(self, camera_id):
+        if camera_id:
+            logging.debug('getting config for camera %(id)s' % {'id': camera_id})
+            
+            if camera_id not in config.get_camera_ids():
+                raise HTTPError(404, 'no such camera')
+            
+            local_config = config.get_camera(camera_id)
+            if utils.local_motion_camera(local_config):
+                ui_config = config.motion_camera_dict_to_ui(local_config)
+                    
+                self.finish_json(ui_config)
+            
+            elif utils.remote_camera(local_config):
+                def on_response(remote_ui_config=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to get remote camera configuration for %(url)s: %(msg)s.' % {
+                                'url': remote.pretty_camera_url(local_config), 'msg': error}})
+                    
+                    for key, value in local_config.items():
+                        remote_ui_config[key.replace('@', '')] = value
+                    
+                    # replace the real device URI with the remote camera URL
+                    remote_ui_config['device_url'] = remote.pretty_camera_url(local_config)
+                    self.finish_json(remote_ui_config)
+                
+                remote.get_config(local_config, on_response)
+                
+            else: # assuming simple mjpeg camera
+                ui_config = config.simple_mjpeg_camera_dict_to_ui(local_config)
+                    
+                self.finish_json(ui_config)
+            
+        else:
+            logging.debug('getting main config')
+            
+            ui_config = config.main_dict_to_ui(config.get_main())
+            self.finish_json(ui_config)
+    
+    @BaseHandler.auth(admin=True)
+    def set_config(self, camera_id):
+        try:
+            ui_config = json.loads(self.request.body)
+            
+        except Exception as e:
+            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+            
+            raise
+        
+        camera_ids = config.get_camera_ids()
+        
+        def set_camera_config(camera_id, ui_config, on_finish):
+            logging.debug('setting config for camera %(id)s...' % {'id': camera_id})
+            
+            if camera_id not in camera_ids:
+                raise HTTPError(404, 'no such camera')
+            
+            local_config = config.get_camera(camera_id)
+            if utils.local_motion_camera(local_config):
+                local_config = config.motion_camera_ui_to_dict(ui_config, local_config)
+
+                config.set_camera(camera_id, local_config)
+            
+                on_finish(None, True) # (no error, motion needs restart)
+
+            elif utils.remote_camera(local_config):
+                # update the camera locally
+                local_config['@enabled'] = ui_config['enabled']
+                config.set_camera(camera_id, local_config)
+                
+                if ui_config.has_key('name'):
+                    def on_finish_wrapper(error=None):
+                        return on_finish(error, False)
+                    
+                    ui_config['enabled'] = True # never disable the camera remotely 
+                    remote.set_config(local_config, ui_config, on_finish_wrapper)
+                
+                else:
+                    # when the ui config supplied has only the enabled state
+                    # and no useful fields (such as "name"),
+                    # the camera was probably disabled due to errors
+                    on_finish(None, False)
+                    
+            else: # assuming simple mjpeg camera
+                local_config = config.simple_mjpeg_camera_ui_to_dict(ui_config, local_config)
+
+                config.set_camera(camera_id, local_config)
+            
+                on_finish(None, False) # (no error, motion doesn't need restart)
+
+        def set_main_config(ui_config):
+            logging.debug('setting main config...')
+            
+            old_main_config = config.get_main()
+            old_admin_credentials = '%s:%s' % (old_main_config.get('@admin_username', ''), old_main_config.get('@admin_password', ''))
+            old_normal_credentials = '%s:%s' % (old_main_config.get('@normal_username', ''), old_main_config.get('@normal_password', ''))
+
+            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', ''))
+            normal_credentials = '%s:%s' % (main_config.get('@normal_username', ''), main_config.get('@normal_password', ''))
+
+            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_config_names.append('@admin_password')
+            reboot = bool([k for k in reboot_config_names if old_main_config.get(k) != main_config.get(k)])
+
+            config.set_main(main_config)
+            
+            reload = False
+            restart = False
+            
+            if admin_credentials != old_admin_credentials:
+                logging.debug('admin credentials changed, reload needed')
+                
+                reload = True
+
+            if normal_credentials != old_normal_credentials:
+                logging.debug('surveillance credentials changed, all camera configs must be updated')
+                
+                # reconfigure all local cameras to update the stream authentication options
+                for camera_id in config.get_camera_ids():
+                    local_config = config.get_camera(camera_id)
+                    if not utils.local_motion_camera(local_config):
+                        continue
+                    
+                    ui_config = config.motion_camera_dict_to_ui(local_config)
+                    local_config = config.motion_camera_ui_to_dict(ui_config, local_config)
+
+                    config.set_camera(camera_id, local_config)
+                    
+                    restart = True
+
+            if reboot and settings.ENABLE_REBOOT:
+                logging.debug('system settings changed, reboot needed')
+        
+            else: 
+                reboot = False
+
+            return {'reload': reload, 'reboot': reboot, 'restart': restart}
+        
+        reload = False # indicates that browser should reload the page
+        reboot = [False] # indicates that the server will reboot immediately
+        restart = [False]  # indicates that the local motion instance was modified and needs to be restarted
+        error = [None]
+        
+        def finish():
+            if reboot[0]:
+                if settings.ENABLE_REBOOT:
+                    def call_reboot():
+                        powerctl.reboot()
+                    
+                    ioloop = IOLoop.instance()
+                    ioloop.add_timeout(datetime.timedelta(seconds=2), call_reboot)
+                    return self.finish({'reload': False, 'reboot': True, 'error': None})
+                
+                else:
+                    reboot[0] = False
+
+            if restart[0]:
+                logging.debug('motion needs to be restarted')
+                
+                motionctl.stop()
+                
+                if settings.SMB_SHARES:
+                    logging.debug('updating SMB mounts')
+                    stop, start = smbctl.update_mounts()  # @UnusedVariable
+
+                    if start:
+                        motionctl.start()
+                
+                else:
+                    motionctl.start()
+
+            self.finish({'reload': reload, 'reboot': reboot[0], 'error': error[0]})
+        
+        if camera_id is not None:
+            if camera_id == 0: # multiple camera configs
+                if len(ui_config) > 1:
+                    logging.debug('setting multiple configs')
+                
+                elif len(ui_config) == 0:
+                    logging.warn('no configuration to set')
+                    
+                    self.finish()
+                
+                so_far = [0]
+                def check_finished(e, r):
+                    restart[0] = restart[0] or r
+                    error[0] = error[0] or e
+                    so_far[0] += 1
+                    
+                    if so_far[0] >= len(ui_config): # finished
+                        finish()
+
+                # make sure main config is handled first
+                items = ui_config.items()
+                items.sort(key=lambda (key, cfg): key != 'main')
+
+                for key, cfg in items:
+                    if key == 'main':
+                        result = set_main_config(cfg)
+                        reload = result['reload'] or reload
+                        reboot[0] = result['reboot'] or reboot[0]
+                        restart[0] = result['restart'] or restart[0]
+                        check_finished(None, reload)
+                        
+                    else:
+                        set_camera_config(int(key), cfg, check_finished)
+            
+            else: # single camera config
+                def on_finish(e, r):
+                    error[0] = e
+                    restart[0] = r
+                    finish()
+
+                set_camera_config(camera_id, ui_config, on_finish)
+
+        else: # main config
+            result = set_main_config(ui_config)
+            reload = result['reload']
+            reboot[0] = result['reboot']
+            restart[0] = result['restart']
+
+    @BaseHandler.auth(admin=True)
+    def set_preview(self, camera_id):
+        try:
+            controls = json.loads(self.request.body)
+            
+        except Exception as e:
+            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+            
+            raise
+
+        camera_config = config.get_camera(camera_id)
+        if utils.v4l2_camera(camera_config): 
+            device = camera_config['videodevice']
+            
+            if 'brightness' in controls:
+                value = int(controls['brightness'])
+                logging.debug('setting brightness to %(value)s...' % {'value': value})
+    
+                v4l2ctl.set_brightness(device, value)
+    
+            if 'contrast' in controls:
+                value = int(controls['contrast'])
+                logging.debug('setting contrast to %(value)s...' % {'value': value})
+    
+                v4l2ctl.set_contrast(device, value)
+    
+            if 'saturation' in controls:
+                value = int(controls['saturation'])
+                logging.debug('setting saturation to %(value)s...' % {'value': value})
+    
+                v4l2ctl.set_saturation(device, value)
+    
+            if 'hue' in controls:
+                value = int(controls['hue'])
+                logging.debug('setting hue to %(value)s...' % {'value': value})
+    
+                v4l2ctl.set_hue(device, value)
+            
+            self.finish_json({})
+
+        elif utils.remote_camera(camera_config):
+            def on_response(error=None):
+                if error:
+                    self.finish_json({'error': error})
+                    
+                else:
+                    self.finish_json()
+            
+            remote.set_preview(camera_config, controls, on_response)
+        
+        else: # not supported
+            self.finish_json({'error': True})
+
+    @BaseHandler.auth()
+    def list(self):
+        logging.debug('listing cameras')
+
+        proto = self.get_data().get('proto')        
+        if proto == 'motioneye':  # remote listing
+            def on_response(cameras=None, error=None):
+                if error:
+                    self.finish_json({'error': error})
+                    
+                else:
+                    cameras = [c for c in cameras if c.get('enabled')]
+                    self.finish_json({'cameras': cameras})
+
+            remote.list(self.get_data(), on_response)
+        
+        elif proto == 'netcam':
+            scheme = self.get_data().get('scheme', 'http')
+
+            def on_response(cameras=None, error=None):
+                if error:
+                    self.finish_json({'error': error})
+                    
+                else:
+                    self.finish_json({'cameras': cameras})
+            
+            if scheme in ['http', 'https']:
+                utils.test_mjpeg_url(self.get_data(), auth_modes=['basic'], allow_jpeg=True, callback=on_response)
+                
+            elif config.motion_rtsp_support() and scheme == 'rtsp':
+                utils.test_rtsp_url(self.get_data(), callback=on_response)
+                
+            else:
+                on_response(error='protocol %s not supported' % scheme)
+
+        elif proto == 'mjpeg':
+            def on_response(cameras=None, error=None):
+                if error:
+                    self.finish_json({'error': error})
+                    
+                else:
+                    self.finish_json({'cameras': cameras})
+            
+            utils.test_mjpeg_url(self.get_data(), auth_modes=['basic', 'digest'], allow_jpeg=False, callback=on_response)
+        
+        elif proto == 'v4l2':
+            configured_devices = set()
+            for camera_id in config.get_camera_ids():
+                data = config.get_camera(camera_id)
+                if utils.v4l2_camera(data):
+                    configured_devices.add(data['videodevice'])
+
+            cameras = [{'id': d[1], 'name': d[2]} for d in v4l2ctl.list_devices()
+                    if (d[0] not in configured_devices) and (d[1] not in configured_devices)]
+            
+            self.finish_json({'cameras': cameras})
+
+        else:  # assuming local motionEye camera listing
+            cameras = []
+            camera_ids = config.get_camera_ids()
+            if not config.get_main().get('@enabled'):
+                camera_ids = []
+                
+            length = [len(camera_ids)]
+            def check_finished():
+                if len(cameras) == length[0]:
+                    cameras.sort(key=lambda c: c['id'])
+                    self.finish_json({'cameras': cameras})
+                    
+            def on_response_builder(camera_id, local_config):
+                def on_response(remote_ui_config=None, error=None):
+                    if error:
+                        cameras.append({
+                            'id': camera_id,
+                            'name': '&lt;' + remote.pretty_camera_url(local_config) + '&gt;',
+                            'enabled': False,
+                            'streaming_framerate': 1,
+                            'framerate': 1
+                        })
+                    
+                    else:
+                        remote_ui_config['id'] = camera_id
+
+                        if not remote_ui_config['enabled'] and local_config['@enabled']:
+                            # if a remote camera is disabled, make sure it's disabled locally as well
+                            local_config['@enabled'] = False
+                            config.set_camera(camera_id, local_config)
+                        
+                        elif remote_ui_config['enabled'] and not local_config['@enabled']:
+                            # if a remote camera is locally disabled, make sure the remote config says the same thing
+                            remote_ui_config['enabled'] = False
+                            
+                        for key, value in local_config.items():
+                            remote_ui_config[key.replace('@', '')] = value
+
+                        cameras.append(remote_ui_config)
+                        
+                    check_finished()
+                    
+                return on_response
+            
+            for camera_id in camera_ids:
+                local_config = config.get_camera(camera_id)
+                if local_config is None:
+                    continue
+                
+                if utils.local_motion_camera(local_config):
+                    ui_config = config.motion_camera_dict_to_ui(local_config)
+                    cameras.append(ui_config)
+                    check_finished()
+
+                elif utils.remote_camera(local_config):
+                    if local_config.get('@enabled') or self.get_argument('force', None) == 'true':
+                        remote.get_config(local_config, on_response_builder(camera_id, local_config))
+                    
+                    else: # don't try to reach the remote of the camera is disabled
+                        on_response_builder(camera_id, local_config)(error=True)
+                        
+                else: # assuming simple mjpeg camera
+                    ui_config = config.simple_mjpeg_camera_dict_to_ui(local_config)
+                    cameras.append(ui_config)
+                    check_finished()
+            
+            if length[0] == 0:        
+                self.finish_json({'cameras': []})
+
+    @BaseHandler.auth(admin=True)
+    def add_camera(self):
+        logging.debug('adding new camera')
+        
+        try:
+            device_details = json.loads(self.request.body)
+            
+        except Exception as e:
+            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+            
+            raise
+
+        camera_config = config.add_camera(device_details)
+
+        if utils.local_motion_camera(camera_config):
+            motionctl.stop()
+            
+            if settings.SMB_SHARES:
+                stop, start = smbctl.update_mounts()  # @UnusedVariable
+
+                if start:
+                    motionctl.start()
+            
+            else:
+                motionctl.start()
+            
+            ui_config = config.motion_camera_dict_to_ui(camera_config)
+            
+            self.finish_json(ui_config)
+        
+        elif utils.remote_camera(camera_config):
+            def on_response(remote_ui_config=None, error=None):
+                if error:
+                    return self.finish_json({'error': error})
+
+                for key, value in camera_config.items():
+                    remote_ui_config[key.replace('@', '')] = value
+                
+                self.finish_json(remote_ui_config)
+                
+            remote.get_config(camera_config, on_response)
+        
+        else: # assuming simple mjpeg camera
+            ui_config = config.simple_mjpeg_camera_dict_to_ui(camera_config)
+            
+            self.finish_json(ui_config)
+    
+    @BaseHandler.auth(admin=True)
+    def rem_camera(self, camera_id):
+        logging.debug('removing camera %(id)s' % {'id': camera_id})
+        
+        local = utils.local_motion_camera(config.get_camera(camera_id))
+        config.rem_camera(camera_id)
+        
+        if local:
+            motionctl.stop()
+            motionctl.start()
+            
+        self.finish_json()
+        
+    @BaseHandler.auth(admin=True)
+    def backup(self):
+        content = config.backup()
+
+        filename = 'motioneye-config.tar.gz'
+        self.set_header('Content-Type', 'application/x-compressed')
+        self.set_header('Content-Disposition', 'attachment; filename=' + filename + ';')
+
+        self.finish(content)
+
+    @BaseHandler.auth(admin=True)
+    def restore(self):
+        try:
+            content = self.request.files['files'][0]['body']
+            
+        except KeyError:
+            raise HTTPError(400, 'file attachment required')
+
+        result = config.restore(content)
+        if result:
+            self.finish_json({'ok': True, 'reboot': result['reboot']})
+            
+        else:
+            self.finish_json({'ok': False})
+
+
+class PictureHandler(BaseHandler):
+    @asynchronous
+    def get(self, camera_id, op, filename=None, group=None):
+        if camera_id is not None:
+            camera_id = int(camera_id)
+            if camera_id not in config.get_camera_ids():
+                raise HTTPError(404, 'no such camera')
+        
+        if op == 'current':
+            self.current(camera_id)
+            
+        elif op == 'list':
+            self.list(camera_id)
+            
+        elif op == 'frame':
+            self.frame(camera_id)
+            
+        elif op == 'download':
+            self.download(camera_id, filename)
+        
+        elif op == 'preview':
+            self.preview(camera_id, filename)
+        
+        elif op == 'zipped':
+            self.zipped(camera_id, group)
+        
+        elif op == 'timelapse':
+            self.timelapse(camera_id, group)
+        
+        else:
+            raise HTTPError(400, 'unknown operation')
+    
+    @asynchronous
+    def post(self, camera_id, op, filename=None, group=None):
+        if camera_id is not None:
+            camera_id = int(camera_id)
+            if camera_id not in config.get_camera_ids():
+                raise HTTPError(404, 'no such camera')
+        
+        if op == 'delete':
+            self.delete(camera_id, filename)
+
+        elif op == 'delete_all':
+            self.delete_all(camera_id, group)
+        
+        else:
+            raise HTTPError(400, 'unknown operation')
+    
+    @BaseHandler.auth(prompt=False)
+    def current(self, camera_id):
+        self.set_header('Content-Type', 'image/jpeg')
+        
+        width = self.get_argument('width', None)
+        height = self.get_argument('height', None)
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            picture = mediafiles.get_current_picture(camera_config,
+                    width=width,
+                    height=height)
+            
+            self.set_cookie('motion_detected_' + str(camera_id), str(motionctl.is_motion_detected(camera_id)).lower())
+            self.try_finish(picture)
+                
+        elif utils.remote_camera(camera_config):
+            def on_response(motion_detected=False, picture=None, error=None):
+                self.set_cookie('motion_detected_' + str(camera_id), str(motion_detected).lower())
+                self.try_finish(picture)
+            
+            remote.get_current_picture(camera_config, width=width, height=height, callback=on_response)
+            
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+            
+
+    @BaseHandler.auth()
+    def list(self, camera_id):
+        logging.debug('listing pictures for camera %(id)s' % {'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            def on_media_list(media_list):
+                if media_list is None:
+                    return self.finish_json({'error': 'Failed to get pictures list.'})
+
+                self.finish_json({
+                    'mediaList': media_list,
+                    'cameraName': camera_config['@name']
+                })
+            
+            mediafiles.list_media(camera_config, media_type='picture',
+                    callback=on_media_list, prefix=self.get_argument('prefix', None))
+
+        elif utils.remote_camera(camera_config):
+            def on_response(remote_list=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to get picture list for %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                self.finish_json(remote_list)
+            
+            remote.list_media(camera_config, media_type='picture', prefix=self.get_argument('prefix', None), callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    def frame(self, camera_id):
+        camera_config = config.get_camera(camera_id)
+        
+        if utils.local_motion_camera(camera_config) or utils.simple_mjpeg_camera(camera_config) or self.get_argument('title', None) is not None:
+            self.render('main.html',
+                    frame=True,
+                    camera_id=camera_id,
+                    camera_config=camera_config,
+                    title=self.get_argument('title', camera_config.get('@name', '')),
+                    admin_username=config.get_main().get('@admin_username'))
+
+        elif utils.remote_camera(camera_config):
+            def on_response(remote_ui_config=None, error=None):
+                if error:
+                    return self.render('main.html',
+                            frame=True,
+                            camera_id=camera_id,
+                            camera_config=camera_config,
+                            title=self.get_argument('title', ''))
+
+                # issue a fake motion_camera_ui_to_dict() call to transform
+                # the remote UI values into motion config directives
+                remote_config = config.motion_camera_ui_to_dict(remote_ui_config)
+                
+                self.render('main.html',
+                        frame=True,
+                        camera_id=camera_id,
+                        camera_config=remote_config,
+                        title=self.get_argument('title', remote_config['@name']),
+                        admin_username=config.get_main().get('@admin_username'))
+
+            remote.get_config(camera_config, on_response)
+        
+    @BaseHandler.auth()
+    def download(self, camera_id, filename):
+        logging.debug('downloading picture %(filename)s of camera %(id)s' % {
+                'filename': filename, 'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            content = mediafiles.get_media_content(camera_config, filename, 'picture')
+            
+            pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
+            self.set_header('Content-Type', 'image/jpeg')
+            self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+            
+            self.finish(content)
+        
+        elif utils.remote_camera(camera_config):
+            def on_response(response=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to download picture from %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                pretty_filename = os.path.basename(filename) # no camera name available w/o additional request
+                self.set_header('Content-Type', 'image/jpeg')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+                
+                self.finish(response)
+
+            remote.get_media_content(camera_config, filename=filename, media_type='picture', callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth()
+    def preview(self, camera_id, filename):
+        logging.debug('previewing picture %(filename)s of camera %(id)s' % {
+                'filename': filename, 'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            content = mediafiles.get_media_preview(camera_config, filename, 'picture',
+                    width=self.get_argument('width', None),
+                    height=self.get_argument('height', None))
+            
+            if content:
+                self.set_header('Content-Type', 'image/jpeg')
+                
+            else:
+                self.set_header('Content-Type', 'image/svg+xml')
+                content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
+                
+            self.finish(content)
+        
+        elif utils.remote_camera(camera_config):
+            def on_response(content=None, error=None):
+                if content:
+                    self.set_header('Content-Type', 'image/jpeg')
+                    
+                else:
+                    self.set_header('Content-Type', 'image/svg+xml')
+                    content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
+                
+                self.finish(content)
+            
+            remote.get_media_preview(camera_config, filename=filename, media_type='picture',
+                    width=self.get_argument('width', None),
+                    height=self.get_argument('height', None),
+                    callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+    
+    @BaseHandler.auth(admin=True)
+    def delete(self, camera_id, filename):
+        logging.debug('deleting picture %(filename)s of camera %(id)s' % {
+                'filename': filename, 'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            try:
+                mediafiles.del_media_content(camera_config, filename, 'picture')
+                self.finish_json()
+                
+            except Exception as e:
+                self.finish_json({'error': unicode(e)})
+
+        elif utils.remote_camera(camera_config):
+            def on_response(response=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to delete picture from %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                self.finish_json()
+
+            remote.del_media_content(camera_config, filename=filename, media_type='picture', callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth()
+    def zipped(self, camera_id, group):
+        key = self.get_argument('key', None)
+        camera_config = config.get_camera(camera_id)
+        
+        if key:
+            logging.debug('serving zip file for group %(group)s of camera %(id)s with key %(key)s' % {
+                    'group': group, 'id': camera_id, 'key': key})
+            
+            if utils.local_motion_camera(camera_config):
+                data = mediafiles.get_prepared_cache(key)
+                if not data:
+                    logging.error('prepared cache data for key "%s" does not exist' % key)
+                    
+                    raise HTTPError(404, 'no such key')
+
+                pretty_filename = camera_config['@name'] + '_' + group
+                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
+         
+                self.set_header('Content-Type', 'application/zip')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.zip;')
+                self.finish(data)
+                
+            elif utils.remote_camera(camera_config):
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to download zip file from %(url)s: %(msg)s.' % {
+                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                    self.set_header('Content-Type', response['content_type'])
+                    self.set_header('Content-Disposition', response['content_disposition'])
+                    self.finish(response['data'])
+
+                remote.get_zipped_content(camera_config, media_type='picture', key=key, group=group, callback=on_response)
+
+            else: # assuming simple mjpeg camera
+                raise HTTPError(400, 'unknown operation')
+
+        else: # prepare
+            logging.debug('preparing zip file for group %(group)s of camera %(id)s' % {
+                    'group': group, 'id': camera_id})
+
+            if utils.local_motion_camera(camera_config):
+                def on_zip(data):
+                    if data is None:
+                        return self.finish_json({'error': 'Failed to create zip file.'})
+    
+                    key = mediafiles.set_prepared_cache(data)
+                    logging.debug('prepared zip file for group %(group)s of camera %(id)s with key %(key)s' % {
+                            'group': group, 'id': camera_id, 'key': key})
+                    self.finish_json({'key': key})
+    
+                mediafiles.get_zipped_content(camera_config, media_type='picture', group=group, callback=on_zip)
+    
+            elif utils.remote_camera(camera_config):
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to make zip file at %(url)s: %(msg)s.' % {
+                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                    self.finish_json({'key': response['key']})
+
+                remote.make_zipped_content(camera_config, media_type='picture', group=group, callback=on_response)
+
+            else: # assuming simple mjpeg camera
+                raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth()
+    def timelapse(self, camera_id, group):
+        key = self.get_argument('key', None)
+        check = self.get_argument('check', False)
+        camera_config = config.get_camera(camera_id)
+
+        if key: # download
+            logging.debug('serving timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
+                    'group': group, 'id': camera_id, 'key': key})
+            
+            if utils.local_motion_camera(camera_config):
+                data = mediafiles.get_prepared_cache(key)
+                if data is None:
+                    logging.error('prepared cache data for key "%s" does not exist' % key)
+
+                    raise HTTPError(404, 'no such key')
+
+                pretty_filename = camera_config['@name'] + '_' + group
+                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
+    
+                self.set_header('Content-Type', 'video/x-msvideo')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.avi;')
+                self.finish(data)
+
+            elif utils.remote_camera(camera_config):
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to download timelapse movie from %(url)s: %(msg)s.' % {
+                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                    self.set_header('Content-Type', response['content_type'])
+                    self.set_header('Content-Disposition', response['content_disposition'])
+                    self.finish(response['data'])
+
+                remote.get_timelapse_movie(camera_config, key, group=group, callback=on_response)
+
+            else: # assuming simple mjpeg camera
+                raise HTTPError(400, 'unknown operation')
+
+        elif check:
+            logging.debug('checking timelapse movie status for group %(group)s of camera %(id)s' % {
+                    'group': group, 'id': camera_id})
+
+            if utils.local_motion_camera(camera_config):
+                status = mediafiles.check_timelapse_movie()
+                if status['progress'] == -1 and status['data']:
+                    key = mediafiles.set_prepared_cache(status['data'])
+                    logging.debug('prepared timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
+                            'group': group, 'id': camera_id, 'key': key})
+                    self.finish_json({'key': key, 'progress': -1})
+
+                else:
+                    self.finish_json(status)
+
+            elif utils.remote_camera(camera_config):
+                def on_response(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to check timelapse movie progress at %(url)s: %(msg)s.' % {
+                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                    if response['progress'] == -1 and response.get('key'):
+                        self.finish_json({'key': response['key'], 'progress': -1})
+                    
+                    else:
+                        self.finish_json(response)
+
+                remote.check_timelapse_movie(camera_config, group=group, callback=on_response)
+
+            else: # assuming simple mjpeg camera
+                raise HTTPError(400, 'unknown operation')
+
+        else: # start timelapse
+            interval = int(self.get_argument('interval'))
+            framerate = int(self.get_argument('framerate'))
+
+            logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with rate %(framerate)s/%(int)s' % {
+                    'group': group, 'id': camera_id, 'framerate': framerate, 'int': interval})
+
+            if utils.local_motion_camera(camera_config):
+                status = mediafiles.check_timelapse_movie()
+                if status['progress'] != -1:
+                    self.finish_json({'progress': status['progress']}) # timelapse already active
+
+                else:
+                    mediafiles.make_timelapse_movie(camera_config, framerate, interval, group=group)
+                    self.finish_json({'progress': -1})
+
+            elif utils.remote_camera(camera_config):
+                def on_status(response=None, error=None):
+                    if error:
+                        return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % {
+                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+                    
+                    if response['progress'] != -1:
+                        return self.finish_json({'progress': response['progress']}) # timelapse already active
+    
+                    def on_make(response=None, error=None):
+                        if error:
+                            return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % {
+                                    'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+    
+                        self.finish_json({'progress': -1})
+                    
+                    remote.make_timelapse_movie(camera_config, framerate, interval, group=group, callback=on_make)
+
+                remote.check_timelapse_movie(camera_config, group=group, callback=on_status)
+
+            else: # assuming simple mjpeg camera
+                raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth(admin=True)
+    def delete_all(self, camera_id, group):
+        logging.debug('deleting picture group %(group)s of camera %(id)s' % {
+                'group': group, 'id': camera_id})
+
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            try:
+                mediafiles.del_media_group(camera_config, group, 'picture')
+                self.finish_json()
+                
+            except Exception as e:
+                self.finish_json({'error': unicode(e)})
+
+        elif utils.remote_camera(camera_config):
+            def on_response(response=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to delete picture group from %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                self.finish_json()
+
+            remote.del_media_group(camera_config, group=group, media_type='picture', callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    def try_finish(self, content):
+        try:
+            self.finish(content)
+            
+        except IOError as e:
+            logging.warning('could not write response: %(msg)s' % {'msg': unicode(e)})
+
+
+class MovieHandler(BaseHandler):
+    @asynchronous
+    def get(self, camera_id, op, filename=None):
+        if camera_id is not None:
+            camera_id = int(camera_id)
+            if camera_id not in config.get_camera_ids():
+                raise HTTPError(404, 'no such camera')
+        
+        if op == 'list':
+            self.list(camera_id)
+            
+        elif op == 'download':
+            self.download(camera_id, filename)
+        
+        elif op == 'preview':
+            self.preview(camera_id, filename)
+        
+        else:
+            raise HTTPError(400, 'unknown operation')
+    
+    @asynchronous
+    def post(self, camera_id, op, filename=None, group=None):
+        if camera_id is not None:
+            camera_id = int(camera_id)
+            if camera_id not in config.get_camera_ids():
+                raise HTTPError(404, 'no such camera')
+        
+        if op == 'delete':
+            self.delete(camera_id, filename)
+        
+        elif op == 'delete_all':
+            self.delete_all(camera_id, group)
+        
+        else:
+            raise HTTPError(400, 'unknown operation')
+    
+    @BaseHandler.auth()
+    def list(self, camera_id):
+        logging.debug('listing movies for camera %(id)s' % {'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            def on_media_list(media_list):
+                if media_list is None:
+                    return self.finish_json({'error': 'Failed to get movies list.'})
+
+                self.finish_json({
+                    'mediaList': media_list,
+                    'cameraName': camera_config['@name']
+                })
+            
+            mediafiles.list_media(camera_config, media_type='movie',
+                    callback=on_media_list, prefix=self.get_argument('prefix', None))
+        
+        elif utils.remote_camera(camera_config):
+            def on_response(remote_list=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to get movie list for %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                self.finish_json(remote_list)
+            
+            remote.list_media(camera_config, media_type='movie', prefix=self.get_argument('prefix', None), callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth()
+    def download(self, camera_id, filename):
+        logging.debug('downloading movie %(filename)s of camera %(id)s' % {
+                'filename': filename, 'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            content = mediafiles.get_media_content(camera_config, filename, 'movie')
+            
+            pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
+            self.set_header('Content-Type', 'video/mpeg')
+            self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+            
+            self.finish(content)
+        
+        elif utils.remote_camera(camera_config):
+            def on_response(response=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to download movie from %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                pretty_filename = os.path.basename(filename) # no camera name available w/o additional request
+                self.set_header('Content-Type', 'video/mpeg')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+                
+                self.finish(response)
+
+            remote.get_media_content(camera_config, filename=filename, media_type='movie', callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth()
+    def preview(self, camera_id, filename):
+        logging.debug('previewing movie %(filename)s of camera %(id)s' % {
+                'filename': filename, 'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            content = mediafiles.get_media_preview(camera_config, filename, 'movie',
+                    width=self.get_argument('width', None),
+                    height=self.get_argument('height', None))
+            
+            if content:
+                self.set_header('Content-Type', 'image/jpeg')
+                
+            else:
+                self.set_header('Content-Type', 'image/svg+xml')
+                content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
+            
+            self.finish(content)
+        
+        elif utils.remote_camera(camera_config):
+            def on_response(content=None, error=None):
+                if content:
+                    self.set_header('Content-Type', 'image/jpeg')
+                    
+                else:
+                    self.set_header('Content-Type', 'image/svg+xml')
+                    content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
+
+                self.finish(content)
+            
+            remote.get_media_preview(camera_config, filename=filename, media_type='movie',
+                    width=self.get_argument('width', None),
+                    height=self.get_argument('height', None),
+                    callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth(admin=True)
+    def delete(self, camera_id, filename):
+        logging.debug('deleting movie %(filename)s of camera %(id)s' % {
+                'filename': filename, 'id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            try:
+                mediafiles.del_media_content(camera_config, filename, 'movie')
+                self.finish_json()
+                
+            except Exception as e:
+                self.finish_json({'error': unicode(e)})
+
+        elif utils.remote_camera(camera_config):
+            def on_response(response=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to delete movie from %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                self.finish_json()
+
+            remote.del_media_content(camera_config, filename=filename, media_type='movie', callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+    @BaseHandler.auth(admin=True)
+    def delete_all(self, camera_id, group):
+        logging.debug('deleting movie group %(group)s of camera %(id)s' % {
+                'group': group, 'id': camera_id})
+
+        camera_config = config.get_camera(camera_id)
+        if utils.local_motion_camera(camera_config):
+            try:
+                mediafiles.del_media_group(camera_config, group, 'movie')
+                self.finish_json()
+                
+            except Exception as e:
+                self.finish_json({'error': unicode(e)})
+
+        elif utils.remote_camera(camera_config):
+            def on_response(response=None, error=None):
+                if error:
+                    return self.finish_json({'error': 'Failed to delete movie group from %(url)s: %(msg)s.' % {
+                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
+
+                self.finish_json()
+
+            remote.del_media_group(camera_config, group=group, media_type='movie', callback=on_response)
+
+        else: # assuming simple mjpeg camera
+            raise HTTPError(400, 'unknown operation')
+
+
+class RelayEventHandler(BaseHandler):
+    @BaseHandler.auth(admin=True)
+    def post(self):
+        event = self.get_argument('event')
+        thread_id = int(self.get_argument('thread_id'))
+        logging.debug('recevied relayed event %(event)s for thread id %(id)s' % {'event': event, 'id': thread_id})
+        
+        camera_id = motionctl.thread_id_to_camera_id(thread_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_motion_camera(camera_config):
+            logging.warn('ignoring event for non-local camera with id %s' % camera_id)
+            return self.finish_json()
+        
+        if event == 'start':
+            if not camera_config['@motion_detection']:
+                logging.debug('ignoring start event for camera with id %s and motion detection disabled' % camera_id)
+                return self.finish_json()
+
+            motionctl.set_motion_detected(camera_id, True)
+            
+        elif event == 'stop':
+            motionctl.set_motion_detected(camera_id, False)
+
+        else:
+            logging.warn('unknown event %s' % event)
+
+        self.finish_json()
+
+
+class LogHandler(BaseHandler):
+    LOGS = {
+        'motion': (os.path.join(settings.LOG_PATH, 'motion.log'),  'motion.log'),
+    }
+
+    @BaseHandler.auth(admin=True)
+    def get(self, name):
+        log = self.LOGS.get(name)
+        if log is None:
+            raise HTTPError(404, 'no such log')
+
+        (path, filename) = log
+
+        self.set_header('Content-Type', 'text/plain')
+        self.set_header('Content-Disposition', 'attachment; filename=' + filename + ';')
+
+        if path.startswith('/'): # an actual path        
+            logging.debug('serving log file "%s" from "%s"' % (filename, path))
+
+            with open(path) as f:
+                self.finish(f.read())
+                
+        else: # a command to execute 
+            logging.debug('serving log file "%s" from command "%s"' % (filename, path))
+
+            try:
+                output = subprocess.check_output(path, shell=True)
+            
+            except Exception as e:
+                output = 'failed to execute command: %s' % e
+                
+            self.finish(output)
+                
+
+class UpdateHandler(BaseHandler):
+    @BaseHandler.auth(admin=True)
+    def get(self):
+        logging.debug('listing versions')
+        
+        versions = update.get_all_versions()
+        current_version = update.get_version()
+        update_version = None
+        if versions and update.compare_versions(versions[-1], current_version) > 0:
+            update_version = versions[-1]
+
+        self.finish_json({
+            'update_version': update_version,
+            'current_version': current_version
+        })
+
+    @BaseHandler.auth(admin=True)
+    def post(self):
+        version = self.get_argument('version')
+        
+        logging.debug('performing update to version %(version)s' % {'version': version})
+        
+        result = update.perform_update(version)
+        
+        self.finish_json(result)
+
+
+class PowerHandler(BaseHandler):
+    @BaseHandler.auth(admin=True)
+    def post(self, op):
+        if op == 'shutdown':
+            self.shut_down()
+            
+        elif op == 'reboot':
+            self.reboot()
+    
+    def shut_down(self):
+        IOLoop.instance().add_timeout(datetime.timedelta(seconds=2), powerctl.shut_down)
+
+    def reboot(self):
+        IOLoop.instance().add_timeout(datetime.timedelta(seconds=2), powerctl.reboot)
+
+
+class VersionHandler(BaseHandler):
+    def get(self):
+        self.render('version.html',
+                version=update.get_version(),
+                hostname=socket.gethostname())
+
+    post = get
+
+
+# this will only trigger the login mechanism on the client side, if required 
+class LoginHandler(BaseHandler):
+    @BaseHandler.auth()
+    def get(self):
+        self.finish_json()
+
+    def post(self):
+        self.set_header('Content-Type', 'text/html')
+        self.finish()
diff --git a/motioneye/mediafiles.py b/motioneye/mediafiles.py
new file mode 100644 (file)
index 0000000..c4f615e
--- /dev/null
@@ -0,0 +1,801 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import errno
+import fcntl
+import functools
+import hashlib
+import logging
+import multiprocessing
+import os.path
+import re
+import stat
+import StringIO
+import subprocess
+import time
+import tornado
+import zipfile
+
+from PIL import Image
+from tornado import ioloop
+
+import config
+import settings
+import utils
+
+
+_PICTURE_EXTS = ['.jpg']
+_MOVIE_EXTS = ['.avi', '.mp4']
+
+# a cache list of paths to movies without preview
+_previewless_movie_files = []
+
+# a cache of prepared files (whose preparing time is significant)
+_prepared_files = {}
+
+_timelapse_process = None
+_timelapse_data = None
+
+
+def _list_media_files(dir, exts, prefix=None):
+    media_files = []
+    
+    if prefix is not None:
+        if prefix == 'ungrouped':
+            prefix = ''
+        
+        root = os.path.join(dir, prefix)
+        for name in os.listdir(root):
+            if name == 'lastsnap.jpg' or name.startswith('.'): # ignore the lastsnap.jpg and hidden files
+                continue
+                
+            full_path = os.path.join(root, name)
+            try:
+                st = os.stat(full_path)
+            
+            except Exception as e:
+                logging.error('stat failed: ' + unicode(e))
+                continue
+                
+            if not stat.S_ISREG(st.st_mode): # not a regular file
+                continue
+
+            full_path_lower = full_path.lower()
+            if not [e for e in exts if full_path_lower.endswith(e)]:
+                continue
+            
+            media_files.append((full_path, st))
+
+    else:    
+        for root, dirs, files in os.walk(dir):  # @UnusedVariable # TODO os.walk can be rewritten to return stat info
+            for name in files:
+                if name == 'lastsnap.jpg' or name.startswith('.'): # ignore the lastsnap.jpg and hidden files
+                    continue
+                
+                full_path = os.path.join(root, name)
+                try:
+                    st = os.stat(full_path)
+                
+                except Exception as e:
+                    logging.error('stat failed: ' + unicode(e))
+                    continue
+                
+                if not stat.S_ISREG(st.st_mode): # not a regular file
+                    continue
+                 
+                full_path_lower = full_path.lower()
+                if not [e for e in exts if full_path_lower.endswith(e)]:
+                    continue
+                
+                media_files.append((full_path, st))
+
+    return media_files
+
+
+def _remove_older_files(dir, moment, exts):
+    for (full_path, st) in _list_media_files(dir, exts):
+        file_moment = datetime.datetime.fromtimestamp(st.st_mtime)
+        if file_moment < moment:
+            logging.debug('removing file %(path)s...' % {'path': full_path})
+            
+            # remove the file itself
+            try:
+                os.remove(full_path)
+            
+            except OSError as e:
+                if e.errno == errno.ENOENT:
+                    pass # the file might have been removed in the meantime
+                
+                else:
+                    logging.error('failed to remove %s: %s' % (full_path, e))
+
+            # remove the parent directories if empty or contain only thumb files
+            dir_path = os.path.dirname(full_path)
+            if not os.path.exists(dir_path):
+                continue
+            
+            listing = os.listdir(dir_path)
+            thumbs = [l for l in listing if l.endswith('.thumb')]
+            
+            if len(listing) == len(thumbs): # only thumbs
+                for p in thumbs:
+                    try:
+                        os.remove(os.path.join(dir_path, p))
+                    
+                    except:
+                        logging.error('failed to remove %s: %s' % (p, e))
+
+            if not listing or len(listing) == len(thumbs):
+                # this will possibly cause following paths that are in the media files for loop
+                # to be removed in advance; the os.remove call will raise ENOENT which is silently ignored 
+                logging.debug('removing empty directory %(path)s...' % {'path': dir_path})
+                try:
+                    os.removedirs(dir_path)
+                
+                except:
+                    logging.error('failed to remove %s: %s' % (dir_path, e))
+
+
+def find_ffmpeg():
+    try:
+        return subprocess.check_output('which ffmpeg', shell=True).strip()
+    
+    except subprocess.CalledProcessError: # not found
+        return None
+
+
+def cleanup_media(media_type):
+    logging.debug('cleaning up %(media_type)ss...' % {'media_type': media_type})
+    
+    if media_type == 'picture':
+        exts = _PICTURE_EXTS
+        
+    elif media_type == 'movie':
+        exts = _MOVIE_EXTS + ['.thumb']
+        
+    for camera_id in config.get_camera_ids():
+        camera_config = config.get_camera(camera_id)
+        if not utils.local_motion_camera(camera_config):
+            continue
+        
+        preserve_media = camera_config.get('@preserve_%(media_type)ss' % {'media_type': media_type}, 0)
+        if preserve_media == 0:
+            return # preserve forever
+        
+        still_images_enabled = bool(
+                ((camera_config['emulate_motion'] or camera_config['output_pictures']) and camera_config['picture_filename']) or
+                (camera_config['snapshot_interval'] and camera_config['snapshot_filename']))
+        
+        movies_enabled = camera_config['ffmpeg_output_movies']
+
+        if media_type == 'picture' and not still_images_enabled:
+            continue # only cleanup pictures for cameras with still images enabled
+        
+        elif media_type == 'movie' and not movies_enabled:
+            continue # only cleanup movies for cameras with movies enabled
+
+        preserve_moment = datetime.datetime.now() - datetime.timedelta(days=preserve_media)
+            
+        target_dir = camera_config.get('target_dir')
+        _remove_older_files(target_dir, preserve_moment, exts=exts)
+
+
+def make_movie_preview(camera_config, full_path):
+    framerate = camera_config['framerate']
+    pre_capture = camera_config['pre_capture']
+    offs = pre_capture / framerate
+    offs = max(4, offs * 2)
+    
+    logging.debug('creating movie preview for %(path)s with an offset of %(offs)s seconds...' % {
+            'path': full_path, 'offs': offs})
+
+    cmd = 'ffmpeg -i "%(path)s" -f mjpeg -vframes 1 -ss %(offs)s -y %(path)s.thumb'
+    
+    try:
+        subprocess.check_output(cmd % {'path': full_path, 'offs': offs}, shell=True, stderr=subprocess.STDOUT)
+    
+    except subprocess.CalledProcessError as e:
+        logging.error('failed to create movie preview for %(path)s: %(msg)s' % {
+                'path': full_path, 'msg': unicode(e)})
+        
+        return None
+    
+    try:
+        st = os.stat(full_path + '.thumb')
+    
+    except os.error:
+        logging.error('failed to create movie preview for %(path)s: ffmpeg error' % {
+                'path': full_path})
+
+        return None
+
+    if st.st_size == 0:
+        logging.debug('movie is too short, grabbing first frame from %(path)s...' % {'path': full_path})
+        
+        # try again, this time grabbing the very first frame
+        try:
+            subprocess.check_output(cmd % {'path': full_path, 'offs': 0}, shell=True, stderr=subprocess.STDOUT)
+        
+        except subprocess.CalledProcessError as e:
+            logging.error('failed to create movie preview for %(path)s: %(msg)s' % {
+                    'path': full_path, 'msg': unicode(e)})
+            
+            return None
+    
+    return full_path + '.thumb'
+
+
+def make_next_movie_preview():
+    global _previewless_movie_files
+    
+    logging.debug('making preview for the next movie...')
+    
+    if _previewless_movie_files:
+        (camera_config, path) = _previewless_movie_files.pop(0)
+        
+        make_movie_preview(camera_config, path)
+    
+    else:
+        logging.debug('gathering movies without preview...')
+        
+        count = 0
+        for camera_id in config.get_camera_ids():
+            camera_config = config.get_camera(camera_id)
+            if not utils.local_motion_camera(camera_config):
+                continue
+            
+            target_dir = camera_config['target_dir']
+            
+            for (full_path, st) in _list_media_files(target_dir, _MOVIE_EXTS):  # @UnusedVariable
+                if os.path.exists(full_path + '.thumb'):
+                    continue
+                
+                logging.debug('found a movie without preview: %(path)s' % {
+                        'path': full_path})
+                
+                _previewless_movie_files.append((camera_config, full_path))
+                count += 1
+        
+        logging.debug('found %(count)d movies without preview' % {'count': count})    
+        
+        if count:
+            make_next_movie_preview()
+
+
+def list_media(camera_config, media_type, callback, prefix=None):
+    target_dir = camera_config.get('target_dir')
+
+    if media_type == 'picture':
+        exts = _PICTURE_EXTS
+        
+    elif media_type == 'movie':
+        exts = _MOVIE_EXTS
+
+    # create a subprocess to retrieve media files
+    def do_list_media(pipe):
+        mf = _list_media_files(target_dir, exts=exts, prefix=prefix)
+        for (p, st) in mf:
+            path = p[len(target_dir):]
+            if not path.startswith('/'):
+                path = '/' + path
+    
+            timestamp = st.st_mtime
+            size = st.st_size
+            
+            pipe.send({
+                'path': path,
+                'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp)),
+                'momentStrShort': utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp), short=True),
+                'sizeStr': utils.pretty_size(size),
+                'timestamp': timestamp
+            })
+        
+        pipe.close()
+    
+    logging.debug('starting media listing process...')
+    
+    (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
+    process = multiprocessing.Process(target=do_list_media, args=(child_pipe, ))
+    process.start()
+    
+    # poll the subprocess to see when it has finished
+    started = datetime.datetime.now()
+    media_list = []
+    
+    def read_media_list():
+        while parent_pipe.poll():
+            media_list.append(parent_pipe.recv())
+    
+    def poll_process():
+        ioloop = tornado.ioloop.IOLoop.instance()
+        if process.is_alive(): # not finished yet
+            now = datetime.datetime.now()
+            delta = now - started
+            if delta.seconds < 120:
+                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process)
+                read_media_list()
+            
+            else: # process did not finish within 2 minutes
+                logging.error('timeout waiting for the media listing process to finish')
+                
+                callback(None)
+
+        else: # finished
+            read_media_list()
+            logging.debug('media listing process has returned %(count)s files' % {'count': len(media_list)})
+            callback(media_list)
+    
+    poll_process()
+
+
+def get_media_content(camera_config, path, media_type):
+    target_dir = camera_config.get('target_dir')
+
+    full_path = os.path.join(target_dir, path)
+    
+    try:
+        with open(full_path) as f:
+            return f.read()
+    
+    except Exception as e:
+        logging.error('failed to read file %(path)s: %(msg)s' % {
+                'path': full_path, 'msg': unicode(e)})
+        
+        return None
+
+
+def get_zipped_content(camera_config, media_type, group, callback):
+    target_dir = camera_config.get('target_dir')
+
+    if media_type == 'picture':
+        exts = _PICTURE_EXTS
+        
+    elif media_type == 'movie':
+        exts = _MOVIE_EXTS
+        
+    working = multiprocessing.Value('b')
+    working.value = True
+
+    # create a subprocess to add files to zip
+    def do_zip(pipe):
+        mf = _list_media_files(target_dir, exts=exts, prefix=group)
+        paths = []
+        for (p, st) in mf:  # @UnusedVariable
+            path = p[len(target_dir):]
+            if path.startswith('/'):
+                path = path[1:]
+
+            paths.append(path)
+            
+        zip_filename = os.path.join(settings.MEDIA_PATH, '.zip-%s' % int(time.time()))
+        logging.debug('adding %d files to zip file "%s"' % (len(paths), zip_filename))
+
+        try:
+            with zipfile.ZipFile(zip_filename, mode='w') as f:
+                for path in paths:
+                    full_path = os.path.join(target_dir, path)
+                    f.write(full_path, path)
+
+        except Exception as e:
+            logging.error('failed to create zip file "%s": %s' % (zip_filename, e))
+
+            working.value = False
+            pipe.close()
+            return
+
+        logging.debug('reading zip file "%s" into memory' % zip_filename)
+
+        try:
+            with open(zip_filename, mode='r') as f:
+                data = f.read()
+
+            working.value = False
+            pipe.send(data)
+            logging.debug('zip data ready')
+
+        except Exception as e:
+            logging.error('failed to read zip file "%s": %s' % (zip_filename, e))
+            working.value = False
+
+        finally:
+            os.remove(zip_filename)
+            pipe.close()
+
+    logging.debug('starting zip process...')
+
+    (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
+    process = multiprocessing.Process(target=do_zip, args=(child_pipe, ))
+    process.start()
+
+    # poll the subprocess to see when it has finished
+    started = datetime.datetime.now()
+
+    def poll_process():
+        ioloop = tornado.ioloop.IOLoop.instance()
+        if working.value:
+            now = datetime.datetime.now()
+            delta = now - started
+            if delta.seconds < settings.ZIP_TIMEOUT:
+                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process)
+
+            else: # process did not finish within 2 minutes
+                logging.error('timeout waiting for the zip process to finish')
+
+                callback(None)
+
+        else: # finished
+            try:
+                data = parent_pipe.recv()
+                logging.debug('zip process has returned %d bytes' % len(data))
+                
+            except:
+                data = None
+            
+            callback(data)
+
+    poll_process()
+
+
+def make_timelapse_movie(camera_config, framerate, interval, group):
+    global _timelapse_process
+    global _timelapse_data
+    
+    target_dir = camera_config.get('target_dir')
+    
+    # create a subprocess to retrieve media files
+    def do_list_media(pipe):
+        mf = _list_media_files(target_dir, exts=_PICTURE_EXTS, prefix=group)
+        for (p, st) in mf:
+            timestamp = st.st_mtime
+
+            pipe.send({
+                'path': p,
+                'timestamp': timestamp
+            })
+
+        pipe.close()
+
+    logging.debug('starting media listing process...')
+    
+    (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
+    _timelapse_process = multiprocessing.Process(target=do_list_media, args=(child_pipe, ))
+    _timelapse_process.progress = 0
+    _timelapse_process.start()
+    _timelapse_data = None
+
+    started = [datetime.datetime.now()]
+    media_list = []
+    
+    tmp_filename = os.path.join(settings.MEDIA_PATH, '.%s.avi' % int(time.time()))
+
+    def read_media_list():
+        while parent_pipe.poll():
+            media_list.append(parent_pipe.recv())
+        
+    def poll_media_list_process():
+        ioloop = tornado.ioloop.IOLoop.instance()
+        if _timelapse_process.is_alive(): # not finished yet
+            now = datetime.datetime.now()
+            delta = now - started[0]
+            if delta.seconds < 300: # the subprocess has 5 minutes to complete its job
+                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_media_list_process)
+                read_media_list()
+
+            else: # process did not finish within 2 minutes
+                logging.error('timeout waiting for the media listing process to finish')
+                
+                _timelapse_process.progress = -1
+
+        else: # finished
+            read_media_list()
+            logging.debug('media listing process has returned %(count)s files' % {'count': len(media_list)})
+            
+            if not media_list:
+                _timelapse_process.progress = -1
+                
+                return
+
+            pictures = select_pictures(media_list)
+            make_movie(pictures)
+
+    def select_pictures(media_list):
+        media_list.sort(key=lambda e: e['timestamp'])
+        start = media_list[0]['timestamp']
+        slices = {}
+        max_idx = 0
+        for m in media_list:
+            offs = m['timestamp'] - start
+            pos = float(offs) / interval - 0.5
+            idx = int(round(pos))
+            max_idx = idx
+            m['delta'] = abs(pos - idx)
+            slices.setdefault(idx, []).append(m)
+
+        selected = []
+        for i in xrange(max_idx + 1):
+            slice = slices.get(i)
+            if not slice:
+                continue
+
+            selected.append(min(slice, key=lambda m: m['delta']))
+
+        logging.debug('selected %d/%d media files' % (len(selected), len(media_list)))
+        
+        return selected
+
+    def make_movie(pictures):
+        global _timelapse_process
+
+        cmd =  'rm -f %(tmp_filename)s;'
+        cmd += 'cat %(jpegs)s | ffmpeg -framerate %(framerate)s -f image2pipe -vcodec mjpeg -i - -vcodec mpeg4 -b:v %(bitrate)s -qscale:v 0.1 -f avi %(tmp_filename)s'
+
+        bitrate = 9999999
+
+        cmd = cmd % {
+            'tmp_filename': tmp_filename,
+            'jpegs': ' '.join((('"' + p['path'] + '"') for p in pictures)),
+            'framerate': framerate,
+            'bitrate': bitrate
+        }
+        
+        logging.debug('executing "%s"' % cmd)
+        
+        _timelapse_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
+        _timelapse_process.progress = 0.01 # 1%
+        
+        # make subprocess stdout pipe non-blocking
+        fd = _timelapse_process.stdout.fileno()
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        poll_movie_process(pictures)
+
+    def poll_movie_process(pictures):
+        global _timelapse_process
+        global _timelapse_data
+        
+        ioloop = tornado.ioloop.IOLoop.instance()
+        if _timelapse_process.poll() is None: # not finished yet
+            ioloop.add_timeout(datetime.timedelta(seconds=0.5), functools.partial(poll_movie_process, pictures))
+
+            try:
+                output = _timelapse_process.stdout.read()
+            
+            except IOError as e:
+                if e.errno == errno.EAGAIN:
+                    output = ''
+                
+                else:
+                    raise
+                
+            frame_index = re.findall('frame=\s*(\d+)', output)
+            try:
+                frame_index = int(frame_index[-1])
+            
+            except (IndexError, ValueError):
+                return
+
+            _timelapse_process.progress = max(0.01, float(frame_index) / len(pictures))
+            
+            logging.debug('timelapse progress: %s' % int(100 * _timelapse_process.progress))
+
+        else: # finished
+            exit_code = _timelapse_process.poll()
+            _timelapse_process = None
+            
+            if exit_code != 0:
+                logging.error('ffmpeg process failed')
+                _timelapse_data = None
+
+                try:
+                    os.remove(tmp_filename)
+
+                except:
+                    pass
+
+            else:
+                logging.debug('reading timelapse movie file "%s" into memory' % tmp_filename)
+    
+                try:
+                    with open(tmp_filename, mode='r') as f:
+                        _timelapse_data = f.read()
+    
+                    logging.debug('timelapse movie process has returned %d bytes' % len(_timelapse_data))
+    
+                except Exception as e:
+                    logging.error('failed to read timelapse movie file "%s": %s' % (tmp_filename, e))
+    
+                finally:
+                    try:
+                        os.remove(tmp_filename)
+
+                    except:
+                        pass
+
+    poll_media_list_process()
+
+
+def check_timelapse_movie():
+    if _timelapse_process:
+        if ((hasattr(_timelapse_process, 'poll') and _timelapse_process.poll() is None) or
+            (hasattr(_timelapse_process, 'is_alive') and _timelapse_process.is_alive())):
+        
+            return {'progress': _timelapse_process.progress, 'data': None}
+        
+        else:
+            return {'progress': _timelapse_process.progress, 'data': _timelapse_data}
+
+    else:
+        return {'progress': -1, 'data': _timelapse_data}
+
+
+def get_media_preview(camera_config, path, media_type, width, height):
+    target_dir = camera_config.get('target_dir')
+    full_path = os.path.join(target_dir, path)
+    
+    if media_type == 'movie':
+        if not os.path.exists(full_path + '.thumb'):
+            if not make_movie_preview(camera_config, full_path):
+                return None
+        
+        full_path += '.thumb'
+    
+    try:
+        with open(full_path) as f:
+            content = f.read()
+    
+    except Exception as e:
+        logging.error('failed to read file %(path)s: %(msg)s' % {
+                'path': full_path, 'msg': unicode(e)})
+        
+        return None
+    
+    if width is height is None:
+        return content
+    
+    sio = StringIO.StringIO(content)
+    image = Image.open(sio)
+    width = width and int(width) or image.size[0]
+    height = height and int(height) or image.size[1]
+    
+    image.thumbnail((width, height), Image.LINEAR)
+
+    sio = StringIO.StringIO()
+    image.save(sio, format='JPEG')
+
+    return sio.getvalue()
+
+
+def del_media_content(camera_config, path, media_type):
+    target_dir = camera_config.get('target_dir')
+
+    full_path = os.path.join(target_dir, path)
+    
+    try:
+        # remove the file itself
+        os.remove(full_path)
+
+        # remove the parent directories if empty or contains only thumb files
+        dir_path = os.path.dirname(full_path)
+        listing = os.listdir(dir_path)
+        thumbs = [l for l in listing if l.endswith('.thumb')]
+        
+        if len(listing) == len(thumbs): # only thumbs
+            for p in thumbs:
+                os.remove(os.path.join(dir_path, p))
+
+        if not listing or len(listing) == len(thumbs):
+            logging.debug('removing empty directory %(path)s...' % {'path': dir_path})
+            os.removedirs(dir_path)
+    
+    except Exception as e:
+        logging.error('failed to remove file %(path)s: %(msg)s' % {
+                'path': full_path, 'msg': unicode(e)})
+        
+        raise
+
+
+def del_media_group(camera_config, group, media_type):
+    if media_type == 'picture':
+        exts = _PICTURE_EXTS
+        
+    elif media_type == 'movie':
+        exts = _MOVIE_EXTS
+        
+    target_dir = camera_config.get('target_dir')
+    full_path = os.path.join(target_dir, group)
+
+    mf = _list_media_files(target_dir, exts=exts, prefix=group)
+    for (path, st) in mf:  # @UnusedVariable
+        try:
+            os.remove(path)
+    
+        except Exception as e:
+            logging.error('failed to remove file %(path)s: %(msg)s' % {
+                    'path': full_path, 'msg': unicode(e)})
+
+            raise
+
+    # remove the group directory if empty or contains only thumb files
+    listing = os.listdir(full_path)
+    thumbs = [l for l in listing if l.endswith('.thumb')]
+
+    if len(listing) == len(thumbs): # only thumbs
+        for p in thumbs:
+            os.remove(os.path.join(full_path, p))
+
+    if not listing or len(listing) == len(thumbs):
+        logging.debug('removing empty directory %(path)s...' % {'path': full_path})
+        os.removedirs(full_path)
+
+
+def get_current_picture(camera_config, width, height):
+    import mjpgclient
+
+    jpg = mjpgclient.get_jpg(camera_config['@id'])
+    
+    if jpg is None:
+        return None
+    
+    if width is height is None:
+        return jpg # no server-side resize needed
+
+    sio = StringIO.StringIO(jpg)
+    image = Image.open(sio)
+    
+    width = width and int(width) or image.size[0]
+    height = height and int(height) or image.size[1]
+    
+    webcam_resolution = camera_config['@webcam_resolution']
+    max_width = image.size[0] * webcam_resolution / 100
+    max_height = image.size[1] * webcam_resolution / 100
+    
+    width = min(max_width, width)
+    height = min(max_height, height)
+    
+    if width >= image.size[0] and height >= image.size[1]:
+        return jpg # no enlarging of the picture on the server side
+    
+    image.thumbnail((width, height), Image.CUBIC)
+
+    sio = StringIO.StringIO()
+    image.save(sio, format='JPEG')
+
+    return sio.getvalue()
+
+
+def get_prepared_cache(key):
+    return _prepared_files.pop(key, None)
+
+
+def set_prepared_cache(data):
+    key = hashlib.sha1(str(time.time())).hexdigest()
+
+    if key in _prepared_files:
+        logging.warn('key "%s" already present in prepared cache' % key)
+        
+    _prepared_files[key] = data
+    
+    def clear():
+        if _prepared_files.pop(key, None) is not None:
+            logging.warn('key "%s" was still present in the prepared cache, removed' % key)
+
+    timeout = 3600 # the user has 1 hour to download the file after creation
+    ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=timeout), clear)
+
+    return key
diff --git a/motioneye/mjpgclient.py b/motioneye/mjpgclient.py
new file mode 100644 (file)
index 0000000..088a962
--- /dev/null
@@ -0,0 +1,301 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import errno
+import logging
+import re
+import socket
+import time
+
+from tornado import iostream, ioloop
+
+import config
+import motionctl
+import settings
+import utils
+
+
+class MjpgClient(iostream.IOStream):
+    clients = {} # dictionary of clients indexed by camera id
+    last_jpgs = {} # dictionary of jpg contents indexed by camera id
+    last_jpg_moment = {} # dictionary of moments of the last received jpeg indexed by camera id
+    last_access = {} # dictionary of access moments indexed by camera id
+    last_erroneous_close_time = 0 # helps detecting erroneous connections and restart motion
+    
+    def __init__(self, camera_id, port, username, password):
+        self._camera_id = camera_id
+        self._port = port
+        self._username = (username or '').encode('utf8')
+        self._password = (password or '').encode('utf8')
+        self._auth_digest_state = {}
+        
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        iostream.IOStream.__init__(self, s)
+        
+        self.set_close_callback(self.on_close)
+        
+    def connect(self):
+        iostream.IOStream.connect(self, ('localhost', self._port), self._on_connect)
+        MjpgClient.clients[self._camera_id] = self
+        
+        logging.debug('mjpg client for camera %(camera_id)s connecting on port %(port)s...' % {
+                'port': self._port, 'camera_id': self._camera_id})
+    
+    def on_close(self):
+        logging.debug('connection closed for mjpg client for camera %(camera_id)s on port %(port)s' % {
+                'port': self._port, 'camera_id': self._camera_id})
+        
+        if MjpgClient.clients.pop(self._camera_id, None):
+            MjpgClient.last_access.pop(self._camera_id, None)
+            MjpgClient.last_jpg_moment.pop(self._camera_id, None)
+             
+            logging.debug('mjpg client for camera %(camera_id)s on port %(port)s removed' % {
+                    'port': self._port, 'camera_id': self._camera_id})
+         
+        if getattr(self, 'error', None) and self.error.errno != errno.ECONNREFUSED:
+            now = time.time()
+            if now - MjpgClient.last_erroneous_close_time < settings.MJPG_CLIENT_TIMEOUT:
+                logging.error('connection problem detected for mjpg client for camera %(camera_id)s on port %(port)s' % {
+                        'port': self._port, 'camera_id': self._camera_id})
+                motionctl.stop(invalidate=True) # this will close all the mjpg clients
+                motionctl.start(deferred=True)
+            MjpgClient.last_erroneous_close_time = now
+
+    def _check_error(self):
+        if self.socket is None:
+            logging.warning('mjpg client connection for camera %(camera_id)s on port %(port)s is closed' % {
+                    'port': self._port, 'camera_id': self._camera_id})
+            
+            self.close()
+            
+            return True
+            
+        error = getattr(self, 'error', None)
+        if (error is None) or (getattr(error, 'errno', None) == 0): # error could also be ESUCCESS for some reason
+            return False
+        
+        self._error(error)
+        
+        return True
+     
+    def _error(self, error):
+        logging.error('mjpg client for camera %(camera_id)s on port %(port)s error: %(msg)s' % {
+                'port': self._port, 'camera_id': self._camera_id, 'msg': unicode(error)})
+        
+        try:
+            self.close()
+        
+        except:
+            pass
+    
+    def _on_connect(self):
+        logging.debug('mjpg client for camera %(camera_id)s connected on port %(port)s' % {
+                'port': self._port, 'camera_id': self._camera_id})
+        
+        if self._username:
+            auth_header = utils.build_basic_header(self._username, self._password)
+            self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
+            
+        else:
+            self.write('GET / HTTP/1.0\r\n\r\n')
+
+        self._seek_http()
+
+    def _seek_http(self):
+        if self._check_error():
+            return
+        
+        self.read_until_regex('HTTP/1.\d \d+ ', self._on_http)
+
+    def _on_http(self, data):
+        if data.endswith('401 '):
+            self._seek_www_authenticate()
+
+        else: # no authorization required, skip to content length
+            self._seek_content_length()
+
+    def _seek_www_authenticate(self):
+        if self._check_error():
+            return
+        
+        self.read_until('WWW-Authenticate:', self._on_before_www_authenticate)
+
+    def _on_before_www_authenticate(self, data):
+        if self._check_error():
+            return
+        
+        self.read_until('\r\n', self._on_www_authenticate)
+    
+    def _on_www_authenticate(self, data):
+        if self._check_error():
+            return
+        
+        m = re.match('Basic\s*realm="([a-zA-Z0-9\-\s]+)"', data.strip())
+        if m:
+            logging.debug('mjpgclient: using basic authentication')
+            
+            auth_header = utils.build_basic_header(self._username, self._password)
+            self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
+            self._seek_http()
+
+            return
+
+        m = re.match('Digest\s*realm="([a-zA-Z0-9\-\s]+)",\s*nonce="([a-zA-Z0-9]+)"', data.strip())
+        if m:
+            logging.debug('mjpgclient: using digest authentication')
+
+            realm, nonce = m.groups()
+            self._auth_digest_state['realm'] = realm
+            self._auth_digest_state['nonce'] = nonce
+    
+            auth_header = utils.build_digest_header('GET', '/', self._username, self._password, self._auth_digest_state)
+            self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
+            self._seek_http()
+            
+            return
+
+        logging.error('mjpgclient: unknown authentication header: "%s"' % data)
+        self._seek_content_length()
+
+    def _seek_content_length(self):
+        if self._check_error():
+            return
+        
+        self.read_until('Content-Length:', self._on_before_content_length)
+    
+    def _on_before_content_length(self, data):
+        if self._check_error():
+            return
+        
+        self.read_until('\r\n\r\n', self._on_content_length)
+    
+    def _on_content_length(self, data):
+        if self._check_error():
+            return
+        
+        matches = re.findall('(\d+)', data)
+        if not matches:
+            self._error('could not find content length in mjpg header line "%(header)s"' % {
+                    'header': data})
+            
+            return
+        
+        length = int(matches[0])
+        
+        self.read_bytes(length, self._on_jpg)
+    
+    def _on_jpg(self, data):
+        MjpgClient.last_jpgs[self._camera_id] = data
+        MjpgClient.last_jpg_moment[self._camera_id] = datetime.datetime.utcnow()
+        self._seek_content_length()
+
+
+def _garbage_collector():
+    logging.debug('running garbage collector for mjpg clients...')
+    
+    now = datetime.datetime.utcnow()
+    for client in MjpgClient.clients.values():
+        camera_id = client._camera_id
+        port = client._port
+        
+        # check for last jpg moment timeout
+        last_jpg_moment = MjpgClient.last_jpg_moment.get(camera_id)
+        if last_jpg_moment is None:
+            MjpgClient.last_jpg_moment[camera_id] = now
+            
+            continue
+        
+        if client.closed():
+            continue
+
+        delta = now - last_jpg_moment
+        delta = delta.days * 86400 + delta.seconds
+        
+        if delta > settings.MJPG_CLIENT_TIMEOUT:
+            logging.error('mjpg client timed out receiving data for camera %(camera_id)s on port %(port)s' % {
+                    'camera_id': camera_id, 'port': port})
+            
+            motionctl.stop(invalidate=True) # this will close all the mjpg clients
+            motionctl.start(deferred=True)
+            
+            break
+
+        # check for last access timeout
+        last_access = MjpgClient.last_access.get(camera_id)
+        if last_access is None:
+            continue
+        
+        delta = now - last_access
+        delta = delta.days * 86400 + delta.seconds
+        
+        if settings.MJPG_CLIENT_IDLE_TIMEOUT and delta > settings.MJPG_CLIENT_IDLE_TIMEOUT:
+            logging.debug('mjpg client for camera %(camera_id)s on port %(port)s has been idle for %(timeout)s seconds, removing it' % {
+                    'camera_id': camera_id, 'port': port, 'timeout': settings.MJPG_CLIENT_IDLE_TIMEOUT})
+            
+            client.close()
+
+            continue
+        
+    io_loop = ioloop.IOLoop.instance()
+    io_loop.add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT), _garbage_collector)
+
+
+def get_jpg(camera_id):
+    if camera_id not in MjpgClient.clients:
+        # mjpg client not started yet for this camera
+        
+        logging.debug('creating mjpg client for camera %(camera_id)s' % {
+                'camera_id': camera_id})
+        
+        camera_config = config.get_camera(camera_id)
+        if not camera_config['@enabled'] or not utils.local_motion_camera(camera_config):
+            logging.error('could not start mjpg client for camera id %(camera_id)s: not enabled or not local' % {
+                    'camera_id': camera_id})
+            
+            return None
+        
+        port = camera_config['stream_port']
+        username, password = None, None
+        if camera_config.get('stream_auth_method') > 0:
+            username, password = camera_config.get('stream_authentication', ':').split(':')
+
+        client = MjpgClient(camera_id, port, username, password)
+        client.connect()
+
+    MjpgClient.last_access[camera_id] = datetime.datetime.utcnow()
+    
+    return MjpgClient.last_jpgs.get(camera_id)
+
+
+def close_all(invalidate=False):
+    for client in MjpgClient.clients.values():
+        client.close()
+    
+    if invalidate:
+        MjpgClient.clients = {}
+        MjpgClient.last_jpgs = {}
+        MjpgClient.last_jpg_moment = {}
+        MjpgClient.last_access = {}
+        MjpgClient.last_erroneous_close_time = 0
+
+
+# schedule the garbage collector
+io_loop = ioloop.IOLoop.instance()
+io_loop.add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT), _garbage_collector)
diff --git a/motioneye/motionctl.py b/motioneye/motionctl.py
new file mode 100644 (file)
index 0000000..df27ae6
--- /dev/null
@@ -0,0 +1,332 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import errno
+import logging
+import os.path
+import re
+import signal
+import subprocess
+import time
+
+from tornado.httpclient import HTTPClient, AsyncHTTPClient, HTTPRequest
+from tornado.ioloop import IOLoop
+
+import config
+import mjpgclient
+import powerctl
+import settings
+import utils
+
+
+_started = False
+_motion_binary_cache = None
+_motion_detected = {}
+
+
+def find_motion():
+    global _motion_binary_cache
+    if _motion_binary_cache:
+        return _motion_binary_cache
+    
+    if settings.MOTION_BINARY:
+        if os.path.exists(settings.MOTION_BINARY):
+            binary = settings.MOTION_BINARY
+        
+        else:
+            return None
+
+    else: # autodetect motion binary path
+        try:
+            binary = subprocess.check_output('which motion', shell=True).strip()
+        
+        except subprocess.CalledProcessError: # not found
+            return None
+
+    try:
+        help = subprocess.check_output(binary + ' -h || true', shell=True)
+    
+    except subprocess.CalledProcessError: # not found
+        return None
+    
+    result = re.findall('^motion Version ([^,]+)', help)
+    version = result and result[0] or ''
+    
+    _motion_binary_cache = (binary, version)
+    
+    return _motion_binary_cache
+
+
+def _disable_initial_motion_detection():
+    for camera_id in config.get_camera_ids():
+        camera_config = config.get_camera(camera_id)
+        if not utils.local_motion_camera(camera_config):
+            continue
+
+        if not camera_config['@motion_detection']:
+            logging.debug('motion detection disabled by config for camera with id %s' % camera_id)
+            set_motion_detection(camera_id, False)
+
+
+def start(deferred=False):
+    if deferred:
+        return IOLoop.instance().add_callback(start, deferred=False)
+
+    global _started
+    
+    _started = True
+    
+    enabled_local_motion_cameras = config.get_enabled_local_motion_cameras()
+    if running() or not enabled_local_motion_cameras:
+        return
+    
+    logging.debug('starting motion')
+    program = find_motion()
+    if not program:
+        raise Exception('motion executable could not be found')
+    
+    program, version = program  # @UnusedVariable
+    
+    logging.debug('using motion binary "%s"' % program)
+
+    motion_config_path = os.path.join(settings.CONF_PATH, 'motion.conf')
+    motion_log_path = os.path.join(settings.LOG_PATH, 'motion.log')
+    motion_pid_path = os.path.join(settings.RUN_PATH, 'motion.pid')
+    
+    args = [program,
+            '-c', motion_config_path,
+            '-n',
+            '-d']
+    
+    if settings.LOG_LEVEL == logging.DEBUG:
+        args.append('9')
+    
+    else:
+        args.append('1')
+
+    log_file = open(motion_log_path, 'w')
+    
+    process = subprocess.Popen(args, stdout=log_file, stderr=log_file, close_fds=True, cwd=settings.CONF_PATH)
+    
+    # wait 2 seconds to see that the process has successfully started
+    for i in xrange(20):  # @UnusedVariable
+        time.sleep(0.1)
+        exit_code = process.poll()
+        if exit_code is not None and exit_code != 0:
+            raise Exception('motion failed to start')
+
+    pid = process.pid
+    
+    # write the pid to file
+    with open(motion_pid_path, 'w') as f:
+        f.write(str(pid) + '\n')
+    
+    _disable_initial_motion_detection()
+    
+    # if mjpg client idle timeout is disabled, create mjpg clients for all cameras by default
+    if not settings.MJPG_CLIENT_IDLE_TIMEOUT:
+        logging.debug('creating default mjpg clients for local cameras')
+        for camera in enabled_local_motion_cameras:
+            mjpgclient.get_jpg(camera['@id'])
+
+
+def stop(invalidate=False):
+    global _started
+    
+    _started = False
+    
+    if not running():
+        return
+    
+    logging.debug('stopping motion')
+
+    mjpgclient.close_all(invalidate=invalidate)
+    
+    pid = _get_pid()
+    if pid is not None:
+        try:
+            # send the TERM signal once
+            os.kill(pid, signal.SIGTERM)
+            
+            # wait 5 seconds for the process to exit
+            for i in xrange(50):  # @UnusedVariable
+                os.waitpid(pid, os.WNOHANG)
+                time.sleep(0.1)
+
+            # send the KILL signal once
+            os.kill(pid, signal.SIGKILL)
+            
+            # wait 2 seconds for the process to exit
+            for i in xrange(20):  # @UnusedVariable
+                time.sleep(0.1)
+                os.waitpid(pid, os.WNOHANG)
+                
+            # the process still did not exit
+            if settings.ENABLE_REBOOT:
+                logging.error('could not terminate the motion process')
+                powerctl.reboot()
+
+            else:
+                raise Exception('could not terminate the motion process')
+        
+        except OSError as e:
+            if e.errno not in (errno.ESRCH, errno.ECHILD):
+                raise
+
+
+def running():
+    pid = _get_pid()
+    if pid is None:
+        return False
+    
+    try:
+        os.waitpid(pid, os.WNOHANG)
+        os.kill(pid, 0)
+        
+        # the process is running
+        return True
+    
+    except OSError as e:
+        if e.errno not in (errno.ESRCH, errno.ECHILD):
+            raise
+
+    return False
+
+
+def started():
+    return _started
+
+
+def get_motion_detection(camera_id):
+    thread_id = camera_id_to_thread_id(camera_id)
+    if thread_id is None:
+        return logging.error('could not find thread id for camera with id %s' % camera_id)
+    
+    url = 'http://127.0.0.1:7999/%(id)s/detection/status' % {'id': thread_id}
+    
+    request = HTTPRequest(url, connect_timeout=5, request_timeout=5)
+    http_client = HTTPClient()
+    try:
+        response = http_client.fetch(request)
+        if response.error:
+            raise response.error 
+    
+    except Exception as e:
+        logging.error('failed to get motion detection status for camera with id %(id)s: %(msg)s' % {
+                'id': camera_id,
+                'msg': unicode(e)})
+
+        return None
+    
+    enabled = bool(response.body.lower().count('active'))
+    
+    logging.debug('motion detection is %(what)s for camera with id %(id)s' % {
+            'what': ['disabled', 'enabled'][enabled],
+            'id': camera_id})
+    
+    return enabled
+
+
+def set_motion_detection(camera_id, enabled):
+    thread_id = camera_id_to_thread_id(camera_id)
+    if thread_id is None:
+        return logging.error('could not find thread id for camera with id %s' % camera_id)
+    
+    if not enabled:
+        _motion_detected[camera_id] = False
+    
+    logging.debug('%(what)s motion detection for camera with id %(id)s' % {
+            'what': ['disabling', 'enabling'][enabled],
+            'id': camera_id})
+    
+    url = 'http://127.0.0.1:7999/%(id)s/detection/%(enabled)s' % {
+            'id': thread_id,
+            'enabled': ['pause', 'start'][enabled]}
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to %(what)s motion detection for camera with id %(id)s: %(msg)s' % {
+                    'what': ['disable', 'enable'][enabled],
+                    'id': camera_id,
+                    'msg': utils.pretty_http_error(response)})
+        
+        else:
+            logging.debug('successfully %(what)s motion detection for camera with id %(id)s' % {
+                    'what': ['disabled', 'enabled'][enabled],
+                    'id': camera_id})
+
+    request = HTTPRequest(url, connect_timeout=4, request_timeout=4)
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, on_response)
+
+
+def is_motion_detected(camera_id):
+    return _motion_detected.get(camera_id, False)
+
+
+def set_motion_detected(camera_id, motion_detected):
+    if motion_detected:
+        logging.debug('marking motion detected for camera with id %s' % camera_id)
+
+    else:
+        logging.debug('clearing motion detected for camera with id %s' % camera_id)
+        
+    _motion_detected[camera_id] = motion_detected
+
+
+def camera_id_to_thread_id(camera_id):
+    # find the corresponding thread_id
+    # (which can be different from camera_id)
+    camera_ids = config.get_camera_ids()
+    thread_id = 0
+    for cid in camera_ids:
+        camera_config = config.get_camera(cid)
+        if utils.local_motion_camera(camera_config):
+            thread_id += 1
+        
+        if cid == camera_id:
+            return thread_id or None
+
+    return None
+    
+
+def thread_id_to_camera_id(thread_id):
+    # find the corresponding camera_id
+    # (which can be different from thread_id)
+    camera_ids = config.get_camera_ids()
+    tid = 0
+    for cid in camera_ids:
+        camera_config = config.get_camera(cid)
+        if utils.local_motion_camera(camera_config):
+            tid += 1
+            if tid == thread_id:
+                return cid
+    
+    return None
+
+
+def _get_pid():
+    motion_pid_path = os.path.join(settings.RUN_PATH, 'motion.pid')
+    
+    # read the pid from file
+    try:
+        with open(motion_pid_path, 'r') as f:
+            return int(f.readline().strip())
+        
+    except (IOError, ValueError):
+        return None
diff --git a/motioneye/ordereddict.py b/motioneye/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)
diff --git a/motioneye/powerctl.py b/motioneye/powerctl.py
new file mode 100644 (file)
index 0000000..3b82ab7
--- /dev/null
@@ -0,0 +1,78 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import logging
+import os
+import subprocess
+
+
+def _find_prog(prog):
+    try:
+        return subprocess.check_output('which %s' % prog, shell=True).strip()
+    
+    except subprocess.CalledProcessError: # not found
+        return None
+
+
+def _exec_prog(prog):
+    logging.info('executing "%s"' % prog)
+    
+    return os.system(prog) == 0
+
+
+def shut_down():
+    logging.info('shutting down')
+    
+    prog = _find_prog('poweroff')
+    if prog:
+        return _exec_prog(prog)
+    
+    prog = _find_prog('shutdown')
+    if prog:
+        return _exec_prog(prog + ' -h now')
+    
+    prog = _find_prog('systemctl')
+    if prog:
+        return _exec_prog(prog + ' poweroff')
+    
+    prog = _find_prog('init')
+    if prog:
+        return _exec_prog(prog + ' 0')
+    
+    return False
+
+
+def reboot():
+    logging.info('rebooting')
+    
+    prog = _find_prog('reboot')
+    if prog:
+        return _exec_prog(prog)
+    
+    prog = _find_prog('shutdown')
+    if prog:
+        return _exec_prog(prog + ' -r now')
+    
+    prog = _find_prog('systemctl')
+    if prog:
+        return _exec_prog(prog + ' reboot')
+    
+    prog = _find_prog('init')
+    if prog:
+        return _exec_prog(prog + ' 6')
+    
+    return False
diff --git a/motioneye/remote.py b/motioneye/remote.py
new file mode 100644 (file)
index 0000000..a10599a
--- /dev/null
@@ -0,0 +1,646 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import functools
+import json
+import logging
+import re
+
+from tornado.httpclient import AsyncHTTPClient, HTTPRequest
+
+import settings
+import utils
+
+_DOUBLE_SLASH_REGEX = re.compile('//+')
+
+
+def _make_request(scheme, host, port, username, password, uri, method='GET', data=None, query=None, timeout=None):
+    uri = _DOUBLE_SLASH_REGEX.sub('/', uri)
+    url = '%(scheme)s://%(host)s%(port)s%(uri)s' % {
+            'scheme': scheme,
+            'host': host,
+            'port': ':' + str(port) if port else '',
+            'uri': uri or ''}
+    
+    query = dict(query or {})
+    query['_username'] = username or ''
+    query['_admin'] = 'true' # always use the admin account
+    
+    if url.count('?'):
+        url += '&'
+        
+    else:
+        url += '?'
+    
+    url += '&'.join([(n + '=' + v) for (n, v) in query.iteritems()])
+    url += '&_signature=' + utils.compute_signature(method, url, data, password)
+
+    if timeout is None:
+        timeout = settings.REMOTE_REQUEST_TIMEOUT
+        
+    return HTTPRequest(url, method, body=data, connect_timeout=timeout, request_timeout=timeout)
+
+
+def _callback_wrapper(callback):
+    @functools.wraps(callback)
+    def wrapper(response):
+        try:
+            decoded = json.loads(response.body)
+            if decoded['error'] == 'unauthorized':
+                response.error = 'Authentication Error'
+                
+            elif decoded['error']:
+                response.error = decoded['error']
+
+        except:
+            pass
+        
+        return callback(response)
+    
+    return wrapper
+
+
+def pretty_camera_url(local_config, camera=True):
+    scheme = local_config.get('@scheme', local_config.get('scheme')) or 'http'
+    host = local_config.get('@host', local_config.get('host'))
+    port = local_config.get('@port', local_config.get('port'))
+    uri = local_config.get('@uri', local_config.get('uri')) or ''
+
+    url = scheme + '://' + host
+    if port and str(port) not in ['80', '443']:
+        url += ':' + str(port)
+    
+    if uri:
+        url += uri
+        
+    if url.endswith('/'):
+        url = url[:-1]
+
+    if camera:
+        if camera is True:
+            url += '/config/' + str(local_config.get('@remote_camera_id', local_config.get('remote_camera_id')))
+        
+        else:
+            url += '/config/' + str(camera)
+
+    return url
+
+
+def _remote_params(local_config):
+    return (
+            local_config.get('@scheme', local_config.get('scheme')) or 'http',
+            local_config.get('@host', local_config.get('host')),
+            local_config.get('@port', local_config.get('port')),
+            local_config.get('@username', local_config.get('username')),
+            local_config.get('@password', local_config.get('password')),
+            local_config.get('@uri', local_config.get('uri')) or '',
+            local_config.get('@remote_camera_id', local_config.get('remote_camera_id')))
+
+
+def list(local_config, callback):
+    scheme, host, port, username, password, uri, _ = _remote_params(local_config)
+    
+    logging.debug('listing remote cameras on %(url)s' % {
+            'url': pretty_camera_url(local_config, camera=False)})
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/config/list/')
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to list remote cameras on %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config, camera=False),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+        
+        try:
+            response = json.loads(response.body)
+            
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config, camera=False),
+                    'msg': unicode(e)})
+            
+            return callback(error=unicode(e))
+        
+        cameras = response['cameras']
+        
+        # filter out simple mjpeg cameras
+        cameras = [c for c in cameras if c['proto'] != 'mjpeg']
+        
+        callback(cameras)
+    
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+    
+
+def get_config(local_config, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+     
+    logging.debug('getting config for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/config/%(id)s/get/' % {'id': camera_id})
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to get config for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+    
+        try:
+            response = json.loads(response.body)
+        
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config),
+                    'msg': unicode(e)})
+            
+            return callback(error=unicode(e))
+        
+        response['host'] = host
+        response['port'] = port
+            
+        callback(response)
+    
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+    
+
+def set_config(local_config, ui_config, callback):
+    scheme = local_config.get('@scheme', local_config.get('scheme'))
+    host = local_config.get('@host', local_config.get('host')) 
+    port = local_config.get('@port', local_config.get('port'))
+    username = local_config.get('@username', local_config.get('username'))
+    password = local_config.get('@password', local_config.get('password'))
+    uri = local_config.get('@uri', local_config.get('uri')) or ''
+    camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
+
+    logging.debug('setting config for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    ui_config = json.dumps(ui_config)
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/config/%(id)s/set/' % {'id': camera_id}, method='POST', data=ui_config)
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to set config for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+    
+        callback()
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def set_preview(local_config, controls, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('setting preview for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    data = json.dumps(controls)
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/config/%(id)s/set_preview/' % {'id': camera_id}, method='POST', data=data)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to set preview for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+        
+            return callback(error=utils.pretty_http_error(response))
+        
+        callback()
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def get_current_picture(local_config, width, height, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('getting current picture for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    query = {}
+    
+    if width:
+        query['width'] = str(width)
+        
+    if height:
+        query['height'] = str(height)
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/current/' % {'id': camera_id}, query=query)
+    
+    def on_response(response):
+        motion_detected = False
+        
+        cookies = response.headers.get('Set-Cookie')
+        if cookies:
+            cookies = cookies.split(';')
+            cookies = [[i.strip() for i in c.split('=')] for c in cookies]
+            cookies = dict([c for c in cookies if len(c) == 2])
+            motion_detected = cookies.get('motion_detected_' + str(camera_id)) == 'true'
+        
+        if response.error:
+            logging.error('failed to get current picture for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+
+        callback(motion_detected, response.body)
+    
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def list_media(local_config, media_type, prefix, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('getting media list for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    query = {}
+    if prefix is not None:
+        query['prefix'] = prefix
+    
+    # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list
+    request = _make_request(scheme, host, port, username, password, uri + '/%(media_type)s/%(id)s/list/' % {
+            'id': camera_id, 'media_type': media_type}, query=query, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to get media list for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+        
+        try:
+            response = json.loads(response.body)
+            
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config),
+                    'msg': unicode(e)})
+            
+            return callback(error=unicode(e))
+        
+        return callback(response)
+    
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def get_media_content(local_config, filename, media_type, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('downloading file %(filename)s of remote camera %(id)s on %(url)s' % {
+            'filename': filename,
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    uri += '/%(media_type)s/%(id)s/download/%(filename)s' % {
+            'media_type': media_type,
+            'id': camera_id,
+            'filename': filename}
+    
+    # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list
+    request = _make_request(scheme, host, port, username, password, uri, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to download file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'filename': filename,
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+        
+        return callback(response.body)
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def make_zipped_content(local_config, media_type, group, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('preparing zip file for group %(group)s of remote camera %(id)s on %(url)s' % {
+            'group': group,
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+
+    prepare_uri = uri + '/%(media_type)s/%(id)s/zipped/%(group)s/' % {
+            'media_type': media_type,
+            'id': camera_id,
+            'group': group}
+    # timeout here is 100 times larger than usual - we expect a big delay
+    request = _make_request(scheme, host, port, username, password, prepare_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to prepare zip file for group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'group': group,
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+
+            return callback(error=utils.pretty_http_error(response))
+        
+        try:
+            key = json.loads(response.body)['key']
+
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config),
+                    'msg': unicode(e)})
+
+            return callback(error=unicode(e))
+
+        callback({'key': key})
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def get_zipped_content(local_config, media_type, key, group, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('downloading zip file for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/%(media_type)s/%(id)s/zipped/%(group)s/?key=%(key)s' % {
+            'media_type': media_type,
+            'group': group,
+            'id': camera_id,
+            'key': key},
+            timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to download zip file for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+
+            return callback(error=utils.pretty_http_error(response))
+
+        callback({
+            'data': response.body,
+            'content_type': response.headers.get('Content-Type'),
+            'content_disposition': response.headers.get('Content-Disposition')
+        })
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def make_timelapse_movie(local_config, framerate, interval, group, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+
+    logging.debug('making timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s' % {
+            'group': group,
+            'id': camera_id,
+            'framerate': framerate,
+            'int': interval,
+            'url': pretty_camera_url(local_config)})
+
+    uri += '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s&framerate=%(framerate)s' % {
+            'id': camera_id,
+            'int': interval,
+            'framerate': framerate,
+            'group': group}
+    
+    request = _make_request(scheme, host, port, username, password, uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to make timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s: %(msg)s' % {
+                    'group': group,
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'int': interval,
+                    'framerate': framerate,
+                    'msg': utils.pretty_http_error(response)})
+
+            return callback(error=utils.pretty_http_error(response))
+        
+        try:
+            response = json.loads(response.body)
+
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config),
+                    'msg': unicode(e)})
+
+            return callback(error=unicode(e))
+        
+        callback(response)
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def check_timelapse_movie(local_config, group, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('checking timelapse movie status for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?check=true' % {
+            'id': camera_id,
+            'group': group})
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to check timelapse movie status for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+
+            return callback(error=utils.pretty_http_error(response))
+        
+        try:
+            response = json.loads(response.body)
+
+        except Exception as e:
+            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
+                    'url': pretty_camera_url(local_config),
+                    'msg': unicode(e)})
+
+            return callback(error=unicode(e))
+        
+        callback(response)
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def get_timelapse_movie(local_config, key, group, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('downloading timelapse movie for remote camera %(id)s on %(url)s' % {
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % {
+            'id': camera_id,
+            'group': group,
+            'key': key},
+            timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to download timelapse movie for remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+
+            return callback(error=utils.pretty_http_error(response))
+
+        callback({
+            'data': response.body,
+            'content_type': response.headers.get('Content-Type'),
+            'content_disposition': response.headers.get('Content-Disposition')
+        })
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def get_media_preview(local_config, filename, media_type, width, height, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('getting file preview for %(filename)s of remote camera %(id)s on %(url)s' % {
+            'filename': filename,
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    uri += '/%(media_type)s/%(id)s/preview/%(filename)s' % {
+            'media_type': media_type,
+            'id': camera_id,
+            'filename': filename}
+    
+    query = {}
+    
+    if width:
+        query['width'] = str(width)
+        
+    if height:
+        query['height'] = str(height)
+    
+    request = _make_request(scheme, host, port, username, password, uri, query=query)
+    
+    def on_response(response):
+        if response.error:
+            logging.error('failed to get file preview for %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'filename': filename,
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+        
+        callback(response.body)
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def del_media_content(local_config, filename, media_type, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('deleting file %(filename)s of remote camera %(id)s on %(url)s' % {
+            'filename': filename,
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    uri += '/%(media_type)s/%(id)s/delete/%(filename)s' % {
+            'media_type': media_type,
+            'id': camera_id,
+            'filename': filename}
+
+    request = _make_request(scheme, host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to delete file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'filename': filename,
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+        
+        callback()
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
+
+
+def del_media_group(local_config, group, media_type, callback):
+    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
+    
+    logging.debug('deleting group %(group)s of remote camera %(id)s on %(url)s' % {
+            'group': group,
+            'id': camera_id,
+            'url': pretty_camera_url(local_config)})
+    
+    uri += '/%(media_type)s/%(id)s/delete_all/%(group)s/' % {
+            'media_type': media_type,
+            'id': camera_id,
+            'group': group}
+
+    request = _make_request(scheme, host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT)
+
+    def on_response(response):
+        if response.error:
+            logging.error('failed to delete group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
+                    'group': group,
+                    'id': camera_id,
+                    'url': pretty_camera_url(local_config),
+                    'msg': utils.pretty_http_error(response)})
+            
+            return callback(error=utils.pretty_http_error(response))
+        
+        callback()
+
+    http_client = AsyncHTTPClient()
+    http_client.fetch(request, _callback_wrapper(on_response))
diff --git a/motioneye/sendmail.py b/motioneye/sendmail.py
new file mode 100755 (executable)
index 0000000..4844c08
--- /dev/null
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import logging
+import os
+import re
+import smtplib
+import socket
+import sys
+import time
+
+from email import Encoders
+from email.mime.text import MIMEText
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEBase import MIMEBase
+from tornado.ioloop import IOLoop
+
+import settings
+
+from motioneye import _configure_settings, _configure_logging, _configure_signals
+
+_configure_settings()
+_configure_signals()
+_configure_logging(module='sendmail')
+
+import config
+import mediafiles
+import tzctl
+
+
+messages = {
+    'motion_start': 'Motion has been detected by camera "%(camera)s/%(hostname)s" at %(moment)s (%(timezone)s).'
+}
+
+subjects = {
+    'motion_start': 'motionEye: motion detected by "%(camera)s"'
+}
+
+
+def send_mail(server, port, account, password, tls, to, subject, message, files):
+    conn = smtplib.SMTP(server, port, timeout=getattr(settings, 'SMTP_TIMEOUT', 60))
+    if tls:
+        conn.starttls()
+    
+    if account and password:
+        conn.login(account, password)
+    
+    _from = account or 'motioneye@' + socket.gethostname()
+    
+    email = MIMEMultipart()
+    email['Subject'] = subject
+    email['From'] = _from
+    email['To'] = ', '.join(to)
+    email.attach(MIMEText(message))
+    
+    for file in reversed(files):
+        part = MIMEBase('image', 'jpeg')
+        with open(file, 'rb') as f:
+            part.set_payload(f.read())
+        
+        Encoders.encode_base64(part)
+        part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(file))
+        email.attach(part)
+    
+    if files:
+        logging.debug('attached %d pictures' % len(files))
+
+    logging.debug('sending email message')
+    conn.sendmail(_from, to, email.as_string())
+    conn.quit()
+
+
+def make_message(subject, message, camera_id, moment, timespan, callback):
+    camera_config = config.get_camera(camera_id)
+    
+    def on_media_files(media_files):
+        logging.debug('got media files')
+        
+        timestamp = time.mktime(moment.timetuple())
+
+        media_files = [m for m in media_files if abs(m['timestamp'] - timestamp) < timespan] # filter out non-recent media files
+        media_files.sort(key=lambda m: m['timestamp'], reverse=True)
+        media_files = [os.path.join(camera_config['target_dir'], re.sub('^/', '', m['path'])) for m in media_files]
+        
+        logging.debug('selected %d pictures' % len(media_files))
+
+        format_dict = {
+            'camera': camera_config['@name'],
+            'hostname': socket.gethostname(),
+            'moment': moment.strftime('%Y-%m-%d %H:%M:%S'),
+        }
+        
+        if settings.LOCAL_TIME_FILE:
+            format_dict['timezone'] = tzctl._get_time_zone()
+        
+        else:
+            format_dict['timezone'] = 'local time'
+    
+        m = message % format_dict
+        s = subject % format_dict
+        s = s.replace('\n', ' ')
+    
+        m += '\n\n'
+        m += 'motionEye.'
+        
+        callback(s, m, media_files)
+
+    if not timespan:
+        return on_media_files([])
+
+    logging.debug('creating email message')
+
+    time.sleep(timespan) # give motion some time to create motion pictures
+    mediafiles.list_media(camera_config, media_type='picture', callback=on_media_files)
+
+
+def print_usage():
+    print 'Usage: sendmail.py <server> <port> <account> <password> <tls> <to> <msg_id> <camera_id> <moment> <frames> [timespan]'
+
+
+if __name__ == '__main__':
+    if len(sys.argv) < 10:
+        print_usage()
+        sys.exit(-1)
+    
+    server = sys.argv[1]
+    port = int(sys.argv[2]) 
+    account = sys.argv[3]
+    password = sys.argv[4]
+    tls = sys.argv[5].lower() == 'true'
+    to = sys.argv[6]
+    msg_id = sys.argv[7]
+    camera_id = sys.argv[8]
+    moment = sys.argv[9]
+    try:
+        timespan = int(sys.argv[10])
+
+    except:
+        timespan = 0
+
+    logging.debug('hello!')
+
+    message = messages.get(msg_id)
+    subject = subjects.get(msg_id)
+    if not message or not subject:
+        logging.error('unknown message id')
+        sys.exit(-1)
+    
+    moment = datetime.datetime.strptime(moment, '%Y-%m-%dT%H:%M:%S')
+    
+    logging.debug('server = %s' % server)
+    logging.debug('port = %s' % port)
+    logging.debug('account = %s' % account)
+    logging.debug('password = ******')
+    logging.debug('server = %s' % server)
+    logging.debug('tls = %s' % tls)
+    logging.debug('to = %s' % to)
+    logging.debug('msg_id = %s' % msg_id)
+    logging.debug('camera_id = %s' % camera_id)
+    logging.debug('moment = %s' % moment.strftime('%Y-%m-%d %H:%M:%S'))
+    logging.debug('smtp timeout = %d' % settings.SMTP_TIMEOUT)
+    logging.debug('timespan = %d' % timespan)
+    
+    if not to:
+        logging.info('no email address specified')
+        sys.exit(0)
+
+    to = [t.strip() for t in re.split('[,;| ]', to)]
+    to = [t for t in to if t]
+
+    io_loop = IOLoop.instance()
+    
+    def on_message(subject, message, files):
+        try:
+            send_mail(server, port, account, password, tls, to, subject, message, files)
+            logging.info('email sent')
+        
+        except Exception as e:
+            logging.error('failed to send mail: %s' % e, exc_info=True)
+
+        io_loop.stop()
+    
+    def ioloop_timeout():
+        io_loop.stop()
+    
+    make_message(subject, message, camera_id, moment, timespan, on_message)
+
+    io_loop.add_timeout(datetime.timedelta(seconds=settings.SMTP_TIMEOUT), ioloop_timeout)
+    io_loop.start()
+
+    logging.debug('bye!')
\ No newline at end of file
diff --git a/motioneye/server.py b/motioneye/server.py
new file mode 100644 (file)
index 0000000..76b385b
--- /dev/null
@@ -0,0 +1,351 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import datetime
+import multiprocessing
+import os.path
+import signal
+import sys 
+
+from tornado.httpclient import AsyncHTTPClient
+from tornado.web import Application
+
+import handlers
+import logging
+import settings
+import template
+
+
+def load_settings():
+    # TODO use optparse
+#     length = len(sys.argv) - 1
+#     for i in xrange(length):
+#         arg = sys.argv[i + 1]
+#         
+#         if not arg.startswith('--'):
+#             continue
+#         
+#         next_arg = None
+#         if i < length - 1:
+#             next_arg = sys.argv[i + 2]
+#         
+#         name = arg[2:].upper().replace('-', '_')
+#         
+#         if hasattr(settings, name):
+#             curr_value = getattr(settings, name)
+#             
+#             if next_arg.lower() == 'debug':
+#                 next_arg = logging.DEBUG
+#             
+#             elif next_arg.lower() == 'info':
+#                 next_arg = logging.INFO
+#             
+#             elif next_arg.lower() == 'warn':
+#                 next_arg = logging.WARN
+#             
+#             elif next_arg.lower() == 'error':
+#                 next_arg = logging.ERROR
+#             
+#             elif next_arg.lower() == 'fatal':
+#                 next_arg = logging.FATAL
+#             
+#             elif next_arg.lower() == 'true':
+#                 next_arg = True
+#             
+#             elif next_arg.lower() == 'false':
+#                 next_arg = False
+#             
+#             elif isinstance(curr_value, int):
+#                 next_arg = int(next_arg)
+#             
+#             elif isinstance(curr_value, float):
+#                 next_arg = float(next_arg)
+# 
+#             setattr(settings, name, next_arg)
+#         
+#         else:
+#             return arg[2:]
+
+    if not os.path.exists(settings.CONF_PATH):
+        logging.fatal('config directory "%s" does not exist' % settings.CONF_PATH)
+        sys.exit(-1)
+    
+    if not os.path.exists(settings.RUN_PATH):
+        logging.fatal('pid directory "%s" does not exist' % settings.RUN_PATH)
+        sys.exit(-1)
+
+    if not os.path.exists(settings.LOG_PATH):
+        logging.fatal('log directory "%s" does not exist' % settings.LOG_PATH)
+        sys.exit(-1)
+
+    if not os.path.exists(settings.MEDIA_PATH):
+        logging.fatal('media directory "%s" does not exist' % settings.MEDIA_PATH)
+        sys.exit(-1)
+
+
+def configure_signals():
+    def bye_handler(signal, frame):
+        import tornado.ioloop
+        
+        logging.info('interrupt signal received, shutting down...')
+
+        # shut down the IO loop if it has been started
+        ioloop = tornado.ioloop.IOLoop.instance()
+        ioloop.stop()
+        
+    def child_handler(signal, frame):
+        # this is required for the multiprocessing mechanism to work
+        multiprocessing.active_children()
+
+    signal.signal(signal.SIGINT, bye_handler)
+    signal.signal(signal.SIGTERM, bye_handler)
+    signal.signal(signal.SIGCHLD, child_handler)
+
+
+def configure_logging(module=None):
+    if module:
+        format = '%(asctime)s: [{module}] %(levelname)s: %(message)s'.format(module=module)
+        
+    else:
+        format = '%(asctime)s: %(levelname)s: %(message)s'
+
+    logging.basicConfig(filename=None, level=settings.LOG_LEVEL,
+            format=format, datefmt='%Y-%m-%d %H:%M:%S')
+
+    logging.getLogger('tornado').setLevel(logging.WARN)
+
+
+def configure_tornado():
+    AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient', max_clients=16)
+
+
+def test_requirements():
+    if os.geteuid() != 0:
+        if settings.SMB_SHARES:
+            print('SMB_SHARES require root privileges')
+            return False
+
+        if settings.ENABLE_REBOOT:
+            print('reboot requires root privileges')
+            return False
+
+    try:
+        import tornado  # @UnusedImport
+
+    except ImportError:
+        logging.fatal('please install tornado version 3.1 or greater')
+        sys.exit(-1)
+
+    try:
+        import jinja2  # @UnusedImport
+
+    except ImportError:
+        logging.fatal('please install jinja2')
+        sys.exit(-1)
+
+    try:
+        import PIL.Image  # @UnusedImport
+
+    except ImportError:
+        logging.fatal('please install pillow or PIL')
+        sys.exit(-1)
+
+    try:
+        import pycurl  # @UnusedImport
+
+    except ImportError:
+        logging.fatal('please install pycurl')
+        sys.exit(-1)
+    
+    import mediafiles
+    has_ffmpeg = mediafiles.find_ffmpeg() is not None
+    
+    import motionctl
+    has_motion = motionctl.find_motion() is not None
+    
+    import v4l2ctl
+    has_v4lutils = v4l2ctl.find_v4l2_ctl() is not None
+
+    import smbctl
+    if settings.SMB_SHARES and smbctl.find_mount_cifs() is None:
+        logging.fatal('please install cifs-utils')
+        sys.exit(-1)
+
+    if not has_ffmpeg:
+        logging.info('ffmpeg not installed')
+
+    if not has_motion:
+        logging.info('motion not installed')
+
+    if not has_v4lutils:
+        logging.info('v4l-utils not installed')
+
+        
+def start_motion():
+    import tornado.ioloop
+    import config
+    import motionctl
+
+    ioloop = tornado.ioloop.IOLoop.instance()
+    
+    # add a motion running checker
+    def checker():
+        if ioloop._stopped:
+            return
+            
+        if not motionctl.running() and motionctl.started() and config.get_enabled_local_motion_cameras():
+            try:
+                logging.error('motion not running, starting it')
+                motionctl.start()
+            
+            except Exception as e:
+                logging.error('failed to start motion: %(msg)s' % {
+                        'msg': unicode(e)}, exc_info=True)
+
+        ioloop.add_timeout(datetime.timedelta(seconds=settings.MOTION_CHECK_INTERVAL), checker)
+    
+    motionctl.start()
+        
+    ioloop.add_timeout(datetime.timedelta(seconds=settings.MOTION_CHECK_INTERVAL), checker)
+
+
+def start_cleanup():
+    import cleanup
+
+    cleanup.start()
+    logging.info('cleanup started')
+
+
+def start_wsswitch():
+    import wsswitch
+
+    wsswitch.start()
+    logging.info('wsswitch started')
+
+
+def start_thumbnailer():
+    import thumbnailer
+
+    thumbnailer.start()
+    logging.info('thumbnailer started')
+
+
+def run_server():
+    import cleanup
+    import motionctl
+    import thumbnailer
+    import tornado.ioloop
+    import smbctl
+
+    application.listen(settings.PORT, settings.LISTEN)
+    logging.info('server started')
+    
+    tornado.ioloop.IOLoop.instance().start()
+
+    logging.info('server stopped')
+    
+    if thumbnailer.running():
+        thumbnailer.stop()
+        logging.info('thumbnailer stopped')
+
+    if cleanup.running():
+        cleanup.stop()
+        logging.info('cleanup stopped')
+
+    if motionctl.running():
+        motionctl.stop()
+        logging.info('motion stopped')
+    
+    if settings.SMB_SHARES:
+        smbctl.umount_all()
+        logging.info('SMB shares unmounted')
+
+
+def log_request(handler):
+    if handler.get_status() < 400:
+        log_method = logging.debug
+    
+    elif handler.get_status() < 500:
+        log_method = logging.warning
+    
+    else:
+        log_method = logging.error
+    
+    request_time = 1000.0 * handler.request.request_time()
+    log_method("%d %s %.2fms", handler.get_status(),
+               handler._request_summary(), request_time)
+
+
+application = Application(
+    [
+        (r'^/$', handlers.MainHandler),
+        (r'^/config/main/(?P<op>set|get)/?$', handlers.ConfigHandler),
+        (r'^/config/(?P<camera_id>\d+)/(?P<op>get|set|rem|set_preview)/?$', handlers.ConfigHandler),
+        (r'^/config/(?P<op>add|list|backup|restore)/?$', handlers.ConfigHandler),
+        (r'^/picture/(?P<camera_id>\d+)/(?P<op>current|list|frame)/?$', handlers.PictureHandler),
+        (r'^/picture/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.PictureHandler),
+        (r'^/picture/(?P<camera_id>\d+)/(?P<op>zipped|timelapse|delete_all)/(?P<group>.+?)/?$', handlers.PictureHandler),
+        (r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
+        (r'^/movie/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.MovieHandler),
+        (r'^/movie/(?P<camera_id>\d+)/(?P<op>delete_all)/(?P<group>.+?)/?$', handlers.MovieHandler),
+        (r'^/_relay_event/?$', handlers.RelayEventHandler),
+        (r'^/log/(?P<name>\w+)/?$', handlers.LogHandler),
+        (r'^/update/?$', handlers.UpdateHandler),
+        (r'^/power/(?P<op>shutdown|reboot)/?$', handlers.PowerHandler),
+        (r'^/version/?$', handlers.VersionHandler),
+        (r'^/login/?$', handlers.LoginHandler),
+        (r'^.*$', handlers.NotFoundHandler),
+    ],
+    debug=False,
+    log_function=log_request,
+    static_path=settings.STATIC_PATH,
+    static_url_prefix=settings.STATIC_URL
+)
+
+template.add_context('STATIC_URL', settings.STATIC_URL)
+
+
+def main():
+    import motioneye
+    
+    load_settings()
+    configure_signals()
+    configure_logging()
+    test_requirements()
+    configure_tornado()
+
+    logging.info('hello! this is motionEye %s' % motioneye.VERSION)
+    
+    if settings.SMB_SHARES:
+        import smbctl
+
+        stop, start = smbctl.update_mounts()  # @UnusedVariable
+        if start:
+            start_motion()
+
+    else:
+        start_motion()
+
+    start_cleanup()
+    start_wsswitch()
+
+    if settings.THUMBNAILER_INTERVAL:
+        start_thumbnailer()
+
+    run_server()
+
+    logging.info('bye!')
diff --git a/motioneye/settings.py b/motioneye/settings.py
new file mode 100644 (file)
index 0000000..9e7f68b
--- /dev/null
@@ -0,0 +1,99 @@
+
+import logging
+import os.path
+import sys
+
+import motioneye
+
+# the root directory of the project
+PROJECT_PATH = os.path.dirname(motioneye.__file__)
+
+# the templates directory
+TEMPLATE_PATH = os.path.join(PROJECT_PATH, 'templates')
+
+# the static files directory
+STATIC_PATH = os.path.join(PROJECT_PATH, 'static')
+
+# static files (.css, .js etc) are served at this root url
+STATIC_URL = '/static/'
+
+# path to the config directory; must be writable
+CONF_PATH = [sys.prefix, ''][sys.prefix == '/usr']  + '/etc/motioneye'
+
+# pid files go here
+for d in ['/run', '/var/run', '/tmp', '/var/tmp']:
+    if os.path.exists(d):
+        RUN_PATH = d
+        break
+    
+else:
+    RUN_PATH = PROJECT_PATH
+
+# log files go here
+for d in ['/log', '/var/log', '/tmp', '/var/tmp']:
+    if os.path.exists(d):
+        LOG_PATH = d
+        break
+    
+else:
+    LOG_PATH = RUN_PATH
+
+# default output path for media files
+MEDIA_PATH = RUN_PATH
+
+# path to motion binary (automatically detected if not set)
+MOTION_BINARY = None
+
+# the log level
+LOG_LEVEL = logging.INFO
+
+# IP addresses to listen on
+LISTEN = '0.0.0.0'
+
+# the TCP port to listen on
+PORT = 8765
+
+# interval in seconds at which motionEye checks the SMB mounts
+MOUNT_CHECK_INTERVAL = 300
+
+# interval in seconds at which motionEye checks if motion is running
+MOTION_CHECK_INTERVAL = 10
+
+# interval in seconds at which the janitor is called to remove old pictures and movies
+CLEANUP_INTERVAL = 43200
+
+# interval in seconds at which the thumbnail mechanism runs (set to 0 to disable) 
+THUMBNAILER_INTERVAL = 60
+
+# timeout in seconds when waiting for response from a remote motionEye server
+REMOTE_REQUEST_TIMEOUT = 10
+
+# timeout in seconds when waiting for mjpg data from the motion daemon
+MJPG_CLIENT_TIMEOUT = 10
+
+# timeout in seconds after which an idle mjpg client is removed (set to 0 to disable)
+MJPG_CLIENT_IDLE_TIMEOUT = 10
+
+# enable SMB shares (requires root) 
+SMB_SHARES = False
+
+# the directory where the SMB mounts will be created
+SMB_MOUNT_ROOT = '/media'
+
+# path to a wpa_supplicant.conf file if wifi settings UI is desired
+WPA_SUPPLICANT_CONF = None
+
+# path to a localtime file if time zone settings UI is desired
+LOCAL_TIME_FILE = None
+
+# enables shutdown and rebooting after changing system settings (such as wifi settings or system updates)
+ENABLE_REBOOT = False
+
+# the timeout in seconds to use when talking to a SMTP server
+SMTP_TIMEOUT = 60
+
+# the time to wait for zip file creation
+ZIP_TIMEOUT = 500
+
+# enable adding and removing cameras from UI
+ADD_REMOVE_CAMERAS = True
diff --git a/motioneye/smbctl.py b/motioneye/smbctl.py
new file mode 100644 (file)
index 0000000..12e4505
--- /dev/null
@@ -0,0 +1,230 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import logging
+import os
+import re
+import subprocess
+import time
+
+from tornado import ioloop
+
+import config
+import settings
+
+
+def find_mount_cifs():
+    try:
+        return subprocess.check_output('which mount.cifs', shell=True).strip()
+    
+    except subprocess.CalledProcessError: # not found
+        return None
+
+
+def make_mount_point(server, share, username):
+    server = re.sub('[^a-zA-Z0-9]', '_', server).lower()
+    share = re.sub('[^a-zA-Z0-9]', '_', share).lower()
+    
+    if username:
+        username = re.sub('[^a-zA-Z0-9]', '_', username).lower()
+        mount_point = os.path.join(settings.SMB_MOUNT_ROOT, 'motioneye_%s_%s_%s' % (server, share, username))
+    
+    else:
+        mount_point = os.path.join(settings.SMB_MOUNT_ROOT, 'motioneye_%s_%s' % (server, share))
+
+    return mount_point
+
+
+def _is_motioneye_mount(mount_point):
+    mount_point_root = os.path.join(settings.SMB_MOUNT_ROOT, 'motioneye_')
+    return bool(re.match('^' + mount_point_root + '\w+$', mount_point))
+
+
+def list_mounts():
+    logging.debug('listing smb mounts...')
+    
+    mounts = []
+    with open('/proc/mounts', 'r') as f:
+        for line in f:
+            line = line.strip()
+            if not line:
+                continue
+            parts = line.split()
+            if len(parts) < 4:
+                continue
+            
+            target = parts[0]
+            mount_point = parts[1]
+            fstype = parts[2]
+            opts = ' '.join(parts[3:])
+            
+            if fstype != 'cifs':
+                continue
+            
+            if not _is_motioneye_mount(mount_point):
+                continue
+            
+            match = re.match('//([^/]+)/(.+)', target)
+            if not match:
+                continue
+            
+            if len(match.groups()) != 2:
+                continue
+            
+            server, share = match.groups()
+            share = share.replace('\\040', ' ') # spaces are reported oddly by /proc/mounts
+            
+            match = re.search('username=([\w\s]+)', opts)
+            if match:
+                username = match.group(1)
+            
+            else:
+                username = None
+                
+            logging.debug('found smb mount "//%s/%s" at "%s"' % (server, share, mount_point))
+            
+            mounts.append({
+                'server': server,
+                'share': share,
+                'username': username,
+                'mount_point': mount_point
+            })
+
+    return mounts
+
+
+def mount(server, share, username, password):
+    mount_point = make_mount_point(server, share, username)
+    
+    logging.debug('making sure mount point "%s" exists' % mount_point)
+    
+    if not os.path.exists(mount_point):    
+        os.makedirs(mount_point)
+        
+    if username:
+        opts = 'username=%s,password=%s' % (username, password)
+        sec_types = ['ntlm', 'ntlmv2', 'none']
+
+    else:
+        opts = 'guest'
+        sec_types = ['none', 'ntlm', 'ntlmv2']
+
+    for sec in sec_types:
+        actual_opts = opts + ',sec=' + sec
+        try:
+            logging.debug('mounting "//%s/%s" at "%s" (sec=%s)' % (server, share, mount_point, sec))
+            subprocess.check_call('mount.cifs "//%s/%s" "%s" -o "%s"' % (server, share, mount_point, actual_opts), shell=True)
+            break
+
+        except subprocess.CalledProcessError:
+            pass
+            
+    else:
+        logging.error('failed to mount smb share "//%s/%s" at "%s"' % (server, share, mount_point))
+        return None
+    
+    # test to see if mount point is writable
+    try:
+        path = os.path.join(mount_point, '.motioneye_' + str(int(time.time())))
+        os.mkdir(path)
+        os.rmdir(path)
+        logging.debug('directory at "%s" is writable' % mount_point)
+    
+    except:
+        logging.error('directory at "%s" is not writable' % mount_point)
+        
+        return None
+    
+    return mount_point
+
+
+def umount(server, share, username):
+    mount_point = make_mount_point(server, share, username)
+    logging.debug('unmounting "//%s/%s" from "%s"' % (server, share, mount_point))
+    
+    try:
+        subprocess.check_call('umount "%s"' % mount_point, shell=True)
+
+    except subprocess.CalledProcessError:
+        logging.error('failed to unmount smb share "//%s/%s" from "%s"' % (server, share, mount_point))
+        
+        return False
+    
+    try:
+        os.rmdir(mount_point)
+    
+    except Exception as e:
+        logging.error('failed to remove smb mount point "%s": %s' % (mount_point, e))
+        
+        return False
+        
+    return True
+
+
+def update_mounts():
+    network_shares = config.get_network_shares()
+    
+    mounts = list_mounts()
+    mounts = dict(((m['server'], m['share'], m['username'] or ''), False) for m in mounts)
+    
+    should_stop = False # indicates that motion should be stopped immediately
+    should_start = True # indicates that motion can be started afterwards
+    for network_share in network_shares:
+        key = (network_share['server'], network_share['share'], network_share['username'] or '')
+        if key in mounts: # found
+            mounts[key] = True
+        
+        else: # needs to be mounted
+            should_stop = True
+            if not mount(network_share['server'], network_share['share'], network_share['username'], network_share['password']):
+                should_start = False
+    
+    # unmount the no longer necessary mounts
+    for (server, share, username), required in mounts.items():
+        if not required:
+            umount(server, share, username)
+            should_stop = True
+    
+    return (should_stop, should_start)
+
+
+def umount_all():
+    for mount in list_mounts():
+        umount(mount['server'], mount['share'], mount['username'])
+
+
+def _check_mounts():
+    import motionctl
+    
+    logging.debug('checking SMB mounts...')
+    
+    stop, start = update_mounts()
+    if stop:
+        motionctl.stop()
+
+    if start:
+        motionctl.start()
+        
+    io_loop = ioloop.IOLoop.instance()
+    io_loop.add_timeout(datetime.timedelta(seconds=settings.MOUNT_CHECK_INTERVAL), _check_mounts)
+
+
+if settings.SMB_SHARES:
+    # schedule the mount checker
+    io_loop = ioloop.IOLoop.instance()
+    io_loop.add_timeout(datetime.timedelta(seconds=settings.MOUNT_CHECK_INTERVAL), _check_mounts)
diff --git a/motioneye/static/css/frame.css b/motioneye/static/css/frame.css
new file mode 100644 (file)
index 0000000..ff0b35f
--- /dev/null
@@ -0,0 +1,43 @@
+
+
+    /* basic */
+
+body {
+    color: #dddddd;
+    background-color: #212121;
+}
+
+
+    /* camera frame */
+
+div.camera-frame {
+    position: relative;
+    padding: 0px;
+    margin: 0px;
+    width: 100%;
+    height: 100%;
+}
+
+div.camera-container {
+    height: 100%;
+    text-align: center;
+    overflow: hidden;
+}
+
+img.camera {
+    height: auto;
+    margin: auto;
+}
+
+img.camera.error,
+img.camera.loading {
+    height: 100% !important;
+}
+
+div.camera-placeholder {
+    overflow: hidden;
+}
+
+div.camera-progress {
+    cursor: default;
+}
diff --git a/motioneye/static/css/jquery.timepicker.css b/motioneye/static/css/jquery.timepicker.css
new file mode 100755 (executable)
index 0000000..ad4665d
--- /dev/null
@@ -0,0 +1,69 @@
+\r
+.ui-timepicker-wrapper {\r
+       overflow-y: auto;\r
+       height: 150px;\r
+       width: 6.5em;\r
+       background: #414141;\r
+       border: 1px solid #515151;\r
+       -webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);\r
+       -moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);\r
+       box-shadow:0 5px 10px rgba(0,0,0,0.2);\r
+       outline: none;\r
+       z-index: 10001;\r
+       margin: 0;\r
+}\r
+\r
+.ui-timepicker-wrapper.ui-timepicker-with-duration {\r
+       width: 11em;\r
+}\r
+\r
+.ui-timepicker-list {\r
+       margin: 0;\r
+       padding: 0;\r
+       list-style: none;\r
+}\r
+\r
+.ui-timepicker-duration {\r
+       margin-left: 5px; color: #888;\r
+}\r
+\r
+.ui-timepicker-list:hover .ui-timepicker-duration {\r
+       color: #888;\r
+}\r
+\r
+.ui-timepicker-list li {\r
+       padding: 3px 0 3px 5px;\r
+       cursor: pointer;\r
+       white-space: nowrap;\r
+       color: white;\r
+       list-style: none;\r
+       font-size: 0.8em;\r
+       margin: 0;\r
+}\r
+\r
+.ui-timepicker-list:hover .ui-timepicker-selected {\r
+       background: #aaa; color: black;\r
+}\r
+\r
+li.ui-timepicker-selected,\r
+.ui-timepicker-list li:hover,\r
+.ui-timepicker-list .ui-timepicker-selected:hover {\r
+       background: #1980EC; color: #fff;\r
+}\r
+\r
+li.ui-timepicker-selected .ui-timepicker-duration,\r
+.ui-timepicker-list li:hover .ui-timepicker-duration {\r
+       color: #ccc;\r
+}\r
+\r
+.ui-timepicker-list li.ui-timepicker-disabled,\r
+.ui-timepicker-list li.ui-timepicker-disabled:hover,\r
+.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {\r
+       color: #888;\r
+       cursor: default;\r
+}\r
+\r
+.ui-timepicker-list li.ui-timepicker-disabled:hover,\r
+.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {\r
+       background: #f2f2f2;\r
+}\r
diff --git a/motioneye/static/css/main.css b/motioneye/static/css/main.css
new file mode 100644 (file)
index 0000000..c02e29b
--- /dev/null
@@ -0,0 +1,1043 @@
+
+
+    /* basic */
+
+* {
+    padding: 0px;
+    border: 0px solid black;
+    margin: 0px;
+    outline: 0px;
+    border-spacing: 0px;
+    border-collapse: separate;
+}
+
+html {
+    height: 100%;
+}
+
+body {
+    height: 100%;
+    color: #dddddd;
+    font-size: 22px;
+    background-color: #212121;
+}
+
+select,
+input[type=text],
+input[type=password],
+textarea {
+    box-sizing: border-box;
+}
+
+
+    /* fonts */
+
+@font-face {
+    font-family: 'Maven Pro';
+    src: url('../fnt/mavenpro-regular-webfont.eot');
+    src: url('../fnt/mavenpro-regular-webfont.eot?#iefix') format('embedded-opentype'),
+         url('../fnt/mavenpro-regular-webfont.woff') format('woff'),
+         url('../fnt/mavenpro-regular-webfont.ttf') format('truetype'),
+         url('../fnt/mavenpro-regular-webfont.svg#maven_proregular') format('svg');
+    font-weight: normal;
+    font-style: normal;
+}
+@font-face {
+    font-family: 'Maven Pro';
+    src: url('../fnt/mavenpro-bold-webfont.eot');
+    src: url('../fnt/mavenpro-bold-webfont.eot?#iefix') format('embedded-opentype'),
+         url('../fnt/mavenpro-bold-webfont.woff') format('woff'),
+         url('../fnt/mavenpro-bold-webfont.ttf') format('truetype'),
+         url('../fnt/mavenpro-bold-webfont.svg#maven_probold') format('svg');
+    font-weight: bold;
+    font-style: normal;
+}
+
+    /* layout */
+
+html {
+    font-family: 'Maven Pro';
+}
+
+div.page,
+div.header-container {
+    position: relative;
+    min-width: 320px;
+    width: 100%;
+}
+
+div.page {
+    font-size: 1em;
+    transition: all 0.5s linear;
+    min-height: 100%;
+}
+
+div.header {
+    background-color: rgba(64, 64, 64, 0.5);
+    box-shadow: 0px 0px 5px rgba(0,0,0,0.3);
+    top: 0px;
+    width: 100%;
+    height: 50px;
+    position: fixed;
+    overflow: hidden;
+    z-index: 10000;
+}
+
+div.header-container {
+    transition: all 0.5s linear;
+}
+
+div.footer {
+    position: absolute;
+    bottom: 5px;
+    width: 100%;
+    height: 3em;
+    font-size: 0.7em;
+    color: #aaa;
+    text-align: center;
+}
+
+div.copyright-note {
+    border-top: 1px solid #333;
+    padding-top: 0.2em;
+    margin: 0px 15%;
+}
+
+div.page-container {
+    transition: all 0.2s linear;
+    padding: 55px 5px 3em 2%;
+}
+
+div.page-container.stretched {
+    margin-left: 40%;
+    padding-left: 5px;
+}
+
+
+    /* icons & icon buttons */
+
+div.button.settings-button {
+    margin: 1px;
+    vertical-align: middle;
+    background-image: url(../img/settings.svg);
+    width: 48px;
+    height: 48px;
+}
+
+div.button.logout-button {
+    margin: 1px;
+    vertical-align: middle;
+    background-image: url(../img/logout.svg);
+    width: 48px;
+    height: 48px;
+}
+
+body.admin div.logout-button {
+    display: none;
+}
+
+body.admin div.settings-top-bar.closed div.logout-button {
+    display: inline-block;
+}
+
+body:not(.admin) div.settings-top-bar div.logout-button {
+    display: none;
+}
+
+div.button.rem-camera-button {
+    display: none;
+    margin: 1px;
+    vertical-align: middle;
+    background-image: url(../img/settings.svg);
+    width: 48px;
+    height: 48px;
+    background-position: -48px 0px;
+}
+
+div.settings-top-bar.open div.button.rem-camera-button {
+    display: inline-block;
+}
+
+div.logo {
+    float: right;
+    display: inline-block;
+    white-space: nowrap;
+    opacity: 0.86;
+}
+
+span.logo {
+    color: white;
+    vertical-align: middle;
+    font-size: 27px;
+    font-weight: bold;
+    position: relative;
+    top: 3px;
+}
+
+img.logo {
+    width: 36px;
+    height: 36px;
+    padding: 7px 3px;
+    vertical-align: middle;
+}
+
+img.background-logo {
+    position: absolute;
+    width: 30%;
+    left: 35%;
+    top: 10em;
+    opacity: 0.03;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    user-select: none;
+}
+
+img.main-loading-progress {
+    display: block;
+    margin: auto;
+    margin-top: 50px;
+}
+
+div.add-camera-message {
+    text-align: center;
+    margin-top: 30px;
+}
+
+div.hostname {
+    vertical-align: middle;
+    display: inline-block;
+    font-size: 27px;
+}
+
+
+    /* settings */
+
+div.settings {
+    background-color: #313131;
+    position: fixed;
+    z-index: 1;
+    top: 50px;
+    left: 0px;
+    width: 0px;
+    bottom: 0px;
+    transition: all 0.2s linear;
+    overflow: auto;
+}
+
+div.settings.open {
+    width: 40%;
+    min-width: 320px;
+}
+
+body:not(.admin) div.settings {
+    display: none !important;
+}
+
+div.settings-container {
+    position: relative;
+    padding-top: 10px;
+    display: none;
+    white-space: nowrap;
+}
+
+div.settings.open div.settings-container {
+    display: block;
+}
+
+div.settings-progress {
+    position: absolute;
+    top: 0px;
+    width: 0px;
+    bottom: 0px;
+    left: 0px;
+    background-color: #313131;
+    opacity: 0;
+    transition: opacity 0.1s linear;
+}
+
+div.settings-top-bar {
+    position: relative;
+    display: inline-block;
+    width: 40%;
+    height: 50px;
+}
+
+div.settings-top-bar.open {
+    background-color: #414141;
+    min-width: 320px;
+}
+
+body:not(.admin) div.settings-top-bar {
+    display: none !important;
+}
+
+div.settings-top-bar.closed div.apply-button {
+    display: none !important;
+}
+
+div.settings-section-title {
+    position: relative;
+    text-align: right;
+    background-color: rgba(100, 100, 100, 0.3);
+    padding: 5px 0.5em 5px 5px;
+}
+
+a.settings-section-title {
+}
+
+table.settings {
+    width: 100%;
+    padding: 0.5em 0.5em 1em 0.5em;
+}
+
+td.settings-item-label {
+    width: 50%;
+    text-align: right;
+    padding-right: 5px;
+}
+
+td.settings-item-value {
+    width: 50%;
+    text-align: left;
+    padding-left: 5px;
+}
+
+span.settings-item-label {
+    font-size: 0.9em;
+}
+
+span.settings-item-unit {
+    font-size: 0.6em;
+    padding: 0px 0.2em;
+}
+
+div.settings-item-separator {
+    height: 1px;
+    border-top: 1px solid #414141;
+    margin: 0.5em 1em;
+}
+
+#cameraSelect {
+    display: none;
+    padding: 4px 1.5em 4px 4px;
+    vertical-align: middle;
+    font-size: 1.1em;
+    width: auto;
+    max-width: 35%;
+}
+
+div.apply-button {
+    position: relative;
+    display: none;
+    opacity: 0;
+    float: right;
+    width: 4em;
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+    margin: 10px;
+    color: white;
+    background-color: #FF6F00;
+    border-radius: 3px;
+    transition: all 0.1s linear;
+}
+
+div.apply-button:HOVER {
+    background-color: #FF7D19;
+}
+
+div.apply-button:ACTIVE {
+    background-color: #F06800;
+}
+
+div.apply-button.progress {
+    background-color: #FF6F00;
+}
+
+img.apply-progress {
+    margin-top: 3px;
+}
+
+div.normal-button {
+    position: relative;
+    height: 1.5em;
+    line-height: 1.5em;
+    text-align: center;
+    margin: 2px 0px;
+    color: white;
+    font-size: 0.9em;
+    border-radius: 3px;
+    transition: all 0.1s linear;
+    width: 7em;
+}
+
+div.update-button,
+div.backup-button,
+div.restore-button {
+    background: #317CAD;
+}
+
+div.shut-down-button,
+div.reboot-button {
+    background: #c0392b;
+}
+
+div.update-button:HOVER,
+div.backup-button:HOVER,
+div.restore-button:HOVER {
+    background: #3498db;
+}
+
+div.shut-down-button:HOVER,
+div.reboot-button:HOVER {
+    background: #D43F2F;
+}
+
+div.update-button:ACTIVE,
+div.backup-button:ACTIVE,
+div.restore-button:ACTIVE {
+    background: #317CAD;
+}
+
+div.shut-down-button:ACTIVE,
+div.reboot-button:ACTIVE {
+    background: #B03427;
+}
+
+div.settings-top-bar.open #cameraSelect {
+    display: inline;
+}
+
+div.settings-top-bar.open div.logout-button {
+    display: none;
+}
+
+div.check-box.section {
+    margin: 0px;
+    float: left;
+}
+
+div.check-box.section div.check-box-button {
+    background-color: #515151;
+}
+
+div.check-box.on.section div.check-box-button {
+    background-color: #317CAD;
+}
+
+div.check-box.on.section:FOCUS div.check-box-button,
+div.check-box.on.section:HOVER div.check-box-button {
+    background-color: #3498db;
+}
+
+input[type=text].working-schedule.number {
+    width: 50px;
+}
+
+#diskUsageProgressBar {
+    width: 90%;
+}
+
+div.hidden,
+tr.hidden {
+    display: none !important;
+}
+
+span.help-mark {
+    display: inline-block;
+    visibility: hidden;
+    text-align: center;
+    background-color: #414141;
+    color: #3498db;
+    font-size: 0.75em;
+    font-family: monospace;
+    width: 1.2em;
+    height: 1.2em;
+    border-radius: 100em;
+    cursor: pointer;
+    vertical-align: middle;
+    position: relative;
+    top: -0.1em;
+}
+
+div.settings-section-title > span.help-mark {
+    background-color: #515151;
+}
+
+div.settings-section-title:HOVER > span.help-mark,
+tr:HOVER span.help-mark {
+    visibility: visible;
+}
+
+span.minimize {
+    display: inline-block;
+    background-image: url(../img/combo-box-arrow.svg);
+    background-size: cover;
+    width: 0.8em;
+    height: 0.8em;
+    cursor: pointer;
+    vertical-align: middle;
+    position: relative;
+    top: -0.1em;
+    transition: transform 0.1s linear;
+    -webkit-transform: rotate(90deg);
+    -moz-transform: rotate(90deg);
+    -ms-transform: rotate(90deg);
+    -o-transform: rotate(90deg);
+    transform: rotate(90deg);
+}
+
+span.minimize.open {
+    -webkit-transform: rotate(0deg);
+    -moz-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -o-transform: rotate(0deg);
+    transform: rotate(0deg);
+}
+
+
+    /* dialogs */
+    
+table.login-dialog {
+    margin: auto;
+    font-size: 22px; /* always bigger, regardless of screen size */
+}
+
+table.add-camera-dialog {
+    margin: auto;
+}
+
+table.add-camera-dialog select,
+table.add-camera-dialog input[type=text],
+table.add-camera-dialog input[type=password] {
+    width: 17em;
+}
+
+span#cameraMsgLabel {
+    color: red;
+    font-size: 0.7em;
+}
+
+div#addCameraInfo {
+    font-size: 0.7em;
+    max-width: 33em;
+}
+
+div.media-dialog {
+}
+
+div.media-dialog-groups {
+    float: left;
+    width: 11em;
+    text-align: center;
+    overflow: auto;
+    white-space: nowrap;
+}
+
+div.media-dialog-groups.small-screen {
+    float: none;
+}
+
+div.media-dialog-group-button {
+    height: 1.5em;
+    width: 10.5em;
+    box-sizing: border-box;
+    line-height: 1.5em;
+    text-align: center;
+    margin: 0em 0.2em 0.2em 0.2em;
+    padding: 0px 0.5em;
+    background-color: #414141;
+    color: #3498db;
+    border-radius: 3px;
+    transition: all 0.1s linear;
+    cursor: pointer;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+div.media-dialog-groups.small-screen div.media-dialog-group-button {
+    display: inline-block;
+}
+
+div.media-dialog-group-button:HOVER {
+    background-color: #515151;
+}
+
+div.media-dialog-group-button:ACTIVE {
+    background-color: #414141;
+}
+
+div.media-dialog-group-button.current {
+    background-color: #317CAD;
+    color: white;
+}
+
+div.media-dialog-group-button.current:HOVER {
+    background-color: #3498db;
+}
+
+div.media-dialog-group-button.current:ACTIVE {
+    background-color: #317CAD;
+}
+
+div.media-dialog-list {
+    overflow: auto;
+    position: relative;
+}
+
+div.media-list-group-title {
+    background-color: #313131;
+    font-size: 1.3em;
+    font-weight: bold;
+    text-align: center;
+    padding: 1em 0px 0.2em 0px;
+}
+
+img.media-list-progress {
+    position: relative;
+    top: 35%;
+    display: block;
+    margin: auto;
+}
+
+div.media-list-entry {
+    height: 4em;
+    background-color: #414141;
+    border-bottom: 1px solid #313131;
+    cursor: pointer;
+    transition: background-color 0.1s linear;
+}
+
+div.media-list-entry:HOVER {
+    background-color: #494949;
+}
+
+div.media-list-entry:ACTIVE {
+    background-color: #3b3b3b;
+}
+
+img.media-list-preview {
+    float: left;
+    height: 3em;
+    margin: 0.45em;
+    border: 1px solid #212121;
+    box-shadow: 1px 1px 6px rgba(0,0,0,0.3);
+}
+
+div.media-list-entry-name {
+    font-weight: bold;
+    font-size: 1.3em;
+    padding: 0.4em 0em;
+    text-align: center;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+div.media-list-entry-details {
+    font-size: 1em;
+    text-align: center;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+div.media-list-entry-details span.details-moment-short {
+    display: none;
+}
+
+div.media-list-download-button,
+div.media-list-delete-button {
+    float: right;
+    clear: right;
+    height: 1.5em;
+    width: 5em;
+    line-height: 1.5em;
+    text-align: center;
+    margin: 0px 0.5em;
+    padding: 0px 0.5em;
+    color: white;
+    border-radius: 3px;
+    transition: all 0.1s linear;
+}
+
+div.media-list-download-button {
+    margin-top: 0.4em;
+    margin-bottom: 0.1em;
+    background: #317CAD;
+}
+
+div.media-list-download-button:HOVER {
+    background-color: #3498db;
+}
+
+div.media-list-download-button:ACTIVE {
+    background-color: #317CAD;
+}
+
+div.media-list-delete-button {
+    margin-top: 0.1em;
+    margin-bottom: 0.4em;
+    background: #c0392b;
+}
+
+div.media-list-delete-button:HOVER {
+    background-color: #D43F2F;
+}
+
+div.media-list-delete-button:ACTIVE {
+    background-color: #B03427;
+}
+
+div.media-dialog-buttons {
+    margin: 0.5em 0px 0px 0px;
+    text-align: center;
+}
+
+div.media-dialog-button {
+    cursor: pointer;
+    display: inline-block;
+    height: 1.5em;
+    line-height: 1.5em;
+    text-align: center;
+    padding: 0px 0.5em;
+    margin: 0px 5px 0px 0px;
+    color: white;
+    background-color: #317CAD;
+    border-radius: 3px;
+    transition: all 0.1s linear;
+}
+
+div.media-dialog-button:HOVER {
+    background-color: #3498db;
+}
+
+div.media-dialog-button:ACTIVE {
+    background-color: #317CAD;
+}
+
+div.picture-dialog-content {
+    position: relative;
+    text-align: center;
+    min-height: 100px;
+}
+
+div.picture-dialog-prev-arrow,
+div.picture-dialog-next-arrow {
+    position: absolute;
+    top: 45%;
+    background-color: rgba(0, 0, 0, 0.6);
+    background-image: url(../img/arrows.svg);
+    background-size: cover;
+    width: 3em;
+    height: 3em;
+    border-radius: 0.3em;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    user-select: none;
+    cursor: pointer;
+}
+
+div.picture-dialog-prev-arrow {
+    left: 1em;
+}
+
+div.picture-dialog-next-arrow {
+    right: 1em;
+    background-position: -100% 0%;
+}
+
+img.picture-dialog-content {
+    border: 1px solid #292929;
+}
+
+img.picture-dialog-progress {
+    position: absolute;
+    background-color: #313131;
+    padding: 10px;
+    border-radius: 10px;
+    opacity: 0.7;
+}
+
+table.timelapse-dialog select {
+    width: 10em;
+}
+
+td.timelapse-warning {
+    font-size: 80%;
+    display: none;
+    color: red;
+    max-width: 20em;
+    text-align: center;
+    white-space: normal;
+    padding-bottom: 1em;
+}
+
+div.media-dialog-delete-all-button {
+    margin-top: 0.1em;
+    margin-bottom: 0.4em;
+    background: #c0392b;
+}
+
+div.media-dialog-delete-all-button:HOVER {
+    background-color: #D43F2F;
+}
+
+div.media-dialog-delete-all-button:ACTIVE {
+    background-color: #B03427;
+}
+
+td.login-dialog-error {
+    color: red;
+    display: none;
+}
+
+
+    /* camera frames */
+
+div.camera-list {
+    text-align: center;
+}
+
+div.camera-frame,
+div.camera-frame-place-holder {
+    position: relative;
+    width: 32%;
+    text-align: left;
+    background-color: #313131;
+    display: inline-block;
+    padding: 0px 3px 3px 3px;
+    border-radius: 3px;
+    transition: all 0.2s, opacity 0s;
+    margin: 2px;
+    opacity: 0;
+    vertical-align: top;
+}
+
+div.camera-frame:only-child,
+div.camera-frame-place-holder:only-child {
+    width: 48%;
+}
+
+div.camera-frame-place-holder {
+    visibility: hidden;
+}
+
+div.camera-frame.motion-detected {
+    background-color: #712727;
+}
+
+div.modal-container div.camera-frame {
+    width: auto;
+    padding: 0px;
+    margin: -7px;
+    background-color: #414141;
+}
+
+div.camera-frame:HOVER {
+    background-color: #414141;
+}
+
+div.camera-frame.motion-detected:HOVER {
+    background-color: #8B3636;
+}
+
+div.camera-top-bar {
+    padding: 3px 0px;
+    font-size: 20px;
+    height: 25px;
+}
+
+div.modal-container div.camera-top-bar {
+    display: none;
+}
+
+span.camera-name {
+    float: left;
+    line-height: 25px;
+}
+
+div.camera-buttons {
+    float: right;
+}
+
+div.camera-button {
+    display: inline-block;
+    width: 24px;
+    height: 24px;
+    background-image: url(../img/top-bar-buttons.svg);
+    background-size: cover;
+    margin-left: 3px;
+    cursor: pointer;
+    transition: all 0.1s linear;
+}
+
+div.camera-button.close {
+    background-position: 0px 0px;
+}
+
+div.camera-button.full-screen {
+    background-position: -200% 0px;
+}
+
+div.camera-button.configure {
+    background-position: -100% 0px;
+}
+
+div.camera-button.media-pictures {
+    background-position: -300% 0px;
+}
+
+div.camera-button.media-movies {
+    background-position: -400% 0px;
+}
+
+div.camera-container {
+    position: relative;
+    padding: 0px;
+}
+
+img.camera {
+    position: relative;
+    width: 100%;
+    display: block;
+    transition: opacity 0.2s linear;
+    opacity: 1;
+    min-height: 160px;
+}
+
+img.camera.error,
+img.camera.loading {
+    opacity: 0;
+}
+
+div.camera-placeholder {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    bottom: 0px;
+    left: 0px;
+    text-align: center;
+    transition: opacity 0.2s linear;
+}
+
+img.no-camera {
+    margin-top: 20%;
+    width: 30%;
+    opacity: 0.8;
+}
+
+div.camera-progress {
+    background: rgba(0, 0, 0, 0.001); /* otherwise IE would not extend this as expected */
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    bottom: 0px;
+    left: 0px;
+    opacity: 0;
+    transition: all 0.2s linear;
+    text-align: center;
+    cursor: pointer;
+}
+
+div.camera-progress.visible {
+    opacity: 0.4;
+}
+
+img.camera-progress {
+    border: 10px solid white;
+    border-radius: 10px;
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    bottom: 0px;
+    right: 0px;
+    margin: auto;
+}
+
+
+    /* media queries */
+
+@media all and (max-width: 1440px) {
+    /* smaller screens */
+    
+    body {
+        font-size: 17px;
+    }
+}
+
+@media all and (max-width: 1000px) {
+    /* small screens (mobile devices) */
+    
+    div.page-container.stretched {
+        margin-left: 0px;
+    }
+    
+    div.settings.open {
+        box-shadow: 0px 0px 10px rgba(0,0,0,0.5);
+        background-color: rgba(40, 40, 40, 0.9);
+    }
+    
+    div.hostname {
+        display: none;
+    }
+
+    div.media-list-entry-name {
+        font-size: 1em;
+        padding: 0.2em 0em 0em 0em;
+    }
+
+    div.media-list-entry-details {
+        padding-top: 0.2em;
+        font-size: 1em;
+        text-align: center;
+        white-space: normal;
+    }
+
+    div.media-list-entry-details span.details-moment {
+        display: none;
+    }
+
+    div.media-list-entry-details span.details-moment-short {
+        display: block;
+    }
+}
+
+@media all and (max-width: 400px) {
+    /* very small screens */
+    
+    body {
+        font-size: 13px;
+    }
+    
+    div.camera-button {
+        background-size: cover;
+        width: 24px;
+        height: 24px;
+    }
+}
+
+@media all and (max-width: 1900px) {
+    div.camera-frame, 
+    div.camera-frame-place-holder {
+        width: 48%;
+    }
+}
+
+@media all and (max-width: 1200px) {
+    div.page-container {
+        padding-left: 1%;
+    }
+
+    div.camera-frame,
+    div.camera-frame-place-holder {
+        width: 98%;
+    }
+
+    div.camera-frame:only-child,
+    div.camera-frame-place-holder:only-child {
+        width: 98%;
+    }
+}
diff --git a/motioneye/static/css/ui.css b/motioneye/static/css/ui.css
new file mode 100644 (file)
index 0000000..f114a8b
--- /dev/null
@@ -0,0 +1,469 @@
+
+    /* general */
+
+::selection,
+::-moz-selection,
+::-webkit-selection {
+    background: #3498db;
+}
+
+option::selection,
+option::-moz-selection,
+option::-webkit-selection {
+    background: transparent;
+}
+
+input[type=checkbox].styled {
+    display: none;
+}
+
+a {
+    color: #3498db;
+    text-decoration: inherit;
+    transition: color 0.1s ease;
+    cursor: pointer;
+}
+
+a:HOVER {
+}
+
+a:ACTIVE {
+    color: #317CAD;
+}
+
+
+    /* buttons */
+
+div.button {
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    user-select: none;
+    cursor: pointer;
+    display: inline-block;    
+}
+
+div.button.dialog {
+    background-color: #414141;
+    min-width: 60px;
+    height: 1.2em;
+    line-height: 1.2em;
+    text-align: center;
+    padding: 0.2em 0.4em;
+    border: 1px solid #317CAD;
+    border-radius: 2px;
+    color: white;
+    transition: all 0.1s ease;
+}
+
+div.button.dialog.default {
+    background-color: #317CAD;
+}
+
+div.button.mouse-effect {
+    opacity: 0.7;
+    transition: opacity 0.1s ease;
+}
+
+div.button.mouse-effect:HOVER {
+    opacity: 1;
+}
+
+div.button.mouse-effect:ACTIVE {
+    opacity: 0.7;
+}
+
+
+    /* check box */
+
+div.check-box {
+    display: inline-block;
+    position: relative;
+    width: 2.5em;
+    height: 1em;
+    border: 1px solid #317CAD;
+    border-radius: 2px;
+    color: #aaaaaa;
+    vertical-align: middle;
+    cursor: pointer;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    user-select: none;
+    margin: 2px;
+    transition: all 0.2s ease;
+}
+
+div.check-box:FOCUS,
+div.check-box:HOVER {
+    border-color: #3498db;
+}
+
+div.check-box-button {
+    width: 50%;
+    height: 100%;
+    left: 0px;
+    background-color: #414141;
+    color: #aaaaaa;
+    position: absolute;
+    text-align: center;
+    line-height: 1em;
+    transition: all 0.1s ease;
+}
+
+span.check-box-text {
+    font-size: 0.5em;
+    font-weight: bold;
+    vertical-align: top;
+}
+
+div.check-box.on div.check-box-button {
+    left: 50%;
+    background-color: #317CAD;
+    color: white;
+}
+
+div.check-box.on:FOCUS div.check-box-button,
+div.check-box.on:HOVER div.check-box-button {
+    background-color: #3498db;
+}
+
+
+    /* input boxes */
+
+input[type=password].styled,
+input[type=text].styled,
+textarea.styled {
+    width: 90%;
+    border: 1px solid #317CAD;
+    border-radius: 2px;
+    background-color: transparent;
+    padding: 1px;
+    color: #dddddd;
+    font-family: inherit;
+    font-size: 0.8em;
+    margin: 2px;
+    transition: all 0.1s ease;
+    resize: none;
+    vertical-align: middle;
+    white-space: nowrap;
+}
+
+input[type=password].styled:FOCUS,
+input[type=text].styled:FOCUS {
+    background-color: #414141;
+}
+
+input[type=password].styled:HOVER,
+input[type=password].styled:FOCUS,
+input[type=text].styled:HOVER,
+input[type=text].styled:FOCUS {
+    border-color: #3498db;
+    color: white;
+}
+
+input[type=text].number {
+    width: 5em;
+}
+
+input[type=text].error,
+input[type=password].error,
+input[type=file].error,
+select.error {
+    background-image: url(../img/validation-error.svg) !important;
+    background-position: center right;
+    background-repeat: no-repeat;
+}
+
+input[type=text].time {
+    width: 3.5em;
+}
+
+input[readonly] {
+    border: 1px solid #555 !important;
+}
+
+
+    /* combo box */
+
+select.styled {
+    -webkit-appearance: none;
+    appearance: none;
+    width: 90%;
+    border: 1px solid #317CAD;
+    border-radius: 2px;
+    background-color: transparent;
+    padding: 1px 1.25em 1px 1px;
+    color: #dddddd;
+    font-family: inherit;
+    font-size: 0.8em;
+    margin: 2px;
+    background-image: url(../img/combo-box-arrow.svg);
+    background-repeat: no-repeat;
+    background-position: right center;
+    cursor: pointer;
+    vertical-align: middle;
+}
+
+select.styled:FOCUS {
+    background-color: #414141;
+    appearance: auto;
+}
+
+select.styled:HOVER,
+select.styled:FOCUS {
+    border-color: #3498db;
+    color: white;
+}
+
+.ff select.styled {
+    background-image: none;
+    padding-right: 1px;
+}
+
+
+    /* slider */
+
+input[type=text].range {
+    display: none;
+}
+
+div.slider {
+    width: 82%;
+    height: 1.7em;
+    position: relative;
+    padding: 0.2em 0px;
+    margin-left: 5%;
+}
+
+div.slider-labels {
+    position: relative;
+    width: 100%;
+    height: 0.5em;
+}
+
+span.slider-label {
+    display: inline-block;
+    width: 20%;
+    text-align: center;
+    overflow: visible;
+    position: absolute;
+    font-size: 0.5em;
+}
+
+div.slider-bar {
+    position: relative;
+    top: 7px;
+    left: -5px;
+    width: 100%;
+}
+
+div.slider-bar-inside {
+    border: 1px solid #317CAD;
+    height: 3px;
+    position: relative;
+    top: 3px;
+    left: 7px;
+    transition: all 0.1s ease;
+}
+
+div.slider:HOVER div.slider-bar-inside,
+div.slider:FOCUS div.slider-bar-inside {
+    border-color: #3498db;
+}
+
+div.slider:FOCUS div.slider-bar-inside {
+    background-color: #414141;
+}
+
+div.slider-cursor {
+    background-image: url(../img/slider-arrow.svg);
+    width: 11px;
+    height: 18px;
+    position: absolute;
+    left: 0px;
+    top: -5px;
+    cursor: pointer;
+}
+
+div.slider-cursor-label {
+    font-size: 0.6em;
+    margin-left: 1.5em;
+    margin-top: 0.15em;
+    background: rgba(30, 30, 30, 0.8);
+    vertical-align: top;
+    padding: 1px 2px;
+    border-radius: 3px;
+    display: none;
+}
+
+
+    /* progress bar */
+
+div.progress-bar-container {
+    position: relative;
+    height: 1em;
+    border: 1px solid #555;
+    vertical-align: middle;
+    margin: 0px 0.2em;
+    text-align: center;
+    line-height: 1em;
+}
+
+div.progress-bar-fill {
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    bottom: 0px;
+    width: 0%;
+    background-color: #555;
+    transition: width 0.1s ease;
+}
+
+span.progress-bar-text {
+    vertical-align: middle;
+    font-size: 0.8em;
+    position: relative;
+}
+
+
+    /* modal dialogs */
+    
+div.modal-glass {
+    display: none;
+    position: fixed;
+    z-index: 10000;
+    top: 0px;
+    right: 0px;
+    bottom: 0px;
+    left: 0px;
+    background-color: black;
+    opacity: 0;
+}
+
+div.modal-container {
+    position: fixed;
+    display: none;
+    z-index: 10001;
+    background-color: #313131;
+    border-radius: 3px;
+    opacity: 0;
+    padding: 5px;
+    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
+}
+
+div.modal-title-bar {
+    min-height: 1.5em;
+    line-height: 1.5em;
+    text-align: center;
+    position: relative;
+}
+
+span.modal-title {
+    color: white;
+    font-size: 1.2em;
+    line-height: 1.2em;
+}
+
+div.modal-close-button {
+    position: absolute;
+    top: 0.2em;
+    right: 0.3em;
+    width: 1.1em;
+    height: 1.1em;
+    background-image: url(../img/top-bar-buttons.svg);
+    background-size: cover;
+    cursor: pointer;
+}
+
+table.modal-buttons-container {
+    width: 100%;
+    margin: auto;
+    text-align: center;
+    table-layout: fixed;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    user-select: none;
+}
+
+table.modal-buttons-container td:not(:FIRST-CHILD) {
+    padding-left: 5px;
+}
+
+table.modal-buttons-container div.button.dialog {
+    display: block;
+}
+
+div.modal-progress {
+    border-radius: 10px;
+    background-image: url(../img/modal-progress.gif);
+    width: 64px;
+    height: 64px;
+    margin: auto;
+}
+
+td.dialog-item-label {
+    text-align: right;
+    padding-right: 5px;
+}
+
+td.dialog-item-value {
+    text-align: left;
+    padding-left: 5px;
+}
+
+span.dialog-item-label {
+    font-size: 0.9em;    
+}
+
+div.dialog-item-separator {
+    height: 1px;
+    border-top: 1px solid #414141;
+    margin: 0.5em 1em;
+}
+
+   
+    /* popup message */
+
+div.popup-message-container {
+    position: fixed;
+    display: none;
+    z-index: 10002;
+    background-color: #313131;
+    border-radius: 3px;
+    opacity: 0;
+    padding: 5px;
+    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
+    top: 60px;
+}
+
+span.popup-message {
+    
+}
+
+span.popup-message.info {
+    color: white;
+}
+
+span.popup-message.error {
+    color: #FF6D55;
+}
+
+
+    /* media queries */
+
+@media all and (max-width: 400px) {
+    span.modal-title {
+        font-size: 1.5em;
+        
+    }
+    
+    div.modal-title-bar {
+        min-height: 2em;
+        line-height: 2em;
+    }
+    
+    div.modal-close-button {
+        background-size: cover;
+        width: 24px;
+        height: 24px;
+    }
+}
diff --git a/motioneye/static/favicon.ico b/motioneye/static/favicon.ico
new file mode 100644 (file)
index 0000000..6cb844d
Binary files /dev/null and b/motioneye/static/favicon.ico differ
diff --git a/motioneye/static/fnt/mavenpro-black-webfont.eot b/motioneye/static/fnt/mavenpro-black-webfont.eot
new file mode 100644 (file)
index 0000000..7ee4811
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-black-webfont.eot differ
diff --git a/motioneye/static/fnt/mavenpro-black-webfont.svg b/motioneye/static/fnt/mavenpro-black-webfont.svg
new file mode 100644 (file)
index 0000000..b888155
--- /dev/null
@@ -0,0 +1,243 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="maven_problack" horiz-adv-x="1159" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="692" />
+<glyph horiz-adv-x="2048" />
+<glyph horiz-adv-x="2048" />
+<glyph unicode="&#xd;" horiz-adv-x="682" />
+<glyph unicode=" "  horiz-adv-x="692" />
+<glyph unicode="&#x09;" horiz-adv-x="692" />
+<glyph unicode="&#xa0;" horiz-adv-x="692" />
+<glyph unicode="!" horiz-adv-x="882" d="M279 1366h354l-45 -975h-264zM297 0v262h317v-262h-317z" />
+<glyph unicode="&#x22;" horiz-adv-x="899" d="M166 1384h254l-35 -411h-182zM477 1384h254l-35 -411h-182z" />
+<glyph unicode="#" horiz-adv-x="1288" d="M152 248v186h176q4 27 19 110l24 132h-164v184h194l37 209h197l-37 -209h217q33 170 37 209h197l-39 -209h125v-184h-154q-4 -27 -20.5 -111t-22.5 -131h143v-186h-174q-6 -37 -19.5 -111t-17.5 -98h-196l37 209h-219q-8 -43 -35 -209h-197l39 209h-147zM524 434h221 l39 242h-217q-6 -47 -22.5 -131t-20.5 -111z" />
+<glyph unicode="$" horiz-adv-x="1212" d="M147 952q0 193 123 299.5t316 120.5v217h125v-215q168 -14 311 -92v-307q-162 96 -311 98v-244q88 -25 148.5 -50t129 -73.5t104 -129t35.5 -189.5q0 -164 -118.5 -272.5t-298.5 -128.5v-207h-125v203q-250 12 -422 133v329q189 -157 419 -157h3v243q-94 23 -154.5 45.5 t-134.5 68.5t-112 123t-38 185zM457 958q0 -57 129 -96v203q-129 -23 -129 -107zM711 299q120 27 120 101q0 60 -120 100v-201z" />
+<glyph unicode="%" horiz-adv-x="1890" d="M147 983q0 377 289 377q291 0 291 -377t-291 -377q-289 0 -289 377zM354 983q0 -145 82 -145q84 0 84 145q0 88 -22.5 116.5t-61.5 28.5q-82 0 -82 -145zM387 0l858 1366h234l-858 -1366h-234zM1141 383q0 377 289 377q291 0 291 -376q-1 -378 -291 -378q-289 0 -289 377 zM1346 383q0 -145 84 -145q82 0 82 143q0 147 -82 147q-84 0 -84 -145z" />
+<glyph unicode="&#x26;" horiz-adv-x="1337" d="M113 397q0 113 45 207t192 189q0 2 -9 13l-15 20q-7 8 -18 24t-19 30.5t-17.5 36t-15.5 41t-10 43t-4 46.5q0 137 86 232t247 95q160 0 249 -91t89 -239q0 -111 -47 -179t-161 -140l22 -27q14 -18 59 -70l107 -126l31 131h235q-32 -195 -104 -328l254 -305h-308l-92 117 q-147 -133 -352 -133q-115 0 -204 41t-139 105.5t-75.5 133t-25.5 133.5zM342 391q0 -23 9 -51.5t32 -60t69 -53t109 -21.5q119 0 207 84l-268 323q-57 -31 -92 -63.5t-48.5 -64t-15.5 -48t-2 -45.5zM469 1049q0 -41 82 -144q82 47 108.5 80t26.5 72q0 43 -31.5 73.5 t-77 30.5t-77 -32.5t-31.5 -79.5z" />
+<glyph unicode="'" horiz-adv-x="583" d="M166 1384h254l-35 -411h-182z" />
+<glyph unicode="(" horiz-adv-x="796" d="M190 627q0 440 211 856h258q-210 -414 -210 -852q0 -440 210 -862h-258q-211 422 -211 858z" />
+<glyph unicode=")" horiz-adv-x="796" d="M133 -231q211 422 211 858q0 440 -211 856h258q211 -416 211 -856q0 -436 -211 -858h-258z" />
+<glyph unicode="*" horiz-adv-x="845" d="M172 1133l158 57l-158 57l78 139l110 -79l-12 131h160l-10 -131l108 79l78 -139l-156 -57l156 -57l-78 -140l-108 80l10 -131h-160l12 131l-110 -80z" />
+<glyph unicode="+" horiz-adv-x="1128" d="M190 498v229h259v258h229v-258h256v-229h-256v-256h-229v256h-259z" />
+<glyph unicode="," horiz-adv-x="751" d="M152 -229l151 401h285l-197 -401h-239z" />
+<glyph unicode="-" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
+<glyph unicode="." horiz-adv-x="811" d="M260 0v283h295v-283h-295z" />
+<glyph unicode="/" horiz-adv-x="948" d="M-29 -201l752 1749h256l-754 -1749h-254z" />
+<glyph unicode="0" horiz-adv-x="1345" d="M119 684q0 700 553 700t553 -700t-553 -700t-553 700zM473 684q0 -229 55.5 -312t143.5 -83q90 0 146 83t56 312q0 399 -202 399q-199 0 -199 -399z" />
+<glyph unicode="1" horiz-adv-x="1040" d="M211 840v346l285 180h288v-1366h-352v975z" />
+<glyph unicode="2" horiz-adv-x="1269" d="M158 0v279q579 433 579 642q0 62 -45 100t-133 38q-180 0 -391 -168v356q223 137 424 137q254 0 373.5 -128t119.5 -308q0 -158 -116.5 -325.5t-272.5 -305.5h398v-317h-936z" />
+<glyph unicode="3" horiz-adv-x="1228" d="M139 98v338q229 -137 402 -137q88 0 144 41t56 102.5t-47 105.5t-159 44h-224v254h228q72 0 107.5 32.5t35.5 79.5q0 49 -55.5 76t-126.5 27h-28h-30q-8 0 -28.5 -1t-32.5 -3t-33.5 -5t-39.5 -8l-42 -13q-24 -7 -50 -17t-52 -23v297q182 96 375 96q88 0 170 -18 t158.5 -58t122.5 -117t46 -181q0 -96 -65.5 -174.5t-147.5 -94.5q121 -35 195 -131t74 -227q0 -117 -49.5 -199t-133.5 -124t-170 -59t-184 -17q-190 0 -416 114z" />
+<glyph unicode="4" horiz-adv-x="1265" d="M80 348v270l598 748h307v-719h164v-299h-164v-348h-350v348h-555zM455 647h180v240z" />
+<glyph unicode="5" horiz-adv-x="1280" d="M162 707l74 661h806v-315h-499l-11 -119q31 4 99 4q229 0 356 -125t127 -385q0 -127 -41 -218t-112.5 -138t-151.5 -67.5t-174 -20.5q-213 0 -442 114v334q213 -135 395 -135q88 0 132 49t44 113q0 82 -45 124t-117 42h-17q-136 0 -229 -60z" />
+<glyph unicode="6" horiz-adv-x="1234" d="M111 630q0 196 49 341t117.5 221t163 122t165 58t142.5 12q176 0 278 -49v-297q-145 45 -250 45q-115 0 -211 -51t-110 -158q67 27 190 27q96 0 170 -15.5t148.5 -55t115.5 -129t41 -220.5q0 -90 -27.5 -175t-85 -158.5t-158.5 -118.5t-233 -45q-505 0 -505 646zM461 629 q0 -160 43 -247q46 -95 112 -95q61 0 98.5 45t44.5 83t7 68q0 78 -38 137.5t-116 59.5q-98 0 -151 -23v-28z" />
+<glyph unicode="7" horiz-adv-x="1226" d="M135 1016v350h1004v-258l-537 -1108h-407l495 1016h-555z" />
+<glyph unicode="8" horiz-adv-x="1261" d="M119 397q0 123 78.5 215.5t193.5 122.5q-88 25 -153.5 98.5t-65.5 174.5q0 376 459 376h2q459 0 459 -376q0 -100 -65 -175t-153 -98q113 -31 193 -123t80 -215q0 -205 -135 -309t-379 -104t-379 104.5t-135 308.5zM473 442q0 -74 40 -122t120 -48q78 0 118 48.5 t40 121.5q0 51 -41 93.5t-117 42.5q-78 0 -119 -42t-41 -94zM514 979q0 -53 37 -83t82 -30q43 0 80 30t37 83q0 43 -31 80t-86 37q-57 0 -88 -37t-31 -80z" />
+<glyph unicode="9" horiz-adv-x="1259" d="M111 887q0 90 27.5 175t85 158.5t158.5 118.5t232 45q506 0 506 -649q0 -193 -49 -338t-117.5 -221t-163 -122t-165 -58t-142.5 -12q-176 0 -278 49v297q145 -45 250 -45q115 0 211 51t110 158q-67 -27 -190 -27q-96 0 -170 15.5t-148.5 55.5t-115.5 129t-41 220z M465 885q0 -78 38 -137.5t115 -59.5q98 0 152 23v28q0 159 -43 247q-47 95 -113 95q-61 0 -98 -45t-44 -83t-7 -68z" />
+<glyph unicode=":" horiz-adv-x="868" d="M281 72v282h294v-282h-294zM281 670v282h294v-282h-294z" />
+<glyph unicode=";" horiz-adv-x="913" d="M166 -229l151 401h285l-196 -401h-240zM309 670v282h295v-282h-295z" />
+<glyph unicode="&#x3c;" horiz-adv-x="1368" d="M246 455v186l862 383v-279l-504 -196l504 -199v-278z" />
+<glyph unicode="=" horiz-adv-x="1306" d="M250 285v239h805v-239h-805zM250 698v240h805v-240h-805z" />
+<glyph unicode="&#x3e;" horiz-adv-x="1351" d="M238 72v278l503 199l-503 196v279l862 -383v-186z" />
+<glyph unicode="?" horiz-adv-x="1075" d="M164 1024v262q59 41 155.5 68.5t196.5 27.5q115 0 200 -33.5t132 -92t69.5 -126t22.5 -145.5q0 -70 -22.5 -126t-56.5 -92l-73 -74l-72 -73q-34 -34 -56.5 -87.5t-22.5 -118.5v-21h-281v27q0 104 31 184t75 127t89 86t75.5 81t30.5 89q0 45 -15 76t-46 43t-58.5 16 t-66.5 4q-180 0 -307 -102zM338 0v262h317v-262h-317z" />
+<glyph unicode="@" horiz-adv-x="1869" d="M166 373q0 182 79 346t203.5 274.5t280 175t309.5 64.5q106 0 220 -38t220.5 -112.5t175 -211t68.5 -308.5q0 -207 -92 -362.5t-268 -155.5q-68 0 -114 41t-64 90q-31 -57 -103.5 -94t-175.5 -37q-156 0 -246 70.5t-90 193.5q0 53 17.5 100.5t63.5 95.5t143.5 76.5 t238.5 28.5q35 0 105 -4q-2 66 -35 99.5t-129 33.5q-162 0 -285 -45l29 178q168 53 274 54q340 0 340 -308q0 -49 -15.5 -151t-15.5 -160q0 -123 82 -123q70 0 139.5 99.5t69.5 279.5q0 162 -88 288t-213 186.5t-258 60.5q-287 0 -509 -218t-222 -513q0 -125 44 -226.5 t107.5 -158t142.5 -94.5t138.5 -51t102.5 -13q258 0 469 104l-23 -149q-184 -86 -444 -86q-106 0 -217 38t-216.5 113.5t-172 213t-66.5 315.5zM766 313q0 -71 139 -71q111 0 154 46t53 158q-27 2 -80 3q-266 0 -266 -136z" />
+<glyph unicode="A" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM612 588h277l-137 424z" />
+<glyph unicode="B" horiz-adv-x="1349" d="M178 0v1366h463q541 0 541 -383q0 -66 -21.5 -116t-59.5 -79.5t-64.5 -43t-61.5 -25.5q274 -78 274 -342q0 -377 -567 -377h-504zM530 289h201h7h4q67 0 114 35q51 38 51 114t-57 111t-125 35h-195v-295zM530 831h181q49 0 89 31t40 84q0 70 -46 101.5t-104 31.5h-160 v-248z" />
+<glyph unicode="C" horiz-adv-x="1314" d="M125 684q0 207 57.5 351.5t155.5 216t201.5 101t226.5 29.5q197 0 438 -96v-305q-205 98 -418 98q-141 0 -225 -86t-84 -309t84 -309t225 -86q213 0 418 98v-305q-242 -96 -438 -96q-123 0 -226.5 29.5t-201.5 101t-155.5 216t-57.5 351.5z" />
+<glyph unicode="D" horiz-adv-x="1443" d="M178 0v1366h492q317 0 483 -174t166 -510q0 -334 -166 -508t-483 -174h-492zM530 289h140q297 0 297 393q0 397 -297 397h-140v-790z" />
+<glyph unicode="E" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952z" />
+<glyph unicode="F" horiz-adv-x="1187" d="M178 0v1366h928v-287h-576v-276h498v-289h-498v-514h-352z" />
+<glyph unicode="G" horiz-adv-x="1427" d="M125 682q0 207 57.5 351.5t155.5 216t201.5 101t226.5 29.5q244 0 471 -102v-283q-207 84 -418 84q-344 0 -344 -395q0 -223 90 -309t242 -86q119 0 188 30v226h-243v262h524v-690q-223 -131 -510 -131q-96 0 -180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293z" />
+<glyph unicode="H" horiz-adv-x="1503" d="M180 0v1366h352v-539h439v539h352v-1366h-352v539h-439v-539h-352z" />
+<glyph unicode="I" horiz-adv-x="708" d="M180 0v1366h352v-1366h-352z" />
+<glyph unicode="J" horiz-adv-x="1060" d="M59 92v326q141 -129 293 -129q72 0 117 41t45 125v911h352v-889q0 -123 -32.5 -215t-79.5 -143t-114 -83t-122 -41t-115 -9q-176 0 -344 106z" />
+<glyph unicode="K" horiz-adv-x="1505" d="M178 0v1366h352v-481l375 481h475l-536 -594l575 -772h-444l-348 532l-97 -108v-424h-352z" />
+<glyph unicode="L" horiz-adv-x="1179" d="M178 0v1366h352v-1049h607v-317h-959z" />
+<glyph unicode="M" horiz-adv-x="1695" d="M180 0v1366h346l322 -465l321 465h347v-1366h-353v846l-315 -453l-316 453v-846h-352z" />
+<glyph unicode="N" horiz-adv-x="1619" d="M178 0v1366h352l555 -819v819h353v-1366h-348l-560 813v-813h-352z" />
+<glyph unicode="O" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM477 684q0 -223 79 -309t210 -86 t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309z" />
+<glyph unicode="P" horiz-adv-x="1337" d="M176 0v1366h553q248 0 385 -126t137 -353q0 -229 -137 -355t-385 -126h-201v-406h-352zM528 696h168q106 0 155.5 56.5t49.5 134.5q0 76 -49 134t-156 58h-168v-383z" />
+<glyph unicode="Q" horiz-adv-x="1529" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293q0 -604 -494 -684q8 -162 213 -162v-227q-500 0 -505 387q-496 80 -496 686zM477 684q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86 t-79 -309z" />
+<glyph unicode="R" horiz-adv-x="1396" d="M178 0v1366h539q250 0 400.5 -116.5t150.5 -327.5q0 -162 -75 -266.5t-200 -145.5l330 -510h-393l-254 457h-146v-457h-352zM530 721h209q72 0 124 50t52 128q0 80 -52 130t-124 50h-209v-358z" />
+<glyph unicode="S" horiz-adv-x="1232" d="M127 954q0 211 145.5 317.5t360.5 106.5q195 0 368 -94v-307q-166 98 -317 98q-80 0 -142.5 -26.5t-62.5 -87.5q0 -39 46 -65t117 -39t151.5 -43t151.5 -73t117 -133t46 -219q0 -180 -143.5 -292.5t-348.5 -112.5q-281 0 -473 133v329q189 -157 419 -157h3 q78 0 134.5 29.5t56.5 84.5q0 37 -35 63t-91 41l-122 31q-66 16 -133.5 45t-122.5 71t-90 119t-35 181z" />
+<glyph unicode="T" horiz-adv-x="1191" d="M25 1047v319h1142v-319h-397v-1047h-352v1047h-393z" />
+<glyph unicode="U" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5z" />
+<glyph unicode="V" horiz-adv-x="1413" d="M25 1366h376l306 -967l305 967h377l-521 -1366h-319z" />
+<glyph unicode="W" horiz-adv-x="2045" d="M27 1366h368l234 -862l243 862h304l243 -862l234 862h368l-440 -1366h-321l-236 782l-236 -782h-321z" />
+<glyph unicode="X" horiz-adv-x="1443" d="M25 0l485 692l-475 674h430l258 -369l260 369h403l-462 -653l503 -713h-430l-288 410l-289 -410h-395z" />
+<glyph unicode="Y" horiz-adv-x="1492" d="M25 1366h428l290 -520l291 520h428l-542 -791v-575h-353v575z" />
+<glyph unicode="Z" horiz-adv-x="1337" d="M133 0v291l617 762h-611v313h1047v-285l-598 -772h598v-309h-1053z" />
+<glyph unicode="[" horiz-adv-x="849" d="M244 -238v1663h436v-247h-209v-1168h209v-248h-436z" />
+<glyph unicode="\" horiz-adv-x="880" d="M-76 1548h256l752 -1749h-254z" />
+<glyph unicode="]" horiz-adv-x="849" d="M172 10h209v1168h-209v247h436v-1663h-436v248z" />
+<glyph unicode="^" horiz-adv-x="1210" d="M246 958l282 455h148l289 -455h-230l-131 244l-131 -244h-227z" />
+<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-209h-1118v209z" />
+<glyph unicode="`" horiz-adv-x="571" d="M92 1393h240l127 -240h-176z" />
+<glyph unicode="a" horiz-adv-x="1142" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86 q-49 4 -68 4q-135 0 -135 -103z" />
+<glyph unicode="b" horiz-adv-x="1230" d="M160 348v1065h350v-383q29 4 133 4q223 0 344 -133t121 -383q0 -534 -502 -534h-2q-137 0 -229 30.5t-136 87t-61.5 114.5t-17.5 132zM510 287q0 -29 26.5 -51.5t81.5 -22.5q72 0 105 90t33 227q0 133 -45 209t-133 76q-18 0 -68 -4v-524z" />
+<glyph unicode="c" horiz-adv-x="993" d="M119 510q0 254 126 390t361 136q178 0 322 -69v-262q-139 63 -258 63q-90 0 -144.5 -64.5t-54.5 -193.5t54.5 -193.5t144.5 -64.5q119 0 258 63v-262q-141 -69 -317 -69h-5q-236 0 -361.5 136t-125.5 390z" />
+<glyph unicode="d" horiz-adv-x="1222" d="M129 518q0 250 121 383t344 133q104 0 133 -4v383h350v-1065q0 -74 -17.5 -132t-61.5 -114.5t-136 -87t-229 -30.5q-504 0 -504 534zM481 530q0 -137 32.5 -227t104.5 -90q55 0 82 22.5t27 51.5v524q-49 4 -68 4q-88 0 -133 -75.5t-45 -209.5z" />
+<glyph unicode="e" horiz-adv-x="1169" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM485 641h230q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z " />
+<glyph unicode="f" horiz-adv-x="776" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141z" />
+<glyph unicode="g" horiz-adv-x="1153" d="M127 516q0 520 491 520q117 0 202 -30.5t128 -84t62.5 -112.5t19.5 -131v-637q0 -51 -10 -102.5t-42 -115t-82 -110.5t-139 -79.5t-204 -32.5q-156 0 -322 53v250q115 -66 252 -66q102 0 145.5 48t49.5 130q-53 -4 -98 -4q-211 0 -332 131t-121 373zM479 510 q0 -127 40 -209t110 -82q24 0 51 6v490q0 80 -80 80q-57 0 -89 -81t-32 -204z" />
+<glyph unicode="h" horiz-adv-x="1316" d="M207 0v1411h352v-387q74 12 174 12q213 0 333 -117.5t120 -316.5v-602h-352v657q0 113 -181 113q-55 0 -94 -14v-756h-352z" />
+<glyph unicode="i" horiz-adv-x="735" d="M193 1139v268h360v-268h-360zM197 0v1022h352v-1022h-352z" />
+<glyph unicode="j" horiz-adv-x="667" d="M6 -141q168 0 168 125v1038h352v-1008q0 -119 -40 -201.5t-114.5 -126.5t-162.5 -62.5t-203 -18.5v254zM170 1139v268h360v-268h-360z" />
+<glyph unicode="k" horiz-adv-x="1318" d="M178 0v1405h352v-631l215 248h451l-416 -451l461 -571h-426l-250 330l-35 -37v-293h-352z" />
+<glyph unicode="l" horiz-adv-x="761" d="M207 0v1411h352v-1411h-352z" />
+<glyph unicode="m" horiz-adv-x="1939" d="M203 0v930l14 6l42 16q26 10 60.5 20.5t78.5 22t92.5 20.5t105.5 15t113 6q147 0 249 -59q175 59 355 59q215 0 337 -110.5t122 -323.5v-602h-353v616q0 158 -141 158q-80 0 -141 -20q26 -77 26 -147v-5v-602h-352v621q0 149 -131 149h-2q-55 0 -123 -18v-752h-352z" />
+<glyph unicode="n" horiz-adv-x="1325" d="M178 0v913q246 123 508 123q217 0 344 -115.5t127 -318.5v-602h-352v657q0 31 -8.5 51.5t-48 41t-115.5 20.5q-55 0 -103 -12v-758h-352z" />
+<glyph unicode="o" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
+<glyph unicode="p" horiz-adv-x="1232" d="M170 -379v1051q0 74 17.5 132t61.5 114.5t136 87t229 30.5q504 0 504 -534q0 -250 -121 -383t-344 -133q-104 0 -133 4v-369h-350zM520 209q49 -4 68 -4q88 0 133 75.5t45 208.5q0 137 -32.5 227.5t-104.5 90.5q-55 0 -82 -22.5t-27 -51.5v-524z" />
+<glyph unicode="q" horiz-adv-x="1228" d="M115 502q0 534 501 534h2q137 0 229.5 -30.5t136.5 -87t61.5 -115t17.5 -131.5v-1051h-350v369q-29 -4 -133 -4q-223 0 -344 133t-121 383zM467 489q0 -133 45 -208.5t133 -75.5q18 0 68 4v524q0 29 -27 51.5t-82 22.5q-72 0 -104.5 -90t-32.5 -228z" />
+<glyph unicode="r" horiz-adv-x="813" d="M164 0v922q264 109 584 116v-295q-127 -2 -232 -18v-725h-352z" />
+<glyph unicode="s" horiz-adv-x="962" d="M90 694q0 166 115 254t293 88q156 0 293 -84v-233q-133 74 -236 74q-111 0 -111 -70q0 -20 45.5 -40.5t110 -46.5t128 -61.5t108.5 -105.5t45 -164q0 -141 -113 -230t-293 -89q-190 0 -346 100v272q160 -125 309 -125q92 0 92 56q0 27 -45 48t-109.5 45t-130 57.5 t-110.5 99t-45 155.5z" />
+<glyph unicode="t" horiz-adv-x="839" d="M49 786v236h139v303h353v-303h213v-236h-213v-495q0 -23 18.5 -39t44.5 -16q63 0 150 51v-252q-88 -49 -205 -49q-178 0 -269.5 94t-91.5 264v442h-139z" />
+<glyph unicode="u" horiz-adv-x="1280" d="M172 461v561h352v-623q0 -150 122 -150l1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475z" />
+<glyph unicode="v" horiz-adv-x="1224" d="M66 1022h374l170 -629l172 629h377l-391 -1022h-311z" />
+<glyph unicode="w" horiz-adv-x="1763" d="M82 1022h373l133 -616l162 616h262l162 -616l133 616h372l-362 -1022h-309l-127 459l-127 -459h-310z" />
+<glyph unicode="x" horiz-adv-x="1249" d="M53 0l363 518l-353 504h414l148 -207l145 207h389l-350 -479l387 -543h-416l-172 252l-166 -252h-389z" />
+<glyph unicode="y" horiz-adv-x="1169" d="M63 1022h365l156 -588l153 588h363l-373 -1002q-90 -231 -177 -327t-212 -96q-133 0 -223 43v256q70 -25 135 -25q47 0 92 56.5t78 170.5z" />
+<glyph unicode="z" horiz-adv-x="1030" d="M94 0v289l424 461h-414v272h805v-285l-381 -444h381v-293h-815z" />
+<glyph unicode="{" horiz-adv-x="804" d="M186 485v238q4 0 11.5 2t25 10t30.5 19.5t24.5 33t11.5 48.5v270q0 145 81 235.5t242 90.5h62v-236h-45q-45 0 -68.5 -22.5t-27 -45t-3.5 -67.5v-238q0 -94 -56 -147t-138 -72q82 -18 138 -71.5t56 -147.5v-238q0 -45 3.5 -67.5t27 -45t68.5 -22.5h45v-235h-62 q-162 0 -242.5 90t-80.5 235v271q0 41 -25.5 69.5t-50.5 36.5z" />
+<glyph unicode="|" horiz-adv-x="1005" d="M379 -203v1569h270v-1569h-270z" />
+<glyph unicode="}" horiz-adv-x="804" d="M156 12h45q45 0 68.5 22.5t26.5 45t3 67.5v238q0 94 56.5 147.5t138.5 71.5q-82 18 -138.5 71.5t-56.5 147.5v238q0 45 -3 67.5t-26.5 45t-68.5 22.5h-45v236h61q162 0 243 -90.5t81 -235.5v-270q0 -41 25.5 -70t51.5 -37l25 -6v-238q-4 0 -11 -2t-24.5 -10t-31 -19.5 t-24.5 -33t-11 -47.5v-271q0 -145 -81 -235t-243 -90h-61v235z" />
+<glyph unicode="~" horiz-adv-x="1071" d="M201 457v186q57 68 129 74q11 1 23 1q59 0 116 -26l135 -60q57 -25 115 -25q12 0 23 1q71 6 128 74v-186q-57 -68 -128 -74q-11 -1 -23 -1q-58 0 -115 25l-135 60q-56 26 -115 26q-12 0 -24 -2q-72 -6 -129 -73z" />
+<glyph unicode="&#xa1;" horiz-adv-x="882" d="M270 -344l45 975h265l45 -975h-355zM289 760v262h317v-262h-317z" />
+<glyph unicode="&#xa2;" horiz-adv-x="1007" d="M117 682q0 254 126 390t361 136h29v158h131v-174q90 -16 162 -53v-262q-90 43 -162 55v-500q70 14 162 55v-262q-74 -35 -162 -53v-172h-131v156h-29q-236 0 -361.5 136t-125.5 390zM469 682q0 -115 45 -178.5t119 -75.5v510q-164 -29 -164 -256z" />
+<glyph unicode="&#xa3;" horiz-adv-x="1153" d="M100 -31v307q80 55 158 74v203h-158v268h158v168q0 162 105.5 277.5t328.5 115.5q162 0 346 -106v-281q-141 119 -299 119q-90 0 -144 -35t-54 -104v-154h340v-268h-340v-211q117 -53 227 -53q141 0 270 86v-297q-82 -76 -181 -90q-35 -6 -70 -6q-62 0 -123 17l-187 51 q-60 17 -122 17q-36 0 -73 -5q-100 -15 -182 -93z" />
+<glyph unicode="&#xa5;" horiz-adv-x="1492" d="M37 1366h428l291 -520l291 520h428l-408 -594h137v-201h-272v-155h272v-201h-272v-215h-352v215h-273v201h273v155h-273v201h137z" />
+<glyph unicode="&#xa7;" horiz-adv-x="1247" d="M127 707q0 98 91 145t192 49q-43 4 -95.5 60.5t-52.5 121.5q0 86 39 148.5t106.5 94.5t141.5 45t160 13q180 0 362 -104v-248q-160 92 -311 92q-170 0 -170 -80q0 -31 84 -63.5t184 -62t184 -100.5t84 -171q0 -61 -48 -100t-94 -50.5t-93 -13.5q49 -27 74.5 -87 t25.5 -122q0 -137 -119.5 -213.5t-328.5 -76.5q-213 0 -410 131v276q171 -153 361 -153h2q66 0 120 17t54 58q0 27 -56.5 49.5t-136.5 46t-158.5 56.5t-135 95.5t-56.5 146.5zM453 715q0 -18 58 -43t210 -76q78 14 78 45q0 18 -49.5 38.5t-123 43t-89.5 29.5 q-84 -10 -84 -37z" />
+<glyph unicode="&#xa8;" horiz-adv-x="899" d="M141 1139v223h246v-223h-246zM489 1139v223h246v-223h-246z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -205 -493.5t-495 -204.5q-289 0 -494 204.5t-205 493.5zM227 682q0 -236 167 -402.5t402.5 -166.5t403.5 166.5t168 402.5q0 238 -167 404.5t-404 166.5q-236 0 -403 -166.5t-167 -404.5z M430 684q0 121 34 206t91 126t117.5 58.5t132.5 17.5q117 0 258 -56v-178q-123 57 -246 57q-180 0 -180 -231t180 -231q123 0 246 57v-178q-139 -57 -258 -58q-72 0 -132.5 17.5t-117.5 59.5t-91 127t-34 206z" />
+<glyph unicode="&#xaa;" horiz-adv-x="833" d="M154 970q0 24 8 48t31.5 55t79 50.5t137.5 19.5h51q0 86 -78 86q-80 0 -174 -35v147q102 41 188 41q125 0 195 -64.5t70 -182.5v-136q0 -106 -57.5 -155t-205.5 -49q-245 0 -245 175zM350 1006q0 -61 58 -62q27 0 42 22.5t15 45.5v49q-8 0 -20.5 1t-18.5 1q-76 0 -76 -57 z" />
+<glyph unicode="&#xab;" horiz-adv-x="1347" d="M70 522l346 389h338l-373 -389l373 -391h-338zM565 522l346 389h338l-372 -389l372 -391h-338z" />
+<glyph unicode="&#xac;" horiz-adv-x="1300" d="M199 465v219h903v-389h-211v170h-692z" />
+<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
+<glyph unicode="&#xae;" horiz-adv-x="1593" d="M100 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -494 204.5t-205 493.5zM229 682q0 -236 167 -402.5t402.5 -166.5t403.5 166.5t168 402.5q0 238 -167 404.5t-404 166.5q-236 0 -403 -166.5t-167 -404.5 zM498 285v796h315q145 0 233.5 -67.5t88.5 -190.5q0 -94 -44 -155t-118 -84l192 -299h-229l-148 266h-83v-266h-207zM705 707h120q41 0 72 28.5t31 73.5q0 47 -31 76.5t-72 29.5h-120v-208z" />
+<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1139v180h479v-180h-479z" />
+<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1143q0 111 57.5 169t166 58t165.5 -58.5t57 -168.5q0 -109 -57 -167t-165.5 -58t-166 58t-57.5 167zM293 1145q0 -57 23.5 -83t74.5 -26t73.5 25.5t22.5 83t-22.5 82t-73.5 24.5q-98 0 -98 -106z" />
+<glyph unicode="&#xb1;" horiz-adv-x="1198" d="M236 12v215h743v-215h-743zM236 608v230h258v258h229v-258h256v-230h-256v-256h-229v256h-258z" />
+<glyph unicode="&#xb2;" horiz-adv-x="696" d="M102 948v150q311 231 312 346q0 74 -97 74q-98 0 -211 -91v193q121 74 230 74q137 0 201.5 -69t64.5 -167q0 -154 -211 -340h215v-170h-504z" />
+<glyph unicode="&#xb3;" horiz-adv-x="686" d="M100 999v183q121 -74 215 -74q49 0 79 21.5t30 54.5q0 82 -111 82h-120v135h122q76 0 76 61q0 27 -29.5 41.5t-66.5 14.5q-98 0 -180 -39v159q100 53 200 54q106 0 186.5 -47.5t80.5 -155.5q0 -51 -36 -93t-79 -50q66 -18 105.5 -70.5t39.5 -122.5q0 -119 -86 -167 t-202 -48q-96 0 -224 61z" />
+<glyph unicode="&#xb4;" horiz-adv-x="559" d="M121 1139l127 239h239l-190 -239h-176z" />
+<glyph unicode="&#xb5;" horiz-adv-x="1325" d="M203 -430v1452h352v-657q0 -31 8 -51.5t48 -41t116 -20.5q55 0 102 12v758h353v-913q-246 -123 -508 -123q-57 0 -119 10v-426h-352z" />
+<glyph unicode="&#xb6;" horiz-adv-x="1083" d="M80 870q0 240 143.5 368t413.5 128h354v-1366h-274v371h-80q-270 0 -413.5 129t-143.5 370z" />
+<glyph unicode="&#xb7;" horiz-adv-x="741" d="M215 420v282h295v-282h-295z" />
+<glyph unicode="&#xb8;" horiz-adv-x="585" d="M106 -250l191 240h176l-127 -240h-240z" />
+<glyph unicode="&#xb9;" horiz-adv-x="512" d="M92 1401v186l154 96h155v-735h-190v525z" />
+<glyph unicode="&#xba;" horiz-adv-x="868" d="M152 1090q0 143 71.5 218.5t204.5 75.5q131 0 202.5 -75.5t71.5 -218.5q0 -141 -71.5 -217t-202.5 -76q-133 0 -204.5 75.5t-71.5 217.5zM348 1090q0 -150 80 -150q78 0 78 150q0 151 -77 151h-1q-80 0 -80 -151z" />
+<glyph unicode="&#xbb;" horiz-adv-x="1374" d="M106 131l373 391l-373 389h338l347 -389l-347 -391h-338zM602 131l373 391l-373 389h338l346 -389l-346 -391h-338z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1476" d="M92 1098v186l154 96h155v-735h-190v524zM170 -45l895 1448h192l-895 -1448h-192zM840 186v146l321 403h166v-387h88v-162h-88v-186h-188v186h-299zM1040 348h99v129z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1550" d="M92 1098v186l154 96h155v-735h-190v524zM170 -45l895 1448h192l-895 -1448h-192zM946 0v150q311 231 311 346q0 73 -93 73h-3q-98 0 -211 -90v193q119 73 227 73h3q137 0 201.5 -68.5t64.5 -166.5q0 -154 -211 -340h215v-170h-504z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1757" d="M137 696v183q121 -74 215 -74q49 0 79 21.5t30 54.5q0 82 -111 82h-121v135h123q76 0 76 61q0 27 -29.5 41t-66.5 14q-98 0 -180 -38v159q100 53 200 54q106 0 186 -47.5t80 -155.5q0 -51 -35.5 -93.5t-78.5 -50.5q66 -18 105.5 -70t39.5 -122q0 -119 -86 -167t-203 -48 q-96 0 -223 61zM387 -45l895 1448h193l-895 -1448h-193zM1067 186v146l322 403h165v-387h88v-162h-88v-186h-188v186h-299zM1268 348h98v129z" />
+<glyph unicode="&#xbf;" horiz-adv-x="1075" d="M164 37q0 70 22.5 126t56 92t72.5 74l73 73q34 34 56.5 87.5t22.5 118.5v21h281v-27q0 -104 -31 -184t-75 -127t-89 -86t-76 -81t-31 -89q0 -45 15.5 -76t46.5 -43t58.5 -16t66.5 -4q180 0 307 102v-262q-59 -41 -155.5 -68.5t-196.5 -27.5q-115 0 -200 33.5t-132 92 t-69.5 126t-22.5 145.5zM449 760v262h317v-262h-317z" />
+<glyph unicode="&#xc0;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM477 1722h240l127 -239h-176zM612 588h277l-137 424z" />
+<glyph unicode="&#xc1;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM612 588h277l-137 424zM659 1483l127 239h240l-190 -239h-177z" />
+<glyph unicode="&#xc2;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM457 1483l245 250h99l246 -250h-209l-86 90l-86 -90h-209zM612 588h277l-137 424z" />
+<glyph unicode="&#xc3;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM440 1497v174q57 68 124 74q11 1 22 1q55 0 106 -26l121 -60q51 -25 105 -25q11 0 22 1q66 6 123 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-121 60q-51 26 -106 26q-11 0 -22 -1 q-66 -6 -124 -74zM612 588h277l-137 424z" />
+<glyph unicode="&#xc4;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM455 1483v223h245v-223h-245zM612 588h277l-137 424zM803 1483v223h246v-223h-246z" />
+<glyph unicode="&#xc5;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM573 1618q0 84 47.5 130t131.5 46q86 0 132 -46t46 -130t-46 -130t-132 -46q-84 0 -131.5 46t-47.5 130zM612 588h277l-137 424zM700 1618q0 -37 11.5 -50.5t40 -13.5t40 13.5t11.5 50.5q0 63 -51.5 63 t-51.5 -63z" />
+<glyph unicode="&#xc6;" horiz-adv-x="2217" d="M51 0l961 1366h1065v-287h-600v-252h520v-288h-520v-250h600v-289h-953v246h-469l-168 -246h-436zM831 504h293v428z" />
+<glyph unicode="&#xc7;" horiz-adv-x="1314" d="M125 684q0 207 57.5 351.5t155.5 216t201.5 101t226.5 29.5q197 0 438 -96v-305q-205 98 -418 98q-141 0 -225 -86t-84 -309t84 -309t225 -86q213 0 418 98v-305q-195 -78 -356 -92l-125 -240h-240l189 240q-109 10 -200 48t-173 112.5t-128 211t-46 322.5z" />
+<glyph unicode="&#xc8;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM424 1722h240l127 -239h-177z" />
+<glyph unicode="&#xc9;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM561 1483l127 239h240l-191 -239h-176z" />
+<glyph unicode="&#xca;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM375 1483l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
+<glyph unicode="&#xcb;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM383 1483v223h246v-223h-246zM731 1483v223h246v-223h-246z" />
+<glyph unicode="&#xcc;" horiz-adv-x="708" d="M76 1722h239l127 -239h-176zM180 0h352v1366h-352v-1366z" />
+<glyph unicode="&#xcd;" horiz-adv-x="708" d="M180 0h352v1366h-352v-1366zM270 1483l127 239h240l-191 -239h-176z" />
+<glyph unicode="&#xce;" horiz-adv-x="708" d="M61 1483l246 250h99l245 -250h-209l-86 90l-86 -90h-209zM180 0h352v1366h-352v-1366z" />
+<glyph unicode="&#xcf;" horiz-adv-x="708" d="M59 1483v223h246v-223h-246zM180 0h352v1366h-352v-1366zM408 1483v223h245v-223h-245z" />
+<glyph unicode="&#xd0;" horiz-adv-x="1527" d="M106 582v200h166v584h492q317 0 483 -174t166 -510q0 -334 -166 -508t-483 -174h-492v582h-166zM625 289h139q297 0 297 393q0 397 -297 397h-139v-297h217v-200h-217v-293z" />
+<glyph unicode="&#xd1;" horiz-adv-x="1619" d="M178 0v1366h352l555 -819v819h353v-1366h-348l-560 813v-813h-352zM510 1497v174q57 68 124 74q10 1 21 1q55 0 105 -26l122 -60q52 -25 106 -25q11 0 22 1q65 6 123 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -106 25l-122 60q-50 26 -105 26q-11 0 -21 -1 q-67 -6 -124 -74z" />
+<glyph unicode="&#xd2;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM477 684q0 -223 79 -309t210 -86 t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309zM500 1722h239l127 -239h-176z" />
+<glyph unicode="&#xd3;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM477 684q0 -223 79 -309t210 -86 t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309zM666 1483l127 239h239l-190 -239h-176z" />
+<glyph unicode="&#xd4;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM471 1483l246 250h98l246 -250h-209 l-86 90l-86 -90h-209zM477 684q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309z" />
+<glyph unicode="&#xd5;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM459 1497v174q57 68 123 74q11 1 22 1 q55 0 106 -26l122 -60q51 -25 105 -25q11 0 21 1q66 6 123 74v-174q-57 -68 -123 -74q-10 -1 -21 -1q-54 0 -105 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -123 -74zM477 684q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309z" />
+<glyph unicode="&#xd6;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM469 1483v223h246v-223h-246zM477 684 q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309zM817 1483v223h246v-223h-246z" />
+<glyph unicode="&#xd7;" horiz-adv-x="1093" d="M147 154l287 370l-284 371h225l174 -223l176 223h223l-284 -371l284 -370h-223l-176 223l-174 -223h-228z" />
+<glyph unicode="&#xd8;" horiz-adv-x="1529" d="M14 0l211 246q-100 168 -100 436q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16q215 0 360 -84l62 72h328l-211 -246q102 -168 102 -438q0 -166 -39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16q-217 0 -363 86l-61 -72h-328zM477 682q0 -82 8 -133 l426 495q-57 33 -145 33q-131 0 -210 -86t-79 -309zM618 319q55 -32 143 -32h5q131 0 210 86t79 309q0 70 -11 135z" />
+<glyph unicode="&#xd9;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM506 1722h239l127 -239h-176z" />
+<glyph unicode="&#xda;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM651 1483l127 239h240l-191 -239h-176z" />
+<glyph unicode="&#xdb;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM467 1483l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
+<glyph unicode="&#xdc;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM465 1483v223h246v-223h-246zM813 1483v223h246v-223h-246z" />
+<glyph unicode="&#xdd;" horiz-adv-x="1492" d="M25 1366h428l290 -520l291 520h428l-542 -791v-575h-353v575zM635 1483l127 239h239l-190 -239h-176z" />
+<glyph unicode="&#xde;" horiz-adv-x="1337" d="M176 0v1366h352v-203h177q248 0 385 -126t137 -353q0 -229 -137.5 -355t-384.5 -126h-177v-203h-352zM528 494h144q106 0 155.5 56t49.5 134q0 76 -49.5 134.5t-155.5 58.5h-144v-383z" />
+<glyph unicode="&#xdf;" horiz-adv-x="1282" d="M180 0v846q0 160 41 271.5t110.5 166.5t143.5 77.5t162 22.5q236 0 357.5 -109.5t121.5 -266.5q0 -106 -60 -187.5t-130 -101.5q250 -74 250 -313q0 -221 -167 -318q-149 -87 -391 -87q-30 0 -61 1v256h12q124 0 217 38q97 40 97 134q0 51 -32 84t-91.5 47t-100.5 19.5 t-102 7.5v233q43 0 84 6.5t88 21.5t74.5 48t27.5 82q0 162 -167 162q-190 0 -191 -267v-874h-293z" />
+<glyph unicode="&#xe0;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM326 1378h239l127 -239h-176zM459 362 q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
+<glyph unicode="&#xe1;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM442 1139l127 239h240l-191 -239h-176zM459 362 q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
+<glyph unicode="&#xe2;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM268 1139l246 250h98l246 -250h-209l-86 90l-86 -90 h-209zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
+<glyph unicode="&#xe3;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM252 1153v174q57 68 124 74q10 1 21 1q55 0 105 -26 l122 -60q52 -25 106 -25q11 0 22 1q65 6 122 74v-174q-57 -68 -122 -74q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
+<glyph unicode="&#xe4;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM266 1139v223h246v-223h-246zM459 362 q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103zM614 1139v223h246v-223h-246z" />
+<glyph unicode="&#xe5;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM385 1272q0 84 47 130t131 46q86 0 132 -46t46 -130 t-46 -130t-132 -46q-84 0 -131 46t-47 130zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103zM512 1272q0 -37 11.5 -50.5t40 -13.5t39.5 13.5t11 50.5q0 63 -51 63t-51 -63z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1792" d="M127 297q0 31 7 63.5t35 78.5t73 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -312 -64v267q176 71 332 71h4q92 0 175 -44t124 -91q35 51 113 94t170 43q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248 q-166 -106 -389 -106q-96 0 -175 48t-112 109q-89 -157 -319 -157h-3q-438 0 -438 315zM479 358q0 -108 99 -108h2q49 0 75.5 39t26.5 82v86q-49 4 -68 4q-135 0 -135 -103zM1030 637h230q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
+<glyph unicode="&#xe7;" horiz-adv-x="993" d="M119 510q0 254 126 390t361 136q178 0 322 -69v-262q-139 63 -258 63q-90 0 -144.5 -64.5t-54.5 -193.5t54.5 -193.5t144.5 -64.5q119 0 258 63v-262q-106 -51 -238 -63l-125 -240h-239l188 240q-193 25 -294 158t-101 362z" />
+<glyph unicode="&#xe8;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM338 1393h240l127 -240h-177zM485 641h230 q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
+<glyph unicode="&#xe9;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM475 1139l127 239h240l-191 -239h-176zM485 641h230 q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
+<glyph unicode="&#xea;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM301 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209z M485 641h230q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
+<glyph unicode="&#xeb;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM299 1139v223h246v-223h-246zM485 641h230 q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121zM647 1139v223h246v-223h-246z" />
+<glyph unicode="&#xec;" horiz-adv-x="735" d="M92 1393h240l127 -240h-176zM197 0h352v1022h-352v-1022z" />
+<glyph unicode="&#xed;" horiz-adv-x="735" d="M197 0h352v1022h-352v-1022zM287 1139l127 239h239l-190 -239h-176z" />
+<glyph unicode="&#xee;" horiz-adv-x="735" d="M78 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209zM197 0h352v1022h-352v-1022z" />
+<glyph unicode="&#xef;" horiz-adv-x="735" d="M76 1139v223h246v-223h-246zM197 0h352v1022h-352v-1022zM424 1139v223h246v-223h-246z" />
+<glyph unicode="&#xf0;" horiz-adv-x="1271" d="M117 481q0 131 41 220.5t115.5 129t148.5 55t170 15.5q123 0 190 -27q-14 109 -123 164l-151 -118l-92 114l61 49h-16q-90 0 -199 -30v301q91 30 219 30h8q162 0 291 -59l107 84l92 -117l-68 -53q215 -193 215 -606q0 -649 -505 -649q-131 0 -232.5 45t-159 118.5 t-85 158.5t-27.5 175zM471 483q0 -31 7 -68.5t44.5 -82.5t98.5 -45q66 0 113 95q42 87 42 247v28q-53 23 -151 23q-78 0 -116 -59.5t-38 -137.5z" />
+<glyph unicode="&#xf1;" horiz-adv-x="1308" d="M178 0v913q246 123 508 123q217 0 344 -115.5t127 -318.5v-602h-352v657q0 31 -8.5 51.5t-48 41t-115.5 20.5q-55 0 -103 -12v-758h-352zM342 1153v174q57 68 124 74q10 1 21 1q55 0 105 -26l122 -60q52 -25 106 -25q11 0 22 1q65 6 123 74v-174q-57 -68 -123 -74 q-11 -1 -22 -1q-54 0 -106 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74z" />
+<glyph unicode="&#xf2;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM332 1378h239l127 -239h-176zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
+<glyph unicode="&#xf3;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270zM469 1139l127 239h240l-191 -239h-176z" />
+<glyph unicode="&#xf4;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM289 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
+<glyph unicode="&#xf5;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM276 1153v174q57 68 124 74q11 1 22 1q55 0 106 -26l122 -60q51 -25 105 -25q11 0 21 1q66 6 123 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60 q-51 26 -106 26q-11 0 -22 -1q-66 -6 -124 -74zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
+<glyph unicode="&#xf6;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM287 1139v223h245v-223h-245zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270zM635 1139v223h246v-223h-246z" />
+<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M188 418v231h916v-231h-916zM520 47v240h252v-240h-252zM520 782v240h252v-240h-252z" />
+<glyph unicode="&#xf8;" horiz-adv-x="1255" d="M23 0l178 209q-68 119 -68 303q0 254 128 389t364 135q141 0 243 -49l31 35h330l-178 -207q65 -119 65 -298v-5q0 -254 -128 -389t-363 -135q-139 0 -244 49l-31 -37h-327zM487 541l193 227q-28 14 -55 14q-131 0 -138 -241zM569 256q23 -14 56 -14q131 0 139 239z" />
+<glyph unicode="&#xf9;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM391 1378h240l127 -239h-176z" />
+<glyph unicode="&#xfa;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM537 1139l127 239h239l-190 -239h-176z" />
+<glyph unicode="&#xfb;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM352 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
+<glyph unicode="&#xfc;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM350 1139v223h246v-223h-246zM698 1139v223h246v-223h-246z" />
+<glyph unicode="&#xfd;" horiz-adv-x="1169" d="M63 1022h373l148 -588l145 588h371l-373 -1002q-90 -231 -177 -327t-212 -96q-133 0 -223 43v256q70 -25 135 -25q47 0 92 56.5t78 170.5zM485 1139l127 239h240l-190 -239h-177z" />
+<glyph unicode="&#xfe;" horiz-adv-x="1218" d="M172 -375v1778h350v-379q43 6 133 6q223 0 344 -133t121 -383t-120.5 -383t-344.5 -133q-90 0 -133 6v-379h-350zM522 221q50 -4 68 -4q178 12 178 297t-178 297q-18 0 -68 -4v-586z" />
+<glyph unicode="&#xff;" horiz-adv-x="1169" d="M63 1022h373l148 -588l145 588h371l-373 -1002q-90 -231 -177 -327t-212 -96q-133 0 -223 43v256q70 -25 135 -25q47 0 92 56.5t78 170.5zM287 1139v223h245v-223h-245zM635 1139v223h246v-223h-246z" />
+<glyph unicode="&#x152;" horiz-adv-x="2160" d="M123 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16q168 0 293 -51v37h952v-287h-600v-252h520v-288h-520v-250h600v-289h-952v39q-125 -51 -293 -51q-96 0 -180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM475 684q0 -223 79 -309t210 -86t210 86t79 309 t-79 309t-210 86t-210 -86t-79 -309z" />
+<glyph unicode="&#x153;" horiz-adv-x="1853" d="M125 512q0 254 128 389t363 135q190 0 308 -86q117 88 297 88q229 0 347 -144t118 -353q0 -61 -7 -111h-581q0 -72 67.5 -121t151.5 -49q188 0 321 80v-248q-166 -106 -401 -106q-193 0 -311 88q-119 -86 -310 -86q-236 0 -363.5 135t-127.5 389zM477 512 q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270zM1106 641h229q-4 59 -34.5 116.5t-80 57.5t-80 -53t-34.5 -121z" />
+<glyph unicode="&#x178;" horiz-adv-x="1492" d="M25 1366h428l290 -520l291 520h428l-542 -791v-575h-353v575zM449 1483v223h245v-223h-245zM797 1483v223h245v-223h-245z" />
+<glyph unicode="&#x2c6;" horiz-adv-x="874" d="M137 1155l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
+<glyph unicode="&#x2dc;" horiz-adv-x="1058" d="M215 1153v174q57 68 124 74q11 1 22 1q55 0 105 -26l122 -60q51 -25 105 -25q11 0 21 1q66 6 124 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74z" />
+<glyph unicode="&#x2000;" horiz-adv-x="897" />
+<glyph unicode="&#x2001;" horiz-adv-x="1794" />
+<glyph unicode="&#x2002;" horiz-adv-x="897" />
+<glyph unicode="&#x2003;" horiz-adv-x="1794" />
+<glyph unicode="&#x2004;" horiz-adv-x="598" />
+<glyph unicode="&#x2005;" horiz-adv-x="448" />
+<glyph unicode="&#x2006;" horiz-adv-x="299" />
+<glyph unicode="&#x2007;" horiz-adv-x="299" />
+<glyph unicode="&#x2008;" horiz-adv-x="224" />
+<glyph unicode="&#x2009;" horiz-adv-x="358" />
+<glyph unicode="&#x200a;" horiz-adv-x="99" />
+<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
+<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
+<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 475v240h860v-240h-860z" />
+<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 475v240h1501v-240h-1501z" />
+<glyph unicode="&#x2018;" horiz-adv-x="616" d="M96 1384h285l151 -401h-239z" />
+<glyph unicode="&#x2019;" horiz-adv-x="616" d="M96 983l152 401h284l-196 -401h-240z" />
+<glyph unicode="&#x201a;" horiz-adv-x="698" d="M147 -229l152 401h285l-197 -401h-240z" />
+<glyph unicode="&#x201c;" horiz-adv-x="980" d="M98 1384h285l152 -401h-240zM455 1384h284l152 -401h-240z" />
+<glyph unicode="&#x201d;" horiz-adv-x="980" d="M98 983l152 401h285l-197 -401h-240zM455 983l151 401h285l-197 -401h-239z" />
+<glyph unicode="&#x201e;" horiz-adv-x="1056" d="M147 -229l152 401h285l-197 -401h-240zM504 -229l151 401h285l-197 -401h-239z" />
+<glyph unicode="&#x2022;" horiz-adv-x="1085" d="M172 665.5q0 153.5 109.5 262t263 108.5t261 -107.5t107.5 -263t-107.5 -263t-261 -107.5t-263 108.5t-109.5 262z" />
+<glyph unicode="&#x2026;" horiz-adv-x="1849" d="M219 0v283h295v-283h-295zM780 0v283h295v-283h-295zM1343 0v283h295v-283h-295z" />
+<glyph unicode="&#x202f;" horiz-adv-x="358" />
+<glyph unicode="&#x2039;" horiz-adv-x="819" d="M63 522l347 389h338l-373 -389l373 -391h-338z" />
+<glyph unicode="&#x203a;" horiz-adv-x="843" d="M80 131l373 391l-373 389h338l346 -389l-346 -391h-338z" />
+<glyph unicode="&#x205f;" horiz-adv-x="448" />
+<glyph unicode="&#x20ac;" horiz-adv-x="1466" d="M98 408v190h166q-2 27 -2 86t2 88h-166v191h197q117 419 606 419h2q199 0 438 -96v-305q-204 98 -417 98q-174 0 -248 -116h397v-191h-455q-4 -53 -4 -88q0 -33 4 -86h455v-190h-397q74 -119 248 -119q213 0 417 98v-305q-240 -96 -438 -96q-492 0 -608 422h-197z" />
+<glyph unicode="&#x2122;" horiz-adv-x="1581" d="M100 1206v160h572v-160h-199v-522h-176v522h-197zM772 684v682h174l160 -233l160 233h174v-682h-176v422l-158 -227l-158 227v-422h-176z" />
+<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
+<glyph unicode="&#xfb01;" horiz-adv-x="1462" d="M84 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-206v-774h-353v774h-141zM981 1139h360v268h-360v-268zM985 0h352v1022h-352v-1022z" />
+<glyph unicode="&#xfb02;" horiz-adv-x="1462" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141zM983 0h352v1411h-352v-1411z" />
+<glyph unicode="&#xfb03;" horiz-adv-x="2287" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141zM858 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141z M1745 1139v268h360v-268h-360zM1749 0v1022h352v-1022h-352z" />
+<glyph unicode="&#xfb04;" horiz-adv-x="2313" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141zM858 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141z M1759 0v1411h352v-1411h-352z" />
+</font>
+</defs></svg> 
\ No newline at end of file
diff --git a/motioneye/static/fnt/mavenpro-black-webfont.ttf b/motioneye/static/fnt/mavenpro-black-webfont.ttf
new file mode 100644 (file)
index 0000000..38d7d3f
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-black-webfont.ttf differ
diff --git a/motioneye/static/fnt/mavenpro-black-webfont.woff b/motioneye/static/fnt/mavenpro-black-webfont.woff
new file mode 100644 (file)
index 0000000..6ddfe8c
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-black-webfont.woff differ
diff --git a/motioneye/static/fnt/mavenpro-bold-webfont.eot b/motioneye/static/fnt/mavenpro-bold-webfont.eot
new file mode 100644 (file)
index 0000000..030d7cb
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-bold-webfont.eot differ
diff --git a/motioneye/static/fnt/mavenpro-bold-webfont.svg b/motioneye/static/fnt/mavenpro-bold-webfont.svg
new file mode 100644 (file)
index 0000000..43eb15b
--- /dev/null
@@ -0,0 +1,243 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="maven_probold" horiz-adv-x="1441" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="692" />
+<glyph horiz-adv-x="2048" />
+<glyph horiz-adv-x="2048" />
+<glyph unicode="&#xd;" horiz-adv-x="682" />
+<glyph unicode=" "  horiz-adv-x="692" />
+<glyph unicode="&#x09;" horiz-adv-x="692" />
+<glyph unicode="&#xa0;" horiz-adv-x="692" />
+<glyph unicode="!" horiz-adv-x="825" d="M283 1366h280l-45 -973h-190zM303 0v227h240v-227h-240z" />
+<glyph unicode="&#x22;" horiz-adv-x="825" d="M166 1384h205l-35 -411h-133zM461 1384h205l-35 -411h-133z" />
+<glyph unicode="#" horiz-adv-x="1216" d="M152 256v152h180l51 292h-174v152h201l36 217h162l-39 -217h232l39 217h159l-38 -217h133v-152h-158l-51 -292h153v-152h-178l-39 -217h-162l39 217h-231l-37 -217h-162l39 217h-155zM494 408h231l49 292h-229z" />
+<glyph unicode="$" horiz-adv-x="1181" d="M150 987q0 174 111.5 273.5t289.5 119.5v209h119v-207q168 -6 334 -92v-239q-160 102 -334 104v-340q74 -23 120 -39t109.5 -53t100 -81t63.5 -116t27 -164q0 -154 -121 -257t-299 -117v-209h-119v209q-219 14 -383 114v250q174 -129 383 -131v379q-76 23 -117 37 t-104.5 47t-96 70t-58 97t-25.5 136zM395 993q0 -53 39 -84.5t117 -58.5v293q-156 -33 -156 -150zM670 229q174 27 174 150q0 121 -174 184v-334z" />
+<glyph unicode="%" horiz-adv-x="1777" d="M145 983q0 377 289 377q291 0 291 -377t-291 -377q-289 0 -289 377zM305 983q0 -133 36 -179t93.5 -46t93 46t35.5 179q0 135 -35.5 180t-93 45t-93.5 -45t-36 -180zM381 0l858 1366h174l-858 -1366h-174zM1065 383q0 377 289 377q291 0 291 -377t-291 -377 q-289 0 -289 377zM1225 383q0 -133 35.5 -179t93 -46t93.5 46t36 179q0 135 -36 180t-93.5 45t-93 -45t-35.5 -180z" />
+<glyph unicode="&#x26;" horiz-adv-x="1310" d="M113 389q0 53 7 94t31.5 96.5t79 108.5t138.5 103q-125 131 -125 249q0 137 85 235.5t240 98.5q139 0 232.5 -92t93.5 -227q0 -104 -50 -174t-173 -152l229 -274q29 72 43 155h201q-29 -180 -109 -319l244 -291h-262l-105 129q-152 -145 -364 -145q-113 0 -201 41 t-137 103t-73.5 130t-24.5 131zM307 383q0 -39 17.5 -83t77 -87t151.5 -43q135 0 242 106l-301 361q-66 -35 -108 -72t-57.5 -72.5t-18.5 -56t-3 -53.5zM438 1059q0 -14 5.5 -31.5t10.5 -29t18 -32t19.5 -27.5t25 -29.5t22.5 -26.5q96 57 129.5 97t33.5 87q0 53 -37.5 90 t-93 37t-94.5 -39t-39 -96z" />
+<glyph unicode="'" horiz-adv-x="534" d="M166 1384h205l-35 -411h-133z" />
+<glyph unicode="(" horiz-adv-x="751" d="M190 627q0 440 211 856h213q-211 -416 -211 -856q0 -436 211 -858h-213q-211 422 -211 858z" />
+<glyph unicode=")" horiz-adv-x="751" d="M133 -231q211 412 211 858q0 449 -211 856h213q211 -416 211 -856q0 -436 -211 -858h-213z" />
+<glyph unicode="*" horiz-adv-x="845" d="M172 1133l166 57l-166 57l78 139l123 -104l-25 156h160l-25 -156l123 104l78 -139l-166 -57l166 -57l-78 -140l-123 105l25 -156h-160l25 156l-123 -105z" />
+<glyph unicode="+" horiz-adv-x="1128" d="M199 520v182h272v275h182v-275h273v-182h-273v-270h-182v270h-272z" />
+<glyph unicode="," horiz-adv-x="677" d="M152 -229l151 401h215l-182 -401h-184z" />
+<glyph unicode="-" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
+<glyph unicode="." horiz-adv-x="759" d="M260 0v227h240v-227h-240z" />
+<glyph unicode="/" horiz-adv-x="897" d="M-29 -201l752 1749h205l-754 -1749h-203z" />
+<glyph unicode="0" horiz-adv-x="1345" d="M133 677.5q0 700.5 538.5 700.5t538.5 -700.5t-538.5 -700.5t-538.5 700.5zM379 678q0 -156 24.5 -260.5t71.5 -152.5t92 -64.5t104.5 -16.5t104.5 16.5t92 64.5t72 152.5t25 260.5q0 125 -17.5 218t-45.5 144.5t-68.5 82t-77.5 39.5t-84 9t-84 -9t-78 -39.5t-68.5 -82 t-45 -144.5t-17.5 -218z" />
+<glyph unicode="1" horiz-adv-x="993" d="M211 936v262l266 168h223v-1366h-245v1085z" />
+<glyph unicode="2" horiz-adv-x="1226" d="M158 0v201q94 70 192.5 154.5t205 193t174 222.5t67.5 204q0 70 -56.5 117t-185.5 47q-100 0 -205.5 -34t-179.5 -85v244q195 120 392 120h3q242 0 358.5 -120.5t116.5 -288.5q0 -182 -137 -373.5t-336 -357.5h494v-244h-903z" />
+<glyph unicode="3" horiz-adv-x="1228" d="M166 84v250q180 -106 373 -107q290 0 290 188q0 91 -68.5 149.5t-230.5 58.5h-174v202h181q115 0 171 47.5t56 116.5q0 82 -73 116t-169 34q-178 0 -327 -90v251q178 84 342 84q205 0 339 -92t134 -295q0 -168 -168 -260q104 -37 167.5 -121t63.5 -204q0 -96 -32.5 -172 t-85 -123t-123 -78t-138 -43t-139.5 -12q-203 0 -389 100z" />
+<glyph unicode="4" horiz-adv-x="1234" d="M78 385v203l622 778h234v-762h178v-219h-178v-385h-246v385h-610zM365 604h323v420z" />
+<glyph unicode="5" horiz-adv-x="1245" d="M170 709l74 657h751v-244h-542q-4 -51 -12.5 -126.5t-12.5 -123.5q96 18 184 18q2 1 4 1q86 0 158 -19q74 -20 146.5 -67t113.5 -141.5t41 -227.5q0 -135 -41 -229t-110.5 -140.5t-143 -65.5t-162.5 -19q-227 0 -415 96v248q190 -121 401 -121q227 0 227 211v20 q-10 219 -223 223h-9q-174 0 -292 -73z" />
+<glyph unicode="6" horiz-adv-x="1222" d="M131 629q0 195 49 340t117 222t162 123t162.5 58t140.5 12q100 0 235 -38v-218q-82 27 -221 27q-47 0 -98 -12.5t-113.5 -42t-112 -96t-65.5 -160.5q90 69 250 69q221 0 342 -120.5t121 -331.5q0 -86 -26.5 -167t-81 -152.5t-152 -114.5t-224.5 -43q-485 0 -485 645z M378 585q0 -103 20 -179q22 -91 64 -132t78.5 -57t75.5 -16q76 0 128.5 31.5t73 80.5t27.5 84t7 66q0 104 -61.5 166.5t-174.5 62.5q-133 0 -237 -69q-1 -19 -1 -38z" />
+<glyph unicode="7" horiz-adv-x="1202" d="M141 1120v246h965v-195l-563 -1171h-291l545 1120h-656z" />
+<glyph unicode="8" horiz-adv-x="1236" d="M129 408q0 74 25.5 134t61.5 94t73 57.5t63 31.5l25 8q-8 2 -21.5 7t-48.5 26.5t-61.5 49.5t-48 81t-21.5 119q0 172 119 270t325.5 98t325.5 -98t119 -270q0 -188 -158 -266l-41 -17q10 -2 26.5 -8t58.5 -30.5t75 -58.5t59.5 -94.5t26.5 -133.5q0 -199 -129 -311.5 t-362.5 -112.5t-362.5 112.5t-129 311.5zM375 416q0 -111 61.5 -175.5t184 -64.5t184 63.5t61.5 176.5q0 82 -63.5 141t-182 59t-182 -59t-63.5 -141zM422 1010q0 -98 59.5 -140.5t139 -42.5t139 42t59.5 141q0 74 -51 124t-147.5 50t-147.5 -50.5t-51 -123.5z" />
+<glyph unicode="9" horiz-adv-x="1234" d="M117 907q0 86 26.5 167t81 152.5t151.5 114.5t224 43q485 0 485 -645q0 -195 -49 -340t-116.5 -222t-161.5 -123t-163 -58t-140 -12q-100 0 -236 39v217q82 -27 221 -27q47 0 98.5 12.5t114 42t111.5 96t65 160.5q-89 -69 -249 -69q-221 0 -342 120.5t-121 331.5z M365 905q0 -104 61 -166.5t174 -62.5q133 0 238 69q1 19 1 38q0 103 -19 179q-23 91 -65 132t-79 57t-76 16q-76 0 -128 -31.5t-72.5 -80.5t-27.5 -84t-7 -66z" />
+<glyph unicode=":" horiz-adv-x="813" d="M293 78v223h235v-223h-235zM293 723v223h235v-223h-235z" />
+<glyph unicode=";" horiz-adv-x="858" d="M182 -229l152 401h215l-182 -401h-185zM322 723v223h235v-223h-235z" />
+<glyph unicode="&#x3c;" horiz-adv-x="1351" d="M246 477v148l850 379v-211l-617 -242l617 -242v-211z" />
+<glyph unicode="=" horiz-adv-x="1306" d="M260 348v176h789v-176h-789zM260 698v176h789v-176h-789z" />
+<glyph unicode="&#x3e;" horiz-adv-x="1329" d="M270 98v211l617 242l-617 242v211l850 -379v-148z" />
+<glyph unicode="?" horiz-adv-x="1042" d="M164 1059v227q150 96 321 96q115 0 200 -33.5t132 -92t69.5 -126t22.5 -145.5q0 -70 -22.5 -126t-56 -92t-72.5 -74l-73 -73q-34 -34 -56.5 -87.5t-22.5 -118.5v-21h-217v27q0 104 31 184t76 127t89 86t74.5 81t30.5 89q0 182 -221 182q-180 0 -305 -110zM379 0v227h239 v-227h-239z" />
+<glyph unicode="@" horiz-adv-x="1869" d="M170 371q0 184 79 349t205 276.5t282.5 176t310.5 64.5q84 0 174 -22.5t183 -75t165.5 -127t119 -191t46.5 -258.5q0 -207 -92.5 -362.5t-266.5 -155.5q-82 0 -127 54.5t-51 97.5q-27 -66 -101.5 -109t-185.5 -43q-154 0 -242.5 69.5t-88.5 190.5q0 53 17 99.5t62.5 94.5 t142.5 76.5t238 28.5q45 0 115 -4q0 76 -34 114t-140 38q-166 0 -279 -43q4 29 13.5 82t13.5 81q158 51 270 52q166 0 252 -77t86 -226q0 -51 -16 -156t-16 -162q0 -133 92 -133q74 0 147.5 103.5t73.5 289.5q0 127 -53.5 234.5t-137.5 175t-185 105.5t-202 38 q-295 0 -522 -224t-227 -527q0 -129 46 -233.5t111.5 -163t145.5 -97.5t141.5 -52.5t106.5 -13.5q262 0 465 99l-21 -131q-188 -84 -442 -84q-106 0 -217 37.5t-216.5 113.5t-173 214.5t-67.5 316.5zM762 309q-1 -80 149 -80q119 0 165 50.5t54 175.5q-27 2 -90 2 q-279 0 -278 -148z" />
+<glyph unicode="A" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM516 551h408l-203 536z" />
+<glyph unicode="B" horiz-adv-x="1323" d="M178 0v1366h492q94 0 174 -19.5t149.5 -61.5t109.5 -120t40 -184q0 -180 -162 -264q238 -86 238 -316q0 -55 -9.5 -103t-42 -105.5t-85 -97.5t-145.5 -67.5t-218 -27.5h-541zM424 215h283q266 0 266 199q0 178 -275 178h-274v-377zM424 809h260q213 0 213 164 q0 180 -231 180h-242v-344z" />
+<glyph unicode="C" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q209 0 414 -81v-230q-195 90 -392 90q-92 0 -157.5 -16.5t-128 -63.5t-95 -147t-32.5 -252t32.5 -252t95 -147t128 -63.5t157.5 -16.5q197 0 392 90v-229q-205 -82 -414 -82q-96 0 -179 16t-170 63.5 t-148.5 123t-100.5 202.5t-39 295z" />
+<glyph unicode="D" horiz-adv-x="1419" d="M178 0v1366h471q317 0 481 -174t164 -510q0 -334 -163.5 -508t-481.5 -174h-471zM424 223h225q213 0 306.5 107.5t93.5 351.5q0 246 -93.5 353.5t-306.5 107.5h-225v-920z" />
+<glyph unicode="E" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909z" />
+<glyph unicode="F" horiz-adv-x="1142" d="M178 0v1366h883v-221h-637v-346h551v-232h-551v-567h-246z" />
+<glyph unicode="G" horiz-adv-x="1406" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q217 0 450 -92v-225q-221 98 -426 98q-72 0 -127 -9t-111 -39t-93 -81t-60.5 -140t-23.5 -210q0 -123 23.5 -212t60.5 -140t93 -81t111.5 -39t126.5 -9q113 0 242 35v327h-321v217h548v-682 q-238 -116 -489 -116h-4q-96 0 -179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294z" />
+<glyph unicode="H" horiz-adv-x="1429" d="M178 0v1366h246v-567h580v567h245v-1366h-245v567h-580v-567h-246z" />
+<glyph unicode="I" horiz-adv-x="600" d="M178 0v1366h246v-1366h-246z" />
+<glyph unicode="J" horiz-adv-x="1003" d="M59 72v243q125 -102 281 -102q115 0 169 48t54 188v917h246v-909q0 -145 -44 -245.5t-117 -147t-139.5 -63.5t-139.5 -17q-168 0 -310 88z" />
+<glyph unicode="K" horiz-adv-x="1415" d="M178 0v1366h246v-578l516 578h330l-551 -600l608 -766h-307l-459 592l-137 -150v-442h-246z" />
+<glyph unicode="L" horiz-adv-x="1124" d="M178 0v1366h246v-1133h657v-233h-903z" />
+<glyph unicode="M" horiz-adv-x="1695" d="M178 0v1366h273l395 -571l395 571h272v-1366h-245v1010l-422 -604l-422 604v-1010h-246z" />
+<glyph unicode="N" horiz-adv-x="1587" d="M178 0v1366h279l702 -1012v1012h246v-1366h-275l-706 1020v-1020h-246z" />
+<glyph unicode="O" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212z" />
+<glyph unicode="P" horiz-adv-x="1277" d="M176 0v1366h510q242 0 374 -120t132 -339t-132 -338.5t-374 -119.5h-264v-449h-246zM422 672h246q162 0 221 59.5t59 175.5q0 115 -59.5 173.5t-220.5 58.5h-246v-467z" />
+<glyph unicode="Q" horiz-adv-x="1529" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t171 63.5t180 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -152 -32 -269.5t-84 -193.5t-126 -127t-148.5 -74.5t-160.5 -31.5q0 -94 69.5 -136.5t165.5 -42.5v-188q-438 0 -438 369q-106 14 -193 53 t-165 115.5t-122 211t-44 314.5zM371 682q0 -154 30.5 -254t91 -148.5t122 -64.5t149.5 -16q86 0 148.5 16t123 64.5t91 148.5t30.5 254t-30.5 254t-91 148.5t-123 64.5t-148.5 16q-88 0 -149.5 -16t-122 -64.5t-91 -148.5t-30.5 -254z" />
+<glyph unicode="R" horiz-adv-x="1337" d="M178 0v1366h524q240 0 373 -113.5t133 -324.5q0 -314 -299 -406l359 -522h-297l-326 481h-221v-481h-246zM424 707h272q125 0 196 45t71 170q0 127 -71 171t-196 44h-272v-430z" />
+<glyph unicode="S" horiz-adv-x="1189" d="M125 985q0 197 139 297t348 100q184 0 367 -94v-239q-158 104 -338 104q-113 0 -191.5 -39t-78.5 -123q0 -68 71.5 -106.5t173 -65t204 -66.5t174 -138.5t71.5 -254.5q0 -166 -137 -271t-334 -105q-260 0 -451 116v250q176 -131 394 -131q282 0 282 157q0 71 -51 118 t-129 69.5t-167 53t-167 66.5t-129 112.5t-51 189.5z" />
+<glyph unicode="T" horiz-adv-x="1169" d="M25 1120v246h1120v-246h-438v-1120h-246v1120h-436z" />
+<glyph unicode="U" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5z" />
+<glyph unicode="V" horiz-adv-x="1347" d="M25 1366h262l387 -1061l387 1061h262l-522 -1366h-252z" />
+<glyph unicode="W" horiz-adv-x="2045" d="M25 1366h258l307 -1010l325 1010h215l326 -1010l309 1010h256l-438 -1366h-254l-305 952l-307 -952h-254z" />
+<glyph unicode="X" horiz-adv-x="1329" d="M25 0l485 690l-473 676h299l326 -461l323 461h289l-469 -666l491 -700h-299l-344 487l-344 -487h-284z" />
+<glyph unicode="Y" horiz-adv-x="1368" d="M25 1366h297l362 -555l360 555h297l-534 -780v-586h-246v586z" />
+<glyph unicode="Z" horiz-adv-x="1271" d="M131 0v225l684 895h-676v246h981v-219l-688 -901h688v-246h-989z" />
+<glyph unicode="[" horiz-adv-x="808" d="M244 -238v1663h395v-190h-207v-1282h207v-191h-395z" />
+<glyph unicode="\" horiz-adv-x="829" d="M-76 1548h205l752 -1749h-203z" />
+<glyph unicode="]" horiz-adv-x="808" d="M172 -47h207v1282h-207v190h395v-1663h-395v191z" />
+<glyph unicode="^" horiz-adv-x="1146" d="M264 969l223 444h113l225 -444h-170l-110 260l-111 -260h-170z" />
+<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-141h-1118v141z" />
+<glyph unicode="`" horiz-adv-x="522" d="M92 1395h201l127 -240h-137z" />
+<glyph unicode="a" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM358 365q0 -164 193 -164h2 q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
+<glyph unicode="b" horiz-adv-x="1181" d="M170 328v1085h246v-391q88 16 168 16q229 0 352 -133t123 -383q0 -137 -36 -241.5t-85 -158.5t-117.5 -88t-119 -42t-101.5 -8q-219 0 -324.5 101t-105.5 243zM416 305q0 -110 184 -110q117 0 164 79.5t47 260.5q0 172 -60.5 240.5t-175.5 68.5q-90 0 -159 -17v-522z" />
+<glyph unicode="c" horiz-adv-x="966" d="M119 510q0 256 125 392t358 136q150 0 295 -57v-205q-121 49 -254 49q-141 0 -209.5 -64.5t-68.5 -250.5q0 -184 68.5 -249.5t209.5 -65.5q127 0 254 51v-205q-145 -57 -295 -57q-233 0 -358 136t-125 390z" />
+<glyph unicode="d" horiz-adv-x="1167" d="M129 522q0 250 122 383t351 133q82 0 170 -16v391h246v-1085q0 -141 -106.5 -242.5t-325.5 -101.5q-51 0 -101.5 8t-118 42t-117.5 88t-85 158.5t-35 241.5zM375 535q0 -180 47 -260t164 -80q186 0 186 110v522q-72 16 -162 17q-115 0 -175 -68.5t-60 -240.5z" />
+<glyph unicode="e" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM381 588h412q-10 106 -62.5 179t-144.5 73 t-143.5 -59.5t-61.5 -192.5z" />
+<glyph unicode="f" horiz-adv-x="731" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174z" />
+<glyph unicode="g" horiz-adv-x="1122" d="M123 514q0 135 35 235.5t83 153.5t113.5 86t114.5 41t98 8q215 0 316.5 -98t101.5 -238v-727q0 -49 -9 -93t-38 -98t-77 -92t-135 -64.5t-202 -26.5q-162 0 -295 41v219q123 -41 267 -41q86 0 140 13t73.5 41t24.5 47.5t5 53.5v56q-82 -16 -155 -17q-223 0 -342 129 t-119 371zM369 510q0 -172 55 -233.5t168 -61.5q75 0 147 14v476q0 114 -169 114h-3q-98 0 -148 -71.5t-50 -237.5z" />
+<glyph unicode="h" horiz-adv-x="1228" d="M193 0v1411h245v-407q88 34 205 34q213 0 333 -111.5t120 -312.5v-614h-244v635q0 41 -7 68.5t-30.5 57.5t-77 45t-135.5 15q-96 0 -164 -30v-791h-245z" />
+<glyph unicode="i" horiz-adv-x="638" d="M193 1161v225h251v-225h-251zM197 0v1022h245v-1022h-245z" />
+<glyph unicode="j" horiz-adv-x="593" d="M6 -205q70 4 112 17.5t57 42t18.5 49t3.5 65.5v1053h245v-1036q0 -66 -7 -113t-33.5 -99.5t-72.5 -85t-128 -56t-195 -27.5v190zM193 1161v225h251v-225h-251z" />
+<glyph unicode="k" horiz-adv-x="1226" d="M188 0v1405h246v-758l344 375h326l-445 -455l492 -567h-311l-338 403l-68 -69v-334h-246z" />
+<glyph unicode="l" horiz-adv-x="659" d="M207 0v1411h246v-1411h-246z" />
+<glyph unicode="m" horiz-adv-x="1908" d="M193 0v924q227 114 446 114h4q166 0 277 -75q190 75 368 75q207 0 322.5 -112.5t115.5 -311.5v-614h-245v631q0 98 -49.5 148t-184.5 50q-106 0 -209 -24q43 -49 43 -191v-614h-243v637q0 94 -48.5 141t-181.5 47q-88 0 -170 -16v-809h-245z" />
+<glyph unicode="n" horiz-adv-x="1273" d="M168 0v926q225 112 469 112q213 0 333 -111.5t120 -312.5v-614h-244v635q0 41 -7.5 68.5t-30 57.5t-73.5 45t-131 15q-90 0 -190 -18v-803h-246z" />
+<glyph unicode="o" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5z" />
+<glyph unicode="p" horiz-adv-x="1175" d="M168 -379v1071q0 141 105.5 242.5t324.5 101.5q51 0 101.5 -8t119 -42t117.5 -88t85 -158.5t36 -241.5q0 -250 -123 -384t-352 -134q-86 0 -168 18v-377h-246zM414 182q85 -12 151 -12q119 0 181.5 69.5t62.5 245.5q0 180 -47 260t-164 80q-184 0 -184 -110v-533z" />
+<glyph unicode="q" horiz-adv-x="1171" d="M115 498q0 137 34.5 241.5t85 158.5t118 88t117.5 42t101 8q219 0 326 -101t107 -243v-1071h-246v377q-82 -18 -170 -18q-229 0 -351 134t-122 384zM360 485q0 -176 62.5 -245.5t181.5 -69.5q68 0 154 12v533q0 110 -184 110h-3q-117 0 -164 -80t-47 -260z" />
+<glyph unicode="r" horiz-adv-x="741" d="M166 0v930q25 12 69 28.5t177 47t264 32.5v-213q-154 -2 -264 -34v-791h-246z" />
+<glyph unicode="s" horiz-adv-x="923" d="M100 719q0 154 108.5 236.5t270.5 82.5q152 0 279 -73v-187q-104 49 -236 49q-176 0 -176 -86q0 -43 50 -67.5t122 -43t143.5 -50t122 -106.5t50.5 -191q0 -129 -104.5 -214t-276.5 -85q-176 0 -324 84v219q135 -90 289 -90q106 0 138 24.5t32 73.5q0 45 -50.5 72.5 t-122 48t-143 48.5t-122 92.5t-50.5 162.5z" />
+<glyph unicode="t" horiz-adv-x="794" d="M49 811v211h172v303h246v-303h242v-211h-242v-504q0 -66 27.5 -89t97.5 -23q66 0 117 22v-203q-72 -30 -157 -30h-5q-152 0 -239 90t-87 248v489h-172z" />
+<glyph unicode="u" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352z" />
+<glyph unicode="v" horiz-adv-x="1169" d="M66 1022h262l256 -772l256 772h264l-391 -1022h-256z" />
+<glyph unicode="w" horiz-adv-x="1710" d="M78 1022h260l217 -737l195 737h211l194 -737l217 737h260l-362 -1022h-246l-168 588l-170 -588h-246z" />
+<glyph unicode="x" horiz-adv-x="1142" d="M57 0l363 516l-354 506h301l204 -291l205 291h273l-342 -483l378 -539h-301l-229 326l-231 -326h-267z" />
+<glyph unicode="y" horiz-adv-x="1107" d="M63 1022h261l229 -713l225 713h260l-389 -1047q-82 -223 -178 -300.5t-199 -77.5q-76 0 -155 28v205q59 -25 116 -25q106 0 197 248z" />
+<glyph unicode="z" horiz-adv-x="999" d="M92 0v229l471 570h-461v223h777v-227l-478 -572h478v-223h-787z" />
+<glyph unicode="{" horiz-adv-x="784" d="M186 506v194q12 2 28.5 7.5t45.5 34t29 71.5v293q0 145 81 235.5t242 90.5h41v-185h-24q-43 0 -70 -11t-37 -34.5t-13 -41t-3 -48.5v-289q0 -86 -52.5 -142t-117.5 -79q66 -20 118 -76.5t52 -142.5v-289q0 -31 3 -48t13 -40.5t37 -35t70 -11.5h24v-184h-41 q-162 0 -242.5 90t-80.5 235v293q0 41 -25.5 70t-50.5 37z" />
+<glyph unicode="|" horiz-adv-x="954" d="M379 -203v1569h219v-1569h-219z" />
+<glyph unicode="}" horiz-adv-x="784" d="M156 -41h22q43 0 69.5 11.5t37 35t13.5 40.5t3 48v289q0 86 52.5 142.5t117.5 76.5q-66 23 -118 79t-52 142v289q0 31 -3 48.5t-13.5 41t-37 34.5t-69.5 11h-22v185h39q164 0 244.5 -89.5t80.5 -236.5v-293q0 -43 25.5 -70.5t52.5 -33.5l25 -9v-194q-4 0 -11.5 -2 t-25 -10.5t-30.5 -19.5t-24.5 -32.5t-11.5 -48.5v-293q0 -147 -81 -236t-244 -89h-39v184z" />
+<glyph unicode="~" horiz-adv-x="1071" d="M201 457v168q57 68 129 74q11 1 23 1q59 0 116 -26l135 -60q57 -25 115 -25q12 0 23 1q71 6 128 74v-168q-57 -68 -128 -74q-11 -1 -23 -1q-58 0 -115 25l-135 60q-56 26 -115 26q-12 0 -24 -2q-72 -6 -129 -73z" />
+<glyph unicode="&#xa1;" horiz-adv-x="825" d="M283 -344l45 973h190l45 -973h-280zM303 795v227h240v-227h-240z" />
+<glyph unicode="&#xa2;" horiz-adv-x="976" d="M117 682q0 246 115.5 382t334.5 146v156h119v-160q98 -10 209 -53v-205q-98 41 -209 47v-626q104 6 209 49v-205q-111 -43 -209 -53v-160h-119v156q-219 10 -334.5 145t-115.5 381zM362 682q0 -158 51.5 -228.5t153.5 -82.5v622q-102 -12 -153.5 -81.5t-51.5 -229.5z" />
+<glyph unicode="&#xa3;" horiz-adv-x="1153" d="M100 -31v209q51 39 174 68v319h-174v195h174v235q0 160 105.5 273.5t327.5 113.5q168 0 331 -94v-211q-149 113 -313 113q-117 0 -178.5 -56.5t-61.5 -152.5v-221h377v-195h-377v-325q6 -2 75 -20.5t85.5 -21.5t74.5 -14q49 -10 76 -10h8q27 2 74.5 4t85 15.5t74.5 35.5 v-215q-86 -44 -182 -44h-4q-98 1 -186 25.5t-178 46.5q-54 13 -112 13q-38 0 -78 -6q-101 -14 -198 -80z" />
+<glyph unicode="&#xa5;" horiz-adv-x="1396" d="M37 1366h297l362 -555l361 555h297l-410 -598h162v-184h-287v-160h287v-184h-287v-240h-246v240h-288v184h288v160h-288v184h161z" />
+<glyph unicode="&#xa7;" horiz-adv-x="1247" d="M137 709q0 98 79 152t175 59q-47 20 -78.5 69t-31.5 103q0 106 67.5 174.5t158.5 93t204 24.5q184 0 344 -92v-211q-164 78 -314 78q-74 0 -135 -21.5t-61 -72.5q0 -27 42 -50.5t105.5 -41t136 -46t136 -61t105.5 -90t42 -131.5q0 -86 -65.5 -137t-161.5 -62 q84 -51 84 -167q0 -133 -117 -214t-315 -81q-211 0 -394 120v230q164 -133 369 -133q82 0 141.5 22.5t59.5 69.5q0 35 -59.5 60.5t-143.5 50t-169 56.5t-144.5 95t-59.5 154zM401 719q0 -43 68 -75t254 -93q61 6 93 33.5t32 54.5q0 37 -55.5 66.5t-147.5 57.5t-106 34 q-45 -4 -91.5 -24.5t-46.5 -53.5z" />
+<glyph unicode="&#xa8;" horiz-adv-x="856" d="M162 1155v195h207v-195h-207zM489 1155v195h207v-195h-207z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -205 -493.5t-495 -204.5q-289 0 -494 204.5t-205 493.5zM203 682q0 -246 174 -420t420 -174q248 0 422 174t174 420q0 248 -174.5 422t-421.5 174q-246 0 -420 -174t-174 -422zM440 682 q0 121 33 206t90.5 127t116.5 59.5t131 17.5q123 0 242 -48v-135q-117 53 -229 53q-1 1 -2 1q-112 0 -174 -57q-64 -56 -64 -224t63 -223.5t176 -55.5q119 0 230 54v-135q-119 -47 -242 -48q-72 0 -131 17.5t-116.5 58.5t-90.5 126t-33 206z" />
+<glyph unicode="&#xaa;" horiz-adv-x="808" d="M154 989q0 25 7 48.5t28.5 57t74.5 54t133 20.5q39 0 101 -8q0 106 -121 107q-98 0 -166 -29v111q78 32 174 32q121 0 186.5 -62.5t65.5 -178.5v-154q0 -80 -58.5 -136t-181.5 -56q-125 0 -184 57t-59 137zM289 1008q0 -92 106 -93q105 1 105 89v71q-74 6 -82 6 q-129 0 -129 -73z" />
+<glyph unicode="&#xab;" horiz-adv-x="1247" d="M66 522l346 389h286l-372 -389l372 -391h-286zM522 522l346 389h287l-373 -389l373 -391h-287z" />
+<glyph unicode="&#xac;" horiz-adv-x="1273" d="M199 492v192h876v-389h-176v197h-700z" />
+<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
+<glyph unicode="&#xae;" horiz-adv-x="1593" d="M100 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -494 204.5t-205 493.5zM205 682q0 -246 174 -420t420 -174q248 0 422 174t174 420q0 248 -174 422t-422 174q-246 0 -420 -174t-174 -422zM528 285v796 h306q139 0 216.5 -66.5t77.5 -189.5q0 -184 -174 -235l209 -305h-174l-188 280h-129v-280h-144zM672 696h157q72 0 114 26t42 99.5t-42 100t-114 26.5h-157v-252z" />
+<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1180v145h479v-145h-479z" />
+<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1143q0 111 57.5 169t166 58t165.5 -58.5t57 -168.5q0 -109 -57 -167t-165.5 -58t-166 58t-57.5 167zM276 1145q0 -68 28 -97.5t87.5 -29.5t86 29.5t26.5 97.5q0 66 -26.5 95.5t-86 29.5t-87.5 -30t-28 -95z" />
+<glyph unicode="&#xb1;" horiz-adv-x="1198" d="M242 61v166h727v-166h-727zM242 604v182h272v275h182v-275h273v-182h-273v-270h-182v270h-272z" />
+<glyph unicode="&#xb2;" horiz-adv-x="696" d="M109 948v109q125 92 234.5 209.5t109.5 208.5q0 88 -129 88q-119 0 -209 -66v133q106 63 213 64q131 0 193.5 -64.5t62.5 -154.5q0 -180 -254 -396h266v-131h-487z" />
+<glyph unicode="&#xb3;" horiz-adv-x="686" d="M111 993v135q96 -57 200 -57q158 0 158 100q0 113 -162 113h-94v109h98q61 0 91 25.5t30 62.5q0 80 -129 80q-100 0 -176 -48v136q98 45 184 45q109 0 181.5 -49.5t72.5 -159.5q0 -90 -90 -139q125 -43 125 -177q0 -121 -85 -175t-196 -54q-109 0 -208 53z" />
+<glyph unicode="&#xb4;" horiz-adv-x="528" d="M121 1155l127 240h201l-191 -240h-137z" />
+<glyph unicode="&#xb5;" horiz-adv-x="1323" d="M203 -430v1452h243v-635q0 -41 7.5 -68.5t30 -57.5t73.5 -45t131 -15q90 0 191 18v803h245v-926q-223 -112 -465 -112h-4q-127 0 -209 51v-465h-243z" />
+<glyph unicode="&#xb6;" horiz-adv-x="911" d="M78 946q0 203 121 311.5t350 108.5h299v-1366h-232v524h-67q-229 0 -350 108.5t-121 313.5z" />
+<glyph unicode="&#xb7;" horiz-adv-x="663" d="M211 444v228h240v-228h-240z" />
+<glyph unicode="&#xb8;" horiz-adv-x="563" d="M119 -256l178 246h160l-127 -246h-211z" />
+<glyph unicode="&#xb9;" horiz-adv-x="512" d="M109 1452v141l143 90h121v-735h-133v586z" />
+<glyph unicode="&#xba;" horiz-adv-x="854" d="M152 1092q0 141 69.5 216.5t200.5 75.5t200.5 -75.5t69.5 -216.5q0 -143 -69.5 -219t-200.5 -76t-200.5 75.5t-69.5 219.5zM289 1092q0 -106 32.5 -144.5t100.5 -38.5q66 0 99.5 38t33.5 145q0 104 -34 142t-99 38q-68 0 -100.5 -38t-32.5 -142z" />
+<glyph unicode="&#xbb;" horiz-adv-x="1277" d="M106 131l373 389l-373 391h287l346 -391l-346 -389h-287zM563 131l373 389l-373 391h287l346 -391l-346 -389h-287z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1476" d="M109 1149v141l143 90h121v-735h-133v586zM168 -45l895 1448h164l-895 -1448h-164zM825 209v108l336 418h125v-409h96v-117h-96v-209h-131v209h-330zM981 326h174v227z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1527" d="M109 1149v141l143 90h121v-735h-133v586zM168 -45l895 1448h164l-895 -1448h-164zM936 0v109q125 92 234.5 209.5t109.5 207.5q0 88 -129 88q-119 0 -209 -65v133q106 63 213 63q131 0 193.5 -64.5t62.5 -154.5q0 -180 -254 -395h266v-131h-487z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1691" d="M135 688v135q96 -57 201 -57q158 0 158 100q0 113 -162 113h-94v108h98q61 0 91 26t30 63q0 79 -126 79h-3q-100 0 -176 -47v135q98 45 184 46q109 0 181.5 -49.5t72.5 -159.5q0 -91 -90 -140q125 -43 125 -176q0 -121 -85 -175t-196 -54q-109 0 -209 53zM369 -45 l895 1448h163l-895 -1448h-163zM1032 209v108l336 418h125v-409h96v-117h-96v-209h-131v209h-330zM1188 326h174v227z" />
+<glyph unicode="&#xbf;" horiz-adv-x="1042" d="M156 37q0 70 22.5 126t56 92t72.5 74l73 73q34 34 56.5 87.5t22.5 118.5v21h217v-27q0 -104 -31 -184t-76 -127t-89 -86t-74.5 -81t-30.5 -89q0 -182 221 -182q180 0 305 110v-227q-150 -96 -321 -96q-115 0 -200 33.5t-132 92t-69.5 126t-22.5 145.5zM446 795v227h240 v-227h-240z" />
+<glyph unicode="&#xc0;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM459 1722h200l127 -239h-137zM516 551h408l-203 536z" />
+<glyph unicode="&#xc1;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM516 551h408l-203 536zM655 1483l127 239h201l-190 -239h-138z" />
+<glyph unicode="&#xc2;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM485 1483l187 250h98l188 -250h-163l-74 114l-74 -114h-162zM516 551h408l-203 536z" />
+<glyph unicode="&#xc3;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM410 1481v155q57 68 124 74q10 1 21 1q55 0 105 -25l122 -61q51 -25 104 -25q12 0 24 2q65 6 122 73v-155q-57 -68 -122 -74q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -104 26q-11 0 -22 -2 q-67 -6 -124 -73zM516 551h408l-203 536z" />
+<glyph unicode="&#xc4;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM455 1483v194h207v-194h-207zM516 551h408l-203 536zM782 1483v194h207v-194h-207z" />
+<glyph unicode="&#xc5;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM516 551h408l-203 536zM543 1599.5q0 83.5 47 130t131 46.5q86 0 132 -46.5t46 -130t-46 -130t-132 -46.5q-84 0 -131 46.5t-47 130zM651 1599.5q0 -49.5 15.5 -69t54.5 -19.5t54.5 19.5t15.5 69 t-15.5 69t-54.5 19.5t-54.5 -19.5t-15.5 -69z" />
+<glyph unicode="&#xc6;" horiz-adv-x="2039" d="M51 0l864 1366h1002v-221h-664v-346h576v-232h-576v-346h664v-221h-909v307h-478l-188 -307h-291zM655 512h353v578z" />
+<glyph unicode="&#xc7;" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q211 0 412 -81v-230q-191 90 -390 90q-92 0 -157.5 -16.5t-128 -63.5t-95 -147t-32.5 -252t32.5 -252t95 -147t128 -63.5t157.5 -16.5q199 0 390 90v-229q-170 -72 -349 -80l-123 -242h-213l177 244 q-109 12 -198 50t-171 113.5t-127 212t-45 320.5z" />
+<glyph unicode="&#xc8;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM397 1722h201l127 -239h-137z" />
+<glyph unicode="&#xc9;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM555 1483l127 239h201l-191 -239h-137z" />
+<glyph unicode="&#xca;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM414 1483l186 250h98l189 -250h-164l-74 114l-74 -114h-161z" />
+<glyph unicode="&#xcb;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM389 1483v194h207v-194h-207zM717 1483v194h207v-194h-207z" />
+<glyph unicode="&#xcc;" horiz-adv-x="600" d="M35 1722h201l126 -239h-137zM178 0h246v1366h-246v-1366z" />
+<glyph unicode="&#xcd;" horiz-adv-x="600" d="M178 0h246v1366h-246v-1366zM240 1483l127 239h200l-190 -239h-137z" />
+<glyph unicode="&#xce;" horiz-adv-x="600" d="M63 1483l187 250h98l189 -250h-164l-74 114l-74 -114h-162zM178 0h246v1366h-246v-1366z" />
+<glyph unicode="&#xcf;" horiz-adv-x="600" d="M33 1483v194h207v-194h-207zM178 0h246v1366h-246v-1366zM360 1483v194h207v-194h-207z" />
+<glyph unicode="&#xd0;" horiz-adv-x="1509" d="M106 598v168h162v600h471q317 0 481 -174t164 -510q0 -334 -163.5 -508t-481.5 -174h-471v598h-162zM514 223h225q213 0 306.5 107.5t93.5 351.5q0 246 -93.5 353.5t-306.5 107.5h-225v-377h242v-168h-242v-375z" />
+<glyph unicode="&#xd1;" horiz-adv-x="1587" d="M178 0v1366h279l702 -1012v1012h246v-1366h-275l-706 1020v-1020h-246zM481 1481v155q57 68 124 74q11 1 22 1q55 0 105 -25l122 -61q50 -25 104 -25q11 0 23 2q66 6 123 73v-155q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -104 26q-11 0 -22 -2 q-67 -6 -125 -73z" />
+<glyph unicode="&#xd2;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM518 1722h201l127 -239h-137z" />
+<glyph unicode="&#xd3;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM680 1483l127 239h201l-191 -239h-137z" />
+<glyph unicode="&#xd4;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM526 1483l187 250h98l188 -250h-163l-74 114l-74 -114h-162z" />
+<glyph unicode="&#xd5;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM451 1481v155q57 68 123 74q11 1 22 1q55 0 106 -25l122 -61q50 -25 103 -25 q12 0 23 2q66 6 123 73v-155q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2q-66 -6 -123 -73z" />
+<glyph unicode="&#xd6;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM496 1483v194h206v-194h-206zM823 1483v194h207v-194h-207z" />
+<glyph unicode="&#xd7;" horiz-adv-x="1062" d="M156 160l282 364l-282 365h184l190 -246l193 246h182l-280 -365l280 -364h-182l-193 243l-190 -243h-184z" />
+<glyph unicode="&#xd8;" horiz-adv-x="1529" d="M68 0l178 211q-121 174 -121 471q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q233 0 385 -102l74 86h237l-180 -213q123 -178 123 -471q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16q-233 0 -387 100l-70 -84h-237zM371 684q0 -168 39 -278 l594 702q-84 57 -242 57q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM520 256q84 -57 242 -57q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 162 -41 274z" />
+<glyph unicode="&#xd9;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM506 1722h201l127 -239h-138z" />
+<glyph unicode="&#xda;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM668 1483l127 239h200l-190 -239h-137z" />
+<glyph unicode="&#xdb;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM514 1483l186 250h99l188 -250h-164l-73 114l-74 -114h-162z" />
+<glyph unicode="&#xdc;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM483 1483v194h207v-194h-207zM811 1483v194h207v-194h-207z" />
+<glyph unicode="&#xdd;" horiz-adv-x="1368" d="M25 1366h297l362 -555l360 555h297l-534 -780v-586h-246v586zM594 1483l127 239h201l-191 -239h-137z" />
+<glyph unicode="&#xde;" horiz-adv-x="1253" d="M176 0v1366h246v-172h209q242 0 374 -120t132 -339t-132.5 -339t-373.5 -120h-209v-276h-246zM422 500h190q162 0 221.5 59t59.5 176q0 115 -59.5 173.5t-221.5 58.5h-190v-467z" />
+<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M180 0v856q0 156 40 265.5t108.5 162.5t140.5 75.5t156 22.5q223 0 344 -113t121 -265q0 -109 -63.5 -187t-133.5 -104q37 -10 70.5 -27.5t82 -51.5t77 -93.5t28.5 -134.5q0 -201 -160 -310q-144 -97 -364 -97q-24 0 -49 1v199h12q334 0 334 215q0 193 -346 204v181 q66 0 124 11t113 58t55 127q0 213 -219 213q-51 0 -89 -13t-75.5 -48t-58 -109.5t-20.5 -187.5v-850h-228z" />
+<glyph unicode="&#xe0;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM313 1395h201l127 -240h-137z M358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
+<glyph unicode="&#xe1;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM358 365q0 -164 193 -164h2 q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM459 1155l127 240h200l-190 -240h-137z" />
+<glyph unicode="&#xe2;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM338 1155l186 250h99l188 -250h-164 l-74 115l-73 -115h-162zM358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
+<glyph unicode="&#xe3;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM258 1153v156q57 68 124 74 q11 1 22 1q55 0 105 -26l121 -60q52 -25 105 -25q11 0 23 1q65 6 123 74v-156q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -106 25l-121 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74zM358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
+<glyph unicode="&#xe4;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM287 1155v195h207v-195h-207z M358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM614 1155v195h207v-195h-207z" />
+<glyph unicode="&#xe5;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM358 365q0 -164 193 -164h2 q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM389 1272q0 84 47 130t131 46q86 0 132 -46t46 -130t-46 -130t-132 -46q-84 0 -131 46t-47 130zM498 1272q0 -49 15 -68.5t54 -19.5t54.5 19.5t15.5 68.5t-15.5 68.5t-54.5 19.5t-54 -19.5t-15 -68.5z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1792" d="M109 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q236 0 338 -143q35 57 121 101t199 44q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198 q-170 -92 -385 -92q-104 0 -193 43t-135 102q-106 -145 -327 -145q-225 0 -331.5 102t-106.5 248zM352 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM979 588h412q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
+<glyph unicode="&#xe7;" horiz-adv-x="966" d="M119 510q0 256 125 392t358 136q150 0 295 -57v-205q-121 49 -254 49q-141 0 -209.5 -64.5t-68.5 -250.5q0 -184 68.5 -249.5t209.5 -65.5q127 0 254 51v-205q-106 -45 -227 -53l-123 -244h-213l176 246q-190 25 -290.5 158t-100.5 362z" />
+<glyph unicode="&#xe8;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM334 1395h201l127 -240h-138zM381 588h412 q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
+<glyph unicode="&#xe9;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM381 588h412q-10 106 -62.5 179t-144.5 73 t-143.5 -59.5t-61.5 -192.5zM496 1155l127 240h200l-190 -240h-137z" />
+<glyph unicode="&#xea;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM346 1155l186 250h99l188 -250h-164l-73 115l-74 -115 h-162zM381 588h412q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
+<glyph unicode="&#xeb;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM309 1155v195h207v-195h-207zM381 588h412 q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5zM637 1155v195h207v-195h-207z" />
+<glyph unicode="&#xec;" horiz-adv-x="638" d="M63 1395h201l127 -240h-137zM197 0h245v1022h-245v-1022z" />
+<glyph unicode="&#xed;" horiz-adv-x="638" d="M197 0h245v1022h-245v-1022zM248 1155l127 240h200l-190 -240h-137z" />
+<glyph unicode="&#xee;" horiz-adv-x="638" d="M84 1155l186 250h99l188 -250h-164l-74 115l-73 -115h-162zM197 0h245v1022h-245v-1022z" />
+<glyph unicode="&#xef;" horiz-adv-x="638" d="M53 1155v195h207v-195h-207zM197 0h245v1022h-245v-1022zM381 1155v195h207v-195h-207z" />
+<glyph unicode="&#xf0;" horiz-adv-x="1232" d="M117 461q0 211 120.5 331.5t342.5 120.5q160 0 249 -69q-27 152 -145 237l-180 -123l-78 113l104 74q-41 10 -90 10q-82 0 -145 -10v219q92 20 160 20q178 0 315 -75l162 112l78 -114l-123 -84q199 -195 198 -594q0 -645 -485 -645q-127 0 -224 43t-151.5 114.5 t-81 152.5t-26.5 167zM365 463q0 -31 7 -66t27.5 -84t72.5 -80.5t128 -31.5q39 0 76 16t79 57t65 132q19 76 19 179q0 19 -1 38q-103 69 -234 69h-4q-113 0 -174 -62.5t-61 -166.5z" />
+<glyph unicode="&#xf1;" horiz-adv-x="1273" d="M168 0v926q225 112 469 112q213 0 333 -111.5t120 -312.5v-614h-244v635q0 41 -7.5 68.5t-30 57.5t-73.5 45t-131 15q-90 0 -190 -18v-803h-246zM326 1153v156q57 68 124 74q10 1 21 1q55 0 105 -26l122 -60q52 -25 105 -25q11 0 23 1q65 6 122 74v-156q-57 -68 -122 -74 q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74z" />
+<glyph unicode="&#xf2;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM326 1395h200l127 -240h-137zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5z" />
+<glyph unicode="&#xf3;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5zM514 1155l127 240h201l-191 -240h-137z" />
+<glyph unicode="&#xf4;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5zM348 1155l187 250h98l188 -250h-164l-73 115l-74 -115h-162 z" />
+<glyph unicode="&#xf5;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM272 1153v156q57 68 124 74q11 1 22 1q55 0 106 -26l122 -60q51 -25 104 -25q11 0 22 1q66 6 123 74v-156q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60 q-51 26 -106 26q-11 0 -22 -1q-66 -6 -124 -74zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5z" />
+<glyph unicode="&#xf6;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM317 1155v195h207v-195h-207zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5zM645 1155v195h207v-195h-207z " />
+<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M211 446v164h866v-164h-866zM541 129v193h207v-193h-207zM541 737v193h207v-193h-207z" />
+<glyph unicode="&#xf8;" horiz-adv-x="1175" d="M39 0l147 176q-82 133 -82 334q0 254 125 390t359 136q164 0 270 -63l43 49h238l-150 -176q82 -127 82 -336q0 -254 -125 -390t-358 -136q-158 0 -271 65l-41 -49h-237zM350 510q0 -68 10 -129l361 426q-47 27 -133 27q-121 0 -179.5 -67t-58.5 -257zM457 215 q51 -29 131 -29q121 0 179 67t58 257q0 82 -8 131z" />
+<glyph unicode="&#xf9;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM381 1395h201l127 -240h-138z" />
+<glyph unicode="&#xfa;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM551 1155l127 240h201l-191 -240h-137z" />
+<glyph unicode="&#xfb;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM393 1155l187 250h98l188 -250h-164l-73 115l-74 -115h-162z" />
+<glyph unicode="&#xfc;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM362 1155v195h207v-195h-207zM690 1155v195h207v-195h-207z" />
+<glyph unicode="&#xfd;" horiz-adv-x="1107" d="M63 1022h261l229 -713l225 713h260l-389 -1047q-82 -223 -178 -300.5t-199 -77.5q-76 0 -155 28v205q59 -25 116 -25q106 0 197 248zM473 1155l127 240h201l-191 -240h-137z" />
+<glyph unicode="&#xfe;" horiz-adv-x="1157" d="M170 -377v1778h246v-389q88 16 168 16q229 0 352 -133t123 -383t-123 -383t-352 -133q-80 0 -168 16v-389h-246zM416 207q69 -17 159 -17q115 0 175.5 71t60.5 251t-60.5 251t-175.5 71q-90 0 -159 -17v-610z" />
+<glyph unicode="&#xff;" horiz-adv-x="1107" d="M63 1022h261l229 -713l225 713h260l-389 -1047q-82 -223 -178 -300.5t-199 -77.5q-76 0 -155 28v205q59 -25 116 -25q106 0 197 248zM285 1155v195h207v-195h-207zM612 1155v195h207v-195h-207z" />
+<glyph unicode="&#x152;" horiz-adv-x="2074" d="M123 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q147 0 270 -43v27h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909v27q-115 -43 -270 -43q-96 0 -179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM369 684q0 -154 33.5 -255t98 -148.5 t131.5 -64.5t161 -17q145 0 237 37v884q-111 45 -237 45q-94 0 -161 -16t-131.5 -63.5t-98 -148.5t-33.5 -253z" />
+<glyph unicode="&#x153;" horiz-adv-x="1853" d="M98 512q0 254 125 390t359 136q229 0 356 -133q119 135 338 135q225 0 341 -143t116 -352q0 -86 -4 -127h-658q10 -133 80 -178t180 -45q180 0 348 79v-198q-170 -92 -385 -92q-233 0 -356 135q-123 -133 -356.5 -133t-358.5 136t-125 390zM344 512q0 -190 58.5 -257 t179 -67t179 67t58.5 257t-58.5 257t-179 67t-179 -67t-58.5 -257zM1071 588h412q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
+<glyph unicode="&#x178;" horiz-adv-x="1368" d="M25 1366h297l362 -555l360 555h297l-534 -780v-586h-246v586zM418 1483v194h207v-194h-207zM745 1483v194h207v-194h-207z" />
+<glyph unicode="&#x2c6;" horiz-adv-x="874" d="M201 1155l186 250h98l189 -250h-164l-74 115l-74 -115h-161z" />
+<glyph unicode="&#x2dc;" horiz-adv-x="1058" d="M215 1153v156q57 68 124 74q11 1 22 1q55 0 105 -26l122 -60q51 -25 104 -25q11 0 22 1q66 6 124 74v-156q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74z" />
+<glyph unicode="&#x2000;" horiz-adv-x="888" />
+<glyph unicode="&#x2001;" horiz-adv-x="1776" />
+<glyph unicode="&#x2002;" horiz-adv-x="888" />
+<glyph unicode="&#x2003;" horiz-adv-x="1776" />
+<glyph unicode="&#x2004;" horiz-adv-x="592" />
+<glyph unicode="&#x2005;" horiz-adv-x="444" />
+<glyph unicode="&#x2006;" horiz-adv-x="296" />
+<glyph unicode="&#x2007;" horiz-adv-x="296" />
+<glyph unicode="&#x2008;" horiz-adv-x="222" />
+<glyph unicode="&#x2009;" horiz-adv-x="355" />
+<glyph unicode="&#x200a;" horiz-adv-x="98" />
+<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
+<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
+<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 500v192h860v-192h-860z" />
+<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 500v192h1501v-192h-1501z" />
+<glyph unicode="&#x2018;" horiz-adv-x="552" d="M96 1384h215l152 -401h-184z" />
+<glyph unicode="&#x2019;" horiz-adv-x="552" d="M96 983l152 401h215l-182 -401h-185z" />
+<glyph unicode="&#x201a;" horiz-adv-x="651" d="M147 -229l152 401h215l-182 -401h-185z" />
+<glyph unicode="&#x201c;" horiz-adv-x="858" d="M96 1384h215l152 -401h-184zM408 1384h215l151 -401h-184z" />
+<glyph unicode="&#x201d;" horiz-adv-x="858" d="M96 983l152 401h215l-182 -401h-185zM408 983l151 401h215l-182 -401h-184z" />
+<glyph unicode="&#x201e;" horiz-adv-x="948" d="M147 -229l152 401h215l-182 -401h-185zM459 -229l151 401h215l-182 -401h-184z" />
+<glyph unicode="&#x2022;" horiz-adv-x="890" d="M172 661.5q0 112.5 80 192.5t194 80q113 0 192 -80t79 -192.5t-79 -192.5t-192 -80q-115 0 -194.5 80t-79.5 192.5z" />
+<glyph unicode="&#x2026;" horiz-adv-x="1726" d="M219 0v227h240v-227h-240zM743 0v227h240v-227h-240zM1266 0v227h239v-227h-239z" />
+<glyph unicode="&#x202f;" horiz-adv-x="355" />
+<glyph unicode="&#x2039;" horiz-adv-x="770" d="M63 522l347 389h286l-372 -389l372 -391h-286z" />
+<glyph unicode="&#x203a;" horiz-adv-x="815" d="M111 131l372 389l-372 391h286l346 -391l-346 -389h-286z" />
+<glyph unicode="&#x205f;" horiz-adv-x="444" />
+<glyph unicode="&#x20ac;" d="M102 412v178h162q-4 57 -4 94t4 94h-162v178h191q115 428 604 428q209 0 414 -81v-230q-194 90 -391 90q-137 0 -231.5 -43t-141.5 -164h530v-178h-567q-4 -57 -4 -94t4 -94h567v-178h-530q47 -121 141 -164t232 -43q197 0 391 90v-229q-205 -82 -414 -82 q-492 0 -604 428h-191z" />
+<glyph unicode="&#x2122;" horiz-adv-x="1572" d="M100 1245v121h559v-121h-219v-561h-121v561h-219zM762 684v682h137l197 -285l196 285h138v-682h-123v506l-211 -303l-211 303v-506h-123z" />
+<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
+<glyph unicode="&#xfb01;" horiz-adv-x="1333" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM950 1161h252v225h-252v-225zM954 0h246v1022h-246v-1022z" />
+<glyph unicode="&#xfb02;" horiz-adv-x="1333" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM958 0h246v1411h-246v-1411z" />
+<glyph unicode="&#xfb03;" horiz-adv-x="2100" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM813 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM1655 1161v225 h251v-225h-251zM1659 0v1022h245v-1022h-245z" />
+<glyph unicode="&#xfb04;" horiz-adv-x="2121" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM813 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM1669 0v1411h246 v-1411h-246z" />
+</font>
+</defs></svg> 
\ No newline at end of file
diff --git a/motioneye/static/fnt/mavenpro-bold-webfont.ttf b/motioneye/static/fnt/mavenpro-bold-webfont.ttf
new file mode 100644 (file)
index 0000000..2120446
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-bold-webfont.ttf differ
diff --git a/motioneye/static/fnt/mavenpro-bold-webfont.woff b/motioneye/static/fnt/mavenpro-bold-webfont.woff
new file mode 100644 (file)
index 0000000..7f73d1e
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-bold-webfont.woff differ
diff --git a/motioneye/static/fnt/mavenpro-medium-webfont.eot b/motioneye/static/fnt/mavenpro-medium-webfont.eot
new file mode 100644 (file)
index 0000000..efb30f5
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-medium-webfont.eot differ
diff --git a/motioneye/static/fnt/mavenpro-medium-webfont.svg b/motioneye/static/fnt/mavenpro-medium-webfont.svg
new file mode 100644 (file)
index 0000000..8e8503b
--- /dev/null
@@ -0,0 +1,243 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="maven_promedium" horiz-adv-x="1529" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="692" />
+<glyph horiz-adv-x="2048" />
+<glyph horiz-adv-x="2048" />
+<glyph unicode="&#xd;" horiz-adv-x="682" />
+<glyph unicode=" "  horiz-adv-x="692" />
+<glyph unicode="&#x09;" horiz-adv-x="692" />
+<glyph unicode="&#xa0;" horiz-adv-x="692" />
+<glyph unicode="!" horiz-adv-x="800" d="M299 1366h195l-17 -1032h-162zM303 0v174h186v-174h-186z" />
+<glyph unicode="&#x22;" horiz-adv-x="821" d="M166 1384h190l-34 -411h-119zM461 1384h190l-37 -411h-118z" />
+<glyph unicode="#" horiz-adv-x="1210" d="M145 260v135h183l55 318h-180v135h203l38 221h144l-39 -221h256l39 221h143l-39 -221h137v-135h-159l-56 -318h158v-135h-180l-39 -221h-143l39 221h-259l-36 -221h-144l39 221h-160zM471 395h256l55 318h-256z" />
+<glyph unicode="$" horiz-adv-x="1198" d="M158 993q0 145 96 256t299 131v209h127v-207q160 -10 307 -86v-188q-141 86 -307 90v-418q86 -31 145.5 -60.5t122 -78.5t95 -120t32.5 -163q0 -156 -103.5 -252t-291.5 -116v-211h-127v207q-193 6 -377 98v197q180 -109 377 -115v467q-76 23 -118 38t-102.5 46t-93 64.5 t-57 89t-24.5 122.5zM348 993q0 -57 51.5 -94t153.5 -74v367q-205 -35 -205 -199zM680 174q205 35 205 184q0 86 -53.5 138.5t-151.5 91.5v-414z" />
+<glyph unicode="%" horiz-adv-x="1814" d="M145 981q0 178 75 278.5t222.5 100.5t222 -100.5t74.5 -278.5t-74.5 -277.5t-222 -99.5t-222.5 99.5t-75 277.5zM276 981q0 -133 38 -197.5t128 -64.5t128 64.5t38 197.5q0 135 -38 198.5t-128 63.5t-128 -63.5t-38 -198.5zM410 0l858 1366h151l-858 -1366h-151z M1087 385q0 176 75 276.5t222.5 100.5t222 -100.5t74.5 -276.5q0 -178 -74.5 -278.5t-222 -100.5t-222.5 100.5t-75 278.5zM1219 385q0 -135 37.5 -198.5t128 -63.5t128 63.5t37.5 198.5q0 133 -37.5 197.5t-128 64.5t-128 -64.5t-37.5 -197.5z" />
+<glyph unicode="&#x26;" horiz-adv-x="1325" d="M111 385q0 115 54 216t210 192q-127 158 -127 272q0 131 93 225t226 94q135 0 227.5 -90t92.5 -221q0 -109 -56.5 -183.5t-183.5 -148.5l266 -319q37 84 54 176h176q-27 -168 -115 -313l238 -285h-228l-116 141q-157 -157 -374 -157h-3q-113 0 -200 39.5t-136 103 t-73.5 129.5t-24.5 129zM281 379q0 -31 11 -65.5t38.5 -75.5t84 -68t134.5 -27q156 0 270 123l-327 396q-74 -39 -121 -79t-64.5 -81t-21.5 -64.5t-4 -58.5zM416 1077.5q0 -65.5 114 -203.5q119 68 155 111t36 100q0 63 -43 105.5t-109 42.5q-63 0 -108 -45t-45 -110.5z" />
+<glyph unicode="'" horiz-adv-x="524" d="M166 1384h190l-34 -411h-119z" />
+<glyph unicode="(" horiz-adv-x="722" d="M190 627q0 440 211 856h185q-211 -416 -211 -856q0 -436 211 -858h-185q-211 422 -211 858z" />
+<glyph unicode=")" horiz-adv-x="722" d="M133 -231q211 412 211 858q0 449 -211 856h184q211 -416 211 -856q0 -436 -211 -858h-184z" />
+<glyph unicode="*" horiz-adv-x="841" d="M172 1118l162 70l-162 72l64 108l139 -100l-19 168h125l-14 -168l139 100l64 -108l-160 -72l160 -70l-64 -112l-139 102l14 -166h-125l19 166l-139 -102z" />
+<glyph unicode="+" horiz-adv-x="1150" d="M199 545v141h297v297h141v-297h295v-141h-295v-295h-141v295h-297z" />
+<glyph unicode="," horiz-adv-x="667" d="M152 -229l151 401h193l-172 -401h-172z" />
+<glyph unicode="-" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
+<glyph unicode="." horiz-adv-x="712" d="M260 0v182h195v-182h-195z" />
+<glyph unicode="/" horiz-adv-x="870" d="M-29 -201l752 1749h178l-754 -1749h-176z" />
+<glyph unicode="0" horiz-adv-x="1351" d="M133 684q0 166 34 294t86 202.5t126 122t145.5 63.5t153.5 16q104 0 191 -29.5t171 -101t132.5 -216t48.5 -351.5q0 -168 -34 -295t-86 -202.5t-126 -123t-144.5 -63.5t-152.5 -16t-153.5 16t-145.5 63.5t-126 123t-86 202.5t-34 295zM324 684q0 -135 21.5 -234.5 t54 -154.5t82.5 -88t94.5 -42t101.5 -9q55 0 100 9t94.5 42t82 88t54 154.5t21.5 234.5t-21.5 233.5t-54 153.5t-82 88t-94.5 42t-100 9q-57 0 -101.5 -9t-94.5 -42t-82.5 -88t-54 -153.5t-21.5 -233.5z" />
+<glyph unicode="1" horiz-adv-x="925" d="M211 1040v199l223 127h207v-1366h-190v1180z" />
+<glyph unicode="2" horiz-adv-x="1189" d="M158 0v152q111 84 214 177t210.5 208.5t172 234.5t64.5 215q0 219 -303 219q-184 0 -336 -102v180q170 98 361 98q238 0 351.5 -115.5t113.5 -277.5q0 -346 -519 -799h533v-190h-862z" />
+<glyph unicode="3" horiz-adv-x="1193" d="M166 63v187q160 -92 334 -92q338 0 338 239.5t-359 239.5h-119v168h115q295 0 295 199q0 200 -292 200h-3q-152 0 -278 -69v182q139 65 296 65h5q238 0 350.5 -110.5t112.5 -267.5q0 -177 -162 -273q229 -102 229 -332q0 -80 -27.5 -150.5t-86 -132t-161 -97 t-237.5 -35.5q-178 0 -350 79z" />
+<glyph unicode="4" horiz-adv-x="1210" d="M78 430v170l600 766h213v-766h194v-170h-194v-430h-191v430h-622zM303 600h397v494z" />
+<glyph unicode="5" horiz-adv-x="1228" d="M178 739l70 627h756v-176h-590l-33 -348q96 26 199 26h8q238 0 361.5 -112.5t123.5 -329.5t-124 -329.5t-361.5 -112.5t-401.5 102v205q133 -125 385 -125q180 0 246 65.5t66 194.5q0 131 -69 196.5t-263 65.5q-150 0 -277 -49z" />
+<glyph unicode="6" horiz-adv-x="1202" d="M133 627q0 141 25.5 256.5t65.5 191.5t96.5 134.5t111.5 90t119.5 51t112 25.5t94.5 6q106 0 211 -28v-172q-81 24 -175 24h-8q-180 0 -295.5 -100t-154.5 -287q111 72 285 72q240 0 348 -128t108 -310q0 -193 -120.5 -331t-350.5 -138q-473 0 -473 643zM324 616 q2 -131 24.5 -224t50 -138t72.5 -68.5t70.5 -26.5t64.5 -3q88 0 149.5 32.5t87 85t35 94.5t9.5 85q0 264 -281 264q-176 0 -282 -101z" />
+<glyph unicode="7" horiz-adv-x="1189" d="M143 1178v188h951v-180l-572 -1186h-211l580 1178h-748z" />
+<glyph unicode="8" horiz-adv-x="1196" d="M125 416q0 104 57.5 188t147.5 131q-154 98 -154 283q0 150 105.5 257t324.5 107t324.5 -107.5t105.5 -258.5q0 -186 -151 -279q200 -105 200 -319q0 -180 -122.5 -307t-356 -127t-357.5 128t-124 304zM315 414q0 -37 9.5 -74t37 -82t90 -72.5t154.5 -27.5t154.5 27.5 t90.5 72.5t37 82t9 74q0 225 -291 225t-291 -225zM365 1022q0 -31 8 -60.5t30.5 -67.5t74.5 -60.5t128 -22.5t128 22.5t73.5 60.5t30 67.5t8.5 60.5q0 186 -240 186q-241 0 -241 -186z" />
+<glyph unicode="9" horiz-adv-x="1212" d="M119 913q0 193 120.5 331t352.5 138q471 0 471 -643q0 -141 -25.5 -256.5t-65.5 -191.5t-96.5 -134.5t-111.5 -90t-119.5 -51t-112 -25.5t-94.5 -6q-106 0 -211 28v172q85 -24 176 -24h7q180 0 295.5 100t154.5 287q-111 -72 -285 -72q-240 0 -348 128t-108 310zM309 913 q0 -264 283 -264q176 0 282 101q-2 131 -24.5 224t-50 138t-72.5 68.5t-70.5 26.5t-64.5 3q-90 0 -151.5 -32.5t-87 -85t-35 -94.5t-9.5 -85z" />
+<glyph unicode=":" horiz-adv-x="813" d="M313 94v180h193v-180h-193zM313 741v181h193v-181h-193z" />
+<glyph unicode=";" horiz-adv-x="858" d="M184 -229l152 401h192l-172 -401h-172zM340 741v181h192v-181h-192z" />
+<glyph unicode="&#x3c;" horiz-adv-x="1357" d="M246 477v148l850 379v-181l-643 -272l643 -272v-181z" />
+<glyph unicode="=" horiz-adv-x="1306" d="M260 365v159h789v-159h-789zM260 698v160h789v-160h-789z" />
+<glyph unicode="&#x3e;" horiz-adv-x="1337" d="M270 98v181l641 272l-641 272v181l848 -379v-148z" />
+<glyph unicode="?" horiz-adv-x="1042" d="M164 1083v203q150 96 321 96q115 0 200 -33.5t132 -92t69.5 -126t22.5 -145.5q0 -82 -30.5 -146.5t-75.5 -106.5l-90 -83q-45 -41 -76 -100.5t-31 -134.5v-70h-180v76q0 104 30.5 184t76 127t89.5 86t74.5 81t30.5 89q0 219 -258 219q-166 0 -305 -123zM416 0v186h200 v-186h-200z" />
+<glyph unicode="@" horiz-adv-x="1869" d="M170 371q0 184 79 348t204 273.5t280 174t311 64.5q82 0 171.5 -21.5t180.5 -74t162.5 -126t117.5 -188t46 -254.5q0 -129 -33.5 -239.5t-113.5 -188.5t-193 -78q-137 0 -186 127q-90 -127 -293 -127q-143 0 -227 64.5t-84 175.5q0 51 16.5 96t59.5 95.5t138 80t232 29.5 q84 0 142 -8q0 4 1 15.5t1 17.5q0 35 -6.5 59.5t-26 50t-62.5 37.5t-110 12q-131 0 -252 -49l18 129q135 45 252 45q156 0 239 -72.5t83 -211.5q0 -53 -9 -129l-16 -125q-4 -37 -4 -74v-25q2 -49 26.5 -72.5t76.5 -23.5q88 0 151.5 97.5t63.5 287.5q0 143 -56.5 255 t-145.5 173.5t-183.5 92t-184.5 30.5q-184 0 -357 -98t-284.5 -273.5t-111.5 -377.5q0 -127 45 -229.5t109.5 -162t145.5 -99.5t144.5 -54.5t114.5 -14.5q250 0 442 103l-18 -133q-184 -82 -428 -82q-82 0 -169 22.5t-177.5 74.5t-161 128t-115.5 193.5t-45 259.5zM725 305 q0 -108 175 -108h3q141 0 188.5 51t61.5 153q4 49 8 74q-55 6 -123 6q-313 0 -313 -176z" />
+<glyph unicode="A" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM463 561h434l-217 567z" />
+<glyph unicode="B" horiz-adv-x="1280" d="M178 0v1366h473q139 0 234.5 -37t138.5 -98.5t59.5 -117.5t16.5 -120q0 -59 -16.5 -109t-40 -80t-48 -51.5t-41.5 -29.5l-16 -8l26 -11q16 -6 57.5 -32.5t73 -59t58.5 -92t27 -129.5q0 -68 -18.5 -127t-65.5 -123.5t-150.5 -102.5t-255.5 -38h-512zM369 168l309 2 q158 0 234.5 53t76.5 176q0 127 -84 173t-241 46h-295v-450zM369 788h280q127 0 193.5 42t66.5 157q0 117 -70.5 163t-207.5 46h-262v-408z" />
+<glyph unicode="C" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q213 0 418 -81v-177q-193 86 -396 86q-242 0 -351 -124.5t-109 -403.5q0 -276 109 -402t351 -126q203 0 396 86v-176q-205 -82 -418 -82q-94 0 -177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295z" />
+<glyph unicode="D" horiz-adv-x="1380" d="M178 0v1366h436q315 0 478 -174t163 -510t-162.5 -509t-478.5 -173h-436zM369 168h215q250 0 366.5 124t116.5 390q0 268 -116.5 392t-366.5 124h-215v-1030z" />
+<glyph unicode="E" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891z" />
+<glyph unicode="F" horiz-adv-x="1124" d="M178 0v1366h860v-178h-669v-416h589v-178h-589v-594h-191z" />
+<glyph unicode="G" horiz-adv-x="1386" d="M125 684q0 166 39 294t100.5 202.5t147.5 122t169 63.5t179 16q227 0 430 -79v-175q-199 82 -412 82q-74 0 -132 -9t-122.5 -42t-107.5 -88t-72 -153.5t-29 -233.5t29 -234.5t72 -154.5t108.5 -88t125 -42t132.5 -9q135 0 275 39v385h-279v178h463v-666 q-227 -108 -476 -108h-5q-96 0 -179 16t-169 63.5t-147.5 123t-100.5 202.5t-39 295z" />
+<glyph unicode="H" horiz-adv-x="1388" d="M178 0v1366h189v-594h655v594h188v-1366h-188v594h-655v-594h-189z" />
+<glyph unicode="I" horiz-adv-x="544" d="M178 0v1366h189v-1366h-189z" />
+<glyph unicode="J" horiz-adv-x="931" d="M61 66v198q119 -96 269 -96q117 0 174 49t57 186v963h189v-950q0 -133 -40 -225.5t-106.5 -134.5t-127 -58t-128.5 -16q-158 0 -287 84z" />
+<glyph unicode="K" horiz-adv-x="1380" d="M178 0v1366h189v-661l626 661h240l-580 -618l631 -748h-235l-521 614l-161 -172v-442h-189z" />
+<glyph unicode="L" horiz-adv-x="1089" d="M178 0v1366h189v-1188h680v-178h-869z" />
+<glyph unicode="M" horiz-adv-x="1665" d="M178 0v1366h205l448 -590l451 590h205v-1366h-189v1077l-467 -612l-464 612v-1077h-189z" />
+<glyph unicode="N" horiz-adv-x="1511" d="M178 0v1366h217l764 -1087v1087h191v-1366h-211l-770 1094v-1094h-191z" />
+<glyph unicode="O" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5z" />
+<glyph unicode="P" horiz-adv-x="1228" d="M176 0v1366h492q227 0 352 -112.5t125 -317.5t-125 -317.5t-352 -112.5h-301v-506h-191zM367 674h284q182 0 243.5 70.5t61.5 189.5t-61 189.5t-244 70.5h-284v-520z" />
+<glyph unicode="Q" d="M125 684q0 166 39 294t100.5 202.5t148.5 122t170 63.5t177 16q96 0 179 -16t169 -63.5t147.5 -122t100.5 -202.5t39 -294q0 -182 -45 -317.5t-124 -212t-167 -114.5t-195 -50q0 -82 54.5 -143.5t150.5 -61.5v-145q-170 0 -280.5 101t-110.5 247q-111 10 -202 46 t-174 111.5t-130 213t-47 325.5zM315 684q0 -135 28 -234.5t69 -154.5t103.5 -88t118.5 -42t125.5 -9t126 9t119 42t104.5 88t69.5 154.5t27.5 234.5t-27.5 233.5t-68.5 153.5t-104.5 88t-119 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -153.5t-28 -233.5z" />
+<glyph unicode="R" horiz-adv-x="1298" d="M178 0v1366h504q229 0 360.5 -107.5t131.5 -306.5q0 -168 -87.5 -267t-218.5 -136l363 -549h-225l-330 518h-307v-518h-191zM369 698h305q178 0 243.5 69t65.5 179q0 250 -317 250h-297v-498z" />
+<glyph unicode="S" horiz-adv-x="1155" d="M125 993q0 162 116.5 275.5t360.5 113.5q184 0 352 -86v-188q-150 90 -329 90q-310 0 -310 -205q0 -55 54.5 -96t134.5 -65.5t175 -63.5t175 -83t134 -129t54 -198q0 -176 -131 -275t-362 -99q-205 0 -406 100v197q190 -115 389 -115q319 0 320 192q0 82 -54 140t-134 84 l-176 59q-96 33 -175.5 67.5t-133.5 107.5t-54 177z" />
+<glyph unicode="T" horiz-adv-x="1163" d="M25 1188v178h1112v-178h-463v-1188h-189v1188h-460z" />
+<glyph unicode="U" horiz-adv-x="1474" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5z" />
+<glyph unicode="V" horiz-adv-x="1288" d="M25 1366h202l416 -1126l418 1126h203l-525 -1366h-190z" />
+<glyph unicode="W" horiz-adv-x="1961" d="M25 1366h198l338 -1090l336 1090h162l338 -1090l336 1090h198l-438 -1366h-195l-319 1026l-322 -1026h-194z" />
+<glyph unicode="X" horiz-adv-x="1253" d="M25 0l489 684l-477 682h231l359 -510l358 510h232l-484 -664l496 -702h-234l-368 526l-371 -526h-231z" />
+<glyph unicode="Y" horiz-adv-x="1275" d="M25 1366h229l383 -559l383 559h231l-520 -754v-612h-188v610z" />
+<glyph unicode="Z" horiz-adv-x="1247" d="M131 0v166l721 1010h-700v190h946v-170l-717 -1008h717v-188h-967z" />
+<glyph unicode="[" horiz-adv-x="784" d="M244 -238v1663h370v-151h-213v-1360h213v-152h-370z" />
+<glyph unicode="\" horiz-adv-x="802" d="M-76 1548h178l752 -1749h-176z" />
+<glyph unicode="]" horiz-adv-x="780" d="M172 -86h211v1360h-211v151h369v-1663h-369v152z" />
+<glyph unicode="^" horiz-adv-x="1146" d="M272 969l215 444h113l217 -444h-143l-129 303l-129 -303h-144z" />
+<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-115h-1118v115z" />
+<glyph unicode="`" horiz-adv-x="491" d="M90 1395h184l127 -240h-116z" />
+<glyph unicode="a" horiz-adv-x="1093" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q145 55 309 55q213 0 329 -110.5t116 -317.5v-295q0 -33 -6.5 -65.5t-31 -83.5t-66.5 -89t-126 -65.5t-194 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88z" />
+<glyph unicode="b" horiz-adv-x="1167" d="M170 315v1096h186v-403q106 30 219 30q88 0 164 -21.5t149 -74.5t114.5 -159.5t41.5 -258.5q0 -135 -31.5 -236.5t-77.5 -157.5t-110.5 -91t-119 -45t-113.5 -10q-111 0 -194 27.5t-125 65.5t-66.5 89t-30.5 84t-6 65zM356 303q0 -153 236 -153q53 0 94 13t83 49 t64.5 115t22.5 197q0 111 -22.5 183.5t-64.5 106.5t-87 46t-107 12q-115 0 -219 -43v-526z" />
+<glyph unicode="c" horiz-adv-x="966" d="M119 512q0 256 124 391t357 135q156 0 301 -57v-178q-139 67 -281 67h-4q-141 0 -221 -72.5t-80 -285.5q0 -113 23.5 -187.5t70 -110.5t94 -48t113.5 -12q147 0 285 65v-178q-145 -57 -301 -57q-233 0 -357 136t-124 392z" />
+<glyph unicode="d" horiz-adv-x="1150" d="M129 524q0 152 42 258.5t114.5 159.5t148.5 74.5t164 21.5q111 0 219 -30v403h187v-1096q0 -33 -6.5 -65.5t-31 -83.5t-66.5 -89t-125 -65.5t-193 -27.5q-59 0 -113.5 10t-119 45t-110.5 91t-78 157.5t-32 236.5zM315 524q0 -119 22.5 -197.5t64.5 -114.5t83 -49t97 -13 q236 0 235 153v526q-104 43 -219 43q-61 0 -106.5 -12t-88.5 -46t-65.5 -106.5t-22.5 -183.5z" />
+<glyph unicode="e" horiz-adv-x="1136" d="M129 498q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -142h-704q34 -260 288 -260q207 0 353 76v-170q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 573h525q-4 51 -17.5 97.5t-41 94.5t-79.5 76.5t-124 28.5 q-129 0 -189.5 -66.5t-73.5 -230.5z" />
+<glyph unicode="f" horiz-adv-x="675" d="M82 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-49h264v-164h-264v-858h-187v858h-143z" />
+<glyph unicode="g" horiz-adv-x="1101" d="M123 518q0 238 107.5 380t330.5 142q186 0 298 -84t112 -239v-760q0 -68 -19.5 -126t-65.5 -113.5t-138.5 -87t-225.5 -31.5q-154 0 -303 43v178q145 -45 287 -45q98 0 159.5 17.5t85 50t28.5 55t5 59.5v90q-104 -29 -206 -29q-219 0 -337 129t-118 371zM311 518 q0 -199 71 -264.5t196 -65.5q110 0 206 39v502q0 23 -4 39t-22.5 45t-68.5 44t-128 15q-51 0 -89 -12t-78 -46t-61.5 -108.5t-21.5 -187.5z" />
+<glyph unicode="h" horiz-adv-x="1187" d="M193 0v1411h188v-424q131 51 270 51q190 0 295 -113.5t105 -322.5v-602h-189v608q0 150 -63.5 200t-165.5 50q-125 0 -252 -49v-809h-188z" />
+<glyph unicode="i" horiz-adv-x="589" d="M195 1180v198h200v-198h-200zM201 0v1022h188v-1022h-188z" />
+<glyph unicode="j" horiz-adv-x="526" d="M6 -242q109 0 142.5 47.5t33.5 135.5v1081h187v-1081q0 -342 -363 -342v159zM176 1180v198h199v-198h-199z" />
+<glyph unicode="k" horiz-adv-x="1204" d="M188 0v1411h187v-817l442 428h254l-444 -426l503 -596h-249l-379 475l-127 -123v-352h-187z" />
+<glyph unicode="l" horiz-adv-x="602" d="M207 0v1411h188v-1411h-188z" />
+<glyph unicode="m" horiz-adv-x="1853" d="M193 0v938q205 102 425 102q166 0 273 -77q193 77 375 77q193 0 301 -101t108 -282v-657h-186v670q0 88 -43 142t-168 54q-135 0 -274 -57q24 -65 24 -146v-6v-657h-188v668q0 90 -47.5 142t-188.5 52q-109 0 -223 -31v-831h-188z" />
+<glyph unicode="n" horiz-adv-x="1198" d="M168 0v934q213 106 436 106q193 0 302.5 -101t109.5 -280v-659h-189v672q0 90 -46 142t-185 52q-125 0 -240 -37v-829h-188z" />
+<glyph unicode="o" horiz-adv-x="1161" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5z" />
+<glyph unicode="p" horiz-adv-x="1167" d="M168 -389v1098q0 33 6 65.5t30.5 82.5t66.5 88t126 65.5t195 27.5q57 0 111.5 -10t119 -45t111.5 -91t78.5 -157.5t31.5 -234.5q0 -152 -41.5 -259.5t-114.5 -159.5t-148.5 -74.5t-166.5 -22.5q-113 0 -217 30v-403h-188zM356 193q106 -41 217 -41q63 0 107.5 12t87.5 46 t65.5 106.5t22.5 183.5q0 119 -22.5 197.5t-64.5 114.5t-83 49t-94 13q-236 0 -236 -153v-528z" />
+<glyph unicode="q" horiz-adv-x="1157" d="M115 500q0 133 31.5 234.5t77.5 157.5t110.5 91t119 45t111.5 10q111 0 195 -27.5t126 -65.5t66.5 -88t30.5 -83t6 -65v-1098h-188v403q-104 -30 -217 -30q-90 0 -166 21.5t-147.5 74.5t-113.5 160.5t-42 259.5zM301 500q0 -111 22.5 -183.5t65.5 -106.5t87 -46t108 -12 q113 0 217 41v528q0 153 -236 153q-53 0 -94 -13t-83 -49t-64.5 -115t-22.5 -197z" />
+<glyph unicode="r" horiz-adv-x="702" d="M166 0v936q256 102 498 102v-172q-152 -2 -310 -43v-823h-188z" />
+<glyph unicode="s" horiz-adv-x="892" d="M102 733q0 127 90.5 216t276.5 89q139 0 274 -67v-162q-133 61 -251 61q-203 0 -203 -131q0 -43 54 -71.5t130 -50t152.5 -52t131 -102.5t54.5 -178q0 -123 -93 -212t-288 -89q-164 0 -295 71v170q133 -75 283 -75h4q98 0 149.5 35.5t51.5 90.5q0 63 -53.5 101.5 t-130.5 60t-153.5 48t-130 88t-53.5 159.5z" />
+<glyph unicode="t" horiz-adv-x="765" d="M49 864v158h178v305h187v-305h272v-158h-272v-594q0 -120 151 -120h2q70 0 119 30v-168q-70 -28 -140 -28h-5q-37 0 -79 9t-100.5 35.5t-96.5 92t-38 162.5v581h-178z" />
+<glyph unicode="u" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5z" />
+<glyph unicode="v" horiz-adv-x="1107" d="M66 1022h200l287 -797l289 797h200l-393 -1022h-192z" />
+<glyph unicode="w" horiz-adv-x="1656" d="M80 1022h207l229 -756l225 756h174l228 -756l227 756h207l-338 -1022h-186l-224 711l-225 -711h-186z" />
+<glyph unicode="x" horiz-adv-x="1073" d="M57 0l369 510l-360 512h229l242 -342l241 342h230l-361 -498l369 -524h-230l-249 354l-250 -354h-230z" />
+<glyph unicode="y" horiz-adv-x="1073" d="M63 1022h201l275 -766l270 766h201l-420 -1131q-109 -294 -317 -294h-1q-68 0 -143 16v168q61 -16 115 -17q127 0 205 236z" />
+<glyph unicode="z" horiz-adv-x="962" d="M92 0v172l520 676h-508v174h748v-172l-518 -674h518v-176h-760z" />
+<glyph unicode="{" horiz-adv-x="763" d="M186 535v137q109 31 109 127v313q0 156 82 238t237 82h17v-158q-84 0 -120 -32t-36 -124v-307q0 -123 -119 -207q119 -88 119 -209v-307q0 -92 36 -123t120 -33v-157h-17q-156 0 -237.5 83t-81.5 238v312q0 96 -109 127z" />
+<glyph unicode="|" horiz-adv-x="946" d="M379 -203v1569h188v-1569h-188z" />
+<glyph unicode="}" horiz-adv-x="759" d="M156 -68q84 2 119.5 33t35.5 123v307q0 121 119 209q-119 84 -119 207v307q0 94 -35.5 125t-119.5 31v158h18q154 0 237 -82t83 -238v-313q0 -94 106 -127v-137q-106 -33 -106 -127v-312q0 -156 -83 -238.5t-237 -82.5h-18v157z" />
+<glyph unicode="~" horiz-adv-x="1024" d="M201 473v135q57 68 123 74q11 1 22 1q55 0 106 -25l122 -61q50 -25 103 -25q12 0 23 2q66 6 123 73v-135q-57 -68 -123 -74q-10 -1 -21 -1q-54 0 -105 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -123 -74z" />
+<glyph unicode="&#xa1;" horiz-adv-x="788" d="M299 -344l16 1030h162l17 -1030h-195zM303 848v174h186v-174h-186z" />
+<glyph unicode="&#xa2;" horiz-adv-x="976" d="M117 684q0 238 108.5 373t313.5 151v158h123v-158q131 -12 237 -55v-178q-123 57 -237 65v-712q121 8 237 63v-178q-117 -47 -237 -55v-158h-123v158q-205 16 -313.5 152t-108.5 374zM313 684q0 -182 59.5 -260t166.5 -94v708q-106 -16 -166 -94t-60 -260z" />
+<glyph unicode="&#xa3;" horiz-adv-x="1155" d="M104 -53v182q82 66 181 82v360h-181v168h181v258q0 74 23.5 139.5t71.5 122t134 89t201 32.5q172 0 325 -90v-192q-143 108 -307 108q-264 0 -264 -209v-258h389v-168h-389v-366q45 -8 139 -33t150 -35q34 -6 75 -6q28 0 61 3q79 7 146 44v-182q-86 -47 -186 -47 t-191.5 25.5t-183.5 47.5q-56 14 -114 14q-38 0 -77 -6q-98 -14 -184 -83z" />
+<glyph unicode="&#xa5;" horiz-adv-x="1298" d="M37 1366h229l383 -559l383 559h232l-435 -629h215v-153h-301v-162h301v-154h-301v-268h-188v268h-301v154h301v162h-301v153h215z" />
+<glyph unicode="&#xa7;" horiz-adv-x="1198" d="M131 725q0 160 225 225q-72 55 -71 140q0 125 100 209.5t313 84.5q166 0 322 -77v-181q-145 80 -291 80q-248 0 -248 -125q0 -31 45 -54t112 -39.5t144.5 -43t144 -60.5t111.5 -96.5t45 -144.5q0 -90 -64.5 -145.5t-160.5 -79.5q72 -53 72 -131q0 -125 -101.5 -214 t-314.5 -89q-190 0 -375 106v191q174 -117 348 -117q246 0 246 123q0 31 -45 54.5t-111.5 39.5t-144.5 43t-144.5 60.5t-111.5 96t-45 144.5zM328 723q0 -33 19.5 -59.5t61.5 -46t79.5 -34t101 -34t100.5 -33.5q90 8 143.5 40t53.5 81q0 33 -19.5 58.5t-61.5 46t-80 35 t-101.5 33.5t-100.5 34q-90 -8 -143 -41t-53 -80z" />
+<glyph unicode="&#xa8;" horiz-adv-x="831" d="M162 1155v174h186v-174h-186zM465 1155v174h186v-174h-186z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 680q0 289 204 493.5t492.5 204.5t493.5 -204.5t205 -493.5t-205 -492.5t-493.5 -203.5t-492.5 203.5t-204 492.5zM188 680q0 -250 178.5 -428t428.5 -178q252 0 430 178t178 428q0 252 -178 430t-430 178q-250 0 -428.5 -178t-178.5 -430zM432 680q0 121 33 206 t90 127t117.5 59.5t130.5 17.5q127 0 244 -48v-104q-113 51 -232 51q-141 0 -204.5 -72.5t-63.5 -236.5q0 -162 63.5 -234.5t204.5 -72.5q115 0 232 49v-103q-117 -47 -244 -47q-55 0 -103.5 9.5t-99.5 37t-87 71.5t-58.5 118t-22.5 172z" />
+<glyph unicode="&#xaa;" horiz-adv-x="813" d="M156 985q0 84 71.5 132t196.5 48q53 0 102 -6q-4 72 -39.5 103.5t-117.5 31.5q-70 0 -156 -28v88q86 30 172 30q119 0 182.5 -61t63.5 -176v-164q0 -25 -7.5 -49.5t-28 -58t-72.5 -55t-128 -21.5q-63 0 -110 15t-71 36.5t-38 50.5t-17 47.5t-3 36.5zM260 987 q0 -96 135 -96q131 0 131 92v92q-35 8 -98 8q-33 0 -66.5 -6t-67.5 -29.5t-34 -60.5z" />
+<glyph unicode="&#xab;" horiz-adv-x="1171" d="M63 522l347 389h223l-365 -389l365 -391h-223zM498 522l346 389h223l-365 -389l365 -391h-223z" />
+<glyph unicode="&#xac;" horiz-adv-x="1273" d="M199 528v156h876v-360h-151v204h-725z" />
+<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
+<glyph unicode="&#xae;" horiz-adv-x="1593" d="M98 684q0 289 205 494.5t496 205.5t495.5 -205.5t204.5 -494.5q0 -291 -204.5 -495.5t-495.5 -204.5t-496 204.5t-205 495.5zM188 684q0 -254 178.5 -432t432.5 -178q252 0 430 178t178 432q0 252 -178 430t-430 178q-254 0 -432.5 -178t-178.5 -430zM528 285v796h295 q133 0 210 -62.5t77 -178.5q0 -98 -51 -156t-127 -78l211 -321h-131l-193 303h-180v-303h-111zM639 692h178q180 0 180 145.5t-184 145.5h-174v-291z" />
+<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1180v123h477v-123h-477z" />
+<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1143q0 111 57.5 169t166 58t165.5 -58.5t57 -168.5q0 -109 -57 -167t-165.5 -58t-166 58t-57.5 167zM260 1143q0 -76 31 -109t100.5 -33t99 33t29.5 109t-29.5 108.5t-99 32.5t-100.5 -32.5t-31 -108.5z" />
+<glyph unicode="&#xb1;" horiz-adv-x="1214" d="M231 55v150h754v-150h-754zM231 612v150h304v301h149v-301h301v-150h-301v-303h-149v303h-304z" />
+<glyph unicode="&#xb2;" horiz-adv-x="671" d="M109 948v82q137 102 246.5 226t109.5 223q0 118 -161 118h-3q-104 0 -180 -55v96q88 53 194 54q127 0 188.5 -62.5t61.5 -148.5q0 -184 -278 -430h274v-103h-452z" />
+<glyph unicode="&#xb3;" horiz-adv-x="661" d="M109 983v100q86 -49 180 -49q182 0 182 127q0 129 -192 129h-62v92h59q158 0 158 106.5t-158 106.5q-86 0 -159 -41v99q74 39 172 39q127 0 187.5 -59.5t60.5 -143.5q0 -95 -86 -148q122 -53 122 -176q0 -94 -66.5 -159.5t-207.5 -65.5q-104 0 -190 43z" />
+<glyph unicode="&#xb4;" horiz-adv-x="518" d="M123 1155l127 240h184l-194 -240h-117z" />
+<glyph unicode="&#xb5;" horiz-adv-x="1253" d="M203 -430v1454h188v-672q0 -90 46 -142t186 -52q125 0 239 37v829h189v-934q-213 -106 -437 -106q-129 0 -223 47v-461h-188z" />
+<glyph unicode="&#xb6;" horiz-adv-x="940" d="M78 936q0 207 123.5 318.5t359.5 111.5h238v-1366h-170v504h-68q-236 0 -359.5 111.5t-123.5 320.5z" />
+<glyph unicode="&#xb7;" horiz-adv-x="663" d="M238 471v174h186v-174h-186z" />
+<glyph unicode="&#xb8;" horiz-adv-x="555" d="M115 -262l182 252h145l-141 -252h-186z" />
+<glyph unicode="&#xb9;" horiz-adv-x="491" d="M109 1509v107l118 67h113v-735h-102v635z" />
+<glyph unicode="&#xba;" horiz-adv-x="864" d="M156 1090q0 115 44 184t99 89.5t125 20.5q113 0 191.5 -64.5t78.5 -229.5q0 -164 -78.5 -229.5t-191.5 -65.5q-111 0 -189.5 65.5t-78.5 229.5zM260 1089.5q0 -120.5 43 -160.5t121 -40t121 40t43 160.5t-43 161.5t-121 41t-121 -41t-43 -161.5z" />
+<glyph unicode="&#xbb;" horiz-adv-x="1212" d="M137 131l365 391l-365 389h223l347 -389l-347 -391h-223zM571 131l365 391l-365 389h224l346 -389l-346 -391h-224z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1447" d="M106 1206v107l119 67h111v-735h-103v635zM145 -45l893 1448h131l-895 -1448h-129zM799 231v93l323 413h115v-413h104v-93h-104v-229h-102v229h-336zM920 324h215v266z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1482" d="M106 1206v107l119 67h111v-735h-103v635zM145 -45l893 1448h131l-895 -1448h-129zM895 2v82q135 102 244.5 226t109.5 222q0 119 -164 119q-92 0 -180 -55v96q88 53 195 53q127 0 188.5 -61t61.5 -149q0 -188 -279 -431h277v-102h-453z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1662" d="M135 680v100q86 -49 180 -49q182 0 183 127q0 129 -193 129h-61v92h59q160 0 160 106.5t-160 106.5q-86 0 -160 -41v99q82 41 172 41q127 0 187.5 -60.5t60.5 -144.5q0 -95 -86 -148q123 -53 123 -178q0 -92 -66.5 -157.5t-207.5 -65.5q-100 0 -191 43zM369 -45l895 1448 h129l-893 -1448h-131zM1024 231v93l322 413h116v-413h105v-93h-105v-229h-102v229h-336zM1145 324h215v266z" />
+<glyph unicode="&#xbf;" horiz-adv-x="1052" d="M174 37q0 82 31 146.5t76 106.5l90 83q45 41 75.5 100t30.5 135v70h180v-76q0 -104 -30.5 -184t-75.5 -127t-89 -86t-75 -81t-31 -89q0 -219 258 -219q166 0 306 123v-203q-150 -96 -322 -96q-115 0 -200 33.5t-132 92t-69.5 126t-22.5 145.5zM467 836v186h201v-186h-201 z" />
+<glyph unicode="&#xc0;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM424 1722h182l129 -239h-117zM463 561h434l-217 567z" />
+<glyph unicode="&#xc1;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM463 561h434l-217 567zM625 1483l129 239h182l-195 -239h-116z" />
+<glyph unicode="&#xc2;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM451 1483l180 250h98l182 -250h-145l-86 133l-86 -133h-143zM463 561h434l-217 567z" />
+<glyph unicode="&#xc3;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM369 1481v135q57 68 123 74q11 1 22 1q55 0 106 -26l122 -61q51 -24 104 -24q11 0 22 1q66 6 123 74v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2 q-66 -6 -123 -73zM463 561h434l-217 567z" />
+<glyph unicode="&#xc4;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM434 1483v174h189v-174h-189zM463 561h434l-217 567zM737 1483v174h189v-174h-189z" />
+<glyph unicode="&#xc5;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM463 561h434l-217 567zM502 1599q0 82 47 128.5t131 46.5t131 -46.5t47 -128.5q0 -84 -47 -130t-131 -46t-131 46.5t-47 129.5zM600 1599.5q0 -51.5 17.5 -72t62.5 -20.5t62.5 20.5t17.5 72t-17.5 72 t-62.5 20.5t-62.5 -20.5t-17.5 -72z" />
+<glyph unicode="&#xc6;" horiz-adv-x="2031" d="M51 0l858 1366h983v-178h-700v-416h616v-178h-616v-416h700v-178h-891v414h-481l-258 -414h-211zM633 592h368v588z" />
+<glyph unicode="&#xc7;" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q213 0 418 -81v-177q-193 86 -396 86q-242 0 -351 -124.5t-109 -403.5q0 -276 109.5 -402t350.5 -126q203 0 396 86v-176q-176 -72 -361 -80l-137 -248h-188l184 250q-111 10 -201 47t-173 112.5t-130 213 t-47 323.5z" />
+<glyph unicode="&#xc8;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM412 1722h184l127 -239h-117z" />
+<glyph unicode="&#xc9;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM582 1483l127 239h184l-195 -239h-116z" />
+<glyph unicode="&#xca;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM416 1483l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
+<glyph unicode="&#xcb;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM397 1483v174h187v-174h-187zM700 1483v174h187v-174h-187z" />
+<glyph unicode="&#xcc;" horiz-adv-x="544" d="M14 1722h185l127 -239h-117zM178 0h189v1366h-189v-1366z" />
+<glyph unicode="&#xcd;" horiz-adv-x="544" d="M178 0v1366h189v-1366h-189zM217 1483l127 239h184l-194 -239h-117z" />
+<glyph unicode="&#xce;" horiz-adv-x="544" d="M41 1483l180 250h98l183 -250h-146l-86 133l-86 -133h-143zM178 0h189v1366h-189v-1366z" />
+<glyph unicode="&#xcf;" horiz-adv-x="544" d="M27 1483v174h186v-174h-186zM178 0h189v1366h-189v-1366zM330 1483v174h186v-174h-186z" />
+<glyph unicode="&#xd0;" horiz-adv-x="1480" d="M109 606v154h163v606h437q315 0 479 -174t164 -510t-164 -509t-479 -173h-437v606h-163zM463 168h215q252 0 368.5 124t116.5 390q0 268 -116.5 392t-368.5 124h-215v-438h305v-154h-305v-438z" />
+<glyph unicode="&#xd1;" horiz-adv-x="1511" d="M178 0v1366h217l764 -1087v1087h191v-1366h-211l-770 1094v-1094h-191zM453 1481v135q57 68 123 74q11 1 22 1q55 0 106 -26l122 -61q51 -24 104 -24q11 0 22 1q66 6 123 74v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2 q-66 -6 -123 -73z" />
+<glyph unicode="&#xd2;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM524 1722h185l127 -239h-117z" />
+<glyph unicode="&#xd3;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM684 1483l127 239h184l-194 -239h-117z" />
+<glyph unicode="&#xd4;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM528 1483l181 250h98l182 -250h-145l-86 133l-86 -133h-144z" />
+<glyph unicode="&#xd5;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM449 1481v135q57 68 123 74q11 1 22 1q55 0 106 -26l122 -61q51 -24 104 -24 q11 0 22 1q66 6 123 74v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2q-66 -6 -123 -73z" />
+<glyph unicode="&#xd6;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM514 1483v174h186v-174h-186zM817 1483v174h187v-174h-187z" />
+<glyph unicode="&#xd7;" horiz-adv-x="1050" d="M158 160l282 364l-282 365h164l200 -258l203 258h162l-281 -365l281 -364h-162l-203 258l-200 -258h-164z" />
+<glyph unicode="&#xd8;" d="M88 0l168 201q-131 174 -131 483q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q244 0 399 -112l82 94h191l-168 -199q131 -174 131 -483q0 -168 -39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16q-244 0 -400 112l-81 -96h-191zM315 684q0 -209 64 -336 l665 789q-99 75 -279 75h-5q-72 0 -127 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM475 231q97 -75 280 -75h5q72 0 127 9t117.5 42t103.5 88t68.5 154.5t27.5 234.5q0 209 -63 336z" />
+<glyph unicode="&#xd9;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM498 1722h184l127 -239h-117z" />
+<glyph unicode="&#xda;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM666 1483l127 239h184l-195 -239h-116z" />
+<glyph unicode="&#xdb;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM506 1483l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
+<glyph unicode="&#xdc;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM492 1483v174h186v-174h-186zM795 1483v174h186v-174h-186z" />
+<glyph unicode="&#xdd;" horiz-adv-x="1275" d="M25 1366h229l383 -559l383 559h231l-520 -754v-612h-188v610zM561 1483l127 239h184l-194 -239h-117z" />
+<glyph unicode="&#xde;" horiz-adv-x="1187" d="M176 0v1366h191v-205h231q227 0 352 -112.5t125 -317.5t-125 -317.5t-352 -112.5h-231v-301h-191zM367 469h215q182 0 243.5 70.5t61.5 189.5v2v2q0 119 -61.5 189.5t-243.5 70.5h-215v-524z" />
+<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M180 0v862q0 154 39 262.5t106.5 161.5t137 75.5t153.5 22.5q236 0 347.5 -114.5t111.5 -276.5q0 -188 -188 -280q254 -74 254 -316q0 -186 -150.5 -291.5t-400.5 -105.5v174q61 0 115.5 10.5t113 33t93 70.5t34.5 118q0 217 -356 231v143q293 4 293 219q0 219 -267 220 q-53 0 -92 -13.5t-75.5 -49.5t-56 -111.5t-19.5 -190.5v-854h-193z" />
+<glyph unicode="&#xe0;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM315 1395h185l127 -240h-117z" />
+<glyph unicode="&#xe1;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM461 1155l127 240h184l-194 -240h-117z" />
+<glyph unicode="&#xe2;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM338 1155l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
+<glyph unicode="&#xe3;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M250 1153v135q57 68 124 74q10 1 21 1q55 0 105 -25l122 -61q51 -25 104 -25q12 0 24 2q65 6 122 73v-135q-57 -68 -122 -74q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74zM301 324q0 -174 246 -174h3q232 0 232 167v166 q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88z" />
+<glyph unicode="&#xe4;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM324 1155v174h186v-174h-186zM627 1155v174h186v-174h-186z" />
+<glyph unicode="&#xe5;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM379 1272q0 84 47 130t131 46q86 0 132 -46t46 -130t-46 -130t-132 -46q-84 0 -131 46t-47 130zM477 1272q0 -51 17.5 -71.5t62.5 -20.5t62.5 20.5t17.5 71.5 t-17.5 71.5t-62.5 20.5t-62.5 -20.5t-17.5 -71.5z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1802" d="M111 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q160 55 309 55q244 0 357 -143q113 143 348 143q205 0 327.5 -134t122.5 -359q0 -74 -6 -142h-704q35 -260 288 -260q197 0 353 76v-170q-158 -67 -357 -67h-6 q-229 0 -350 131q-106 -129 -352 -129q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67zM297 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM965 573h524q-27 297 -262 297q-129 0 -189.5 -66.5t-72.5 -230.5z " />
+<glyph unicode="&#xe7;" horiz-adv-x="966" d="M119 512q0 256 124 391t357 135q156 0 301 -57v-178q-142 67 -285 67q-141 0 -221 -72.5t-80 -285.5q0 -113 23.5 -187.5t70 -110.5t94.5 -48t113 -12q147 0 285 65v-178q-104 -41 -223 -53l-139 -254h-189l182 254q-201 18 -307 152t-106 372z" />
+<glyph unicode="&#xe8;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM346 1395h184l127 -240h-116z" />
+<glyph unicode="&#xe9;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM506 1155l127 240h184l-194 -240h-117z" />
+<glyph unicode="&#xea;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM350 1155l180 250h99l182 -250h-145l-86 133l-86 -133h-144z" />
+<glyph unicode="&#xeb;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM336 1155v174h186v-174h-186zM639 1155v174h186v-174h-186z" />
+<glyph unicode="&#xec;" horiz-adv-x="589" d="M41 1395h184l127 -240h-116zM201 0h188v1022h-188v-1022z" />
+<glyph unicode="&#xed;" horiz-adv-x="589" d="M201 0v1022h188v-1022h-188zM236 1155l126 240h185l-195 -240h-116z" />
+<glyph unicode="&#xee;" horiz-adv-x="589" d="M63 1155l181 250h98l182 -250h-145l-86 133l-86 -133h-144zM201 0h188v1022h-188v-1022z" />
+<glyph unicode="&#xef;" horiz-adv-x="589" d="M49 1155v174h187v-174h-187zM201 0h188v1022h-188v-1022zM352 1155v174h187v-174h-187z" />
+<glyph unicode="&#xf0;" horiz-adv-x="1212" d="M119 453q0 182 108.5 310t347.5 128q170 0 285 -72q-41 193 -162 293l-217 -131l-67 113l151 90q-63 22 -149 22h-8q-74 0 -156 -18v172q81 22 178 22h8q184 0 322 -79l178 108l68 -113l-136 -81q193 -197 193 -590q0 -643 -473 -643q-229 0 -350 138t-121 331zM309 453 q0 -43 9.5 -85t35 -94.5t87 -85t149.5 -32.5q39 0 64.5 3t70.5 26.5t72.5 68.5t50 138t24.5 224q-106 100 -282 101q-281 0 -281 -264z" />
+<glyph unicode="&#xf1;" horiz-adv-x="1198" d="M168 0v934q213 106 436 106q193 0 302.5 -101t109.5 -280v-659h-189v672q0 90 -46 142t-185 52q-125 0 -240 -37v-829h-188zM281 1153v135q57 68 123 74q11 1 22 1q55 0 106 -25l122 -61q50 -25 103 -25q12 0 23 2q66 6 123 73v-135q-57 -68 -123 -74q-11 -1 -22 -1 q-53 0 -104 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -123 -74z" />
+<glyph unicode="&#xf2;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM319 1395h185l127 -240h-117z" />
+<glyph unicode="&#xf3;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM532 1155l127 240h185l-195 -240h-117z" />
+<glyph unicode="&#xf4;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM350 1155l180 250h99l182 -250h-145l-86 133l-86 -133h-144z" />
+<glyph unicode="&#xf5;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM270 1153v135q57 68 124 74q11 1 22 1q55 0 106 -25l122 -61 q50 -25 103 -25q12 0 23 2q66 6 123 73v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -124 -74zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5 t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5z" />
+<glyph unicode="&#xf6;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM336 1155v174h186v-174h-186zM639 1155v174h186v-174h-186z" />
+<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M227 453v151h832v-151h-832zM549 143v174h186v-174h-186zM549 739v174h186v-174h-186z" />
+<glyph unicode="&#xf8;" horiz-adv-x="1175" d="M55 0l135 160q-90 131 -90 350q0 156 43 264.5t117 162.5t151.5 76.5t170.5 22.5q170 0 284 -73l52 59h190l-135 -162q90 -131 90 -350q0 -156 -43 -264.5t-117 -162.5t-151.5 -76.5t-169.5 -22.5q-172 0 -287 73l-49 -57h-191zM289 510q0 -119 26 -199l441 520 q-61 39 -174 39q-66 0 -112 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM406 188q59 -38 170 -38h6q66 0 111.5 12t90.5 48t67.5 110.5t22.5 189.5q0 125 -28 201z" />
+<glyph unicode="&#xf9;" horiz-adv-x="1208" d="M168 430v592h188v-606q0 -139 58.5 -204t185.5 -65t186.5 64.5t59.5 204.5v606h186v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM358 1395h185l127 -240h-117z" />
+<glyph unicode="&#xfa;" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM528 1155l127 240h185l-195 -240h-117z" />
+<glyph unicode="&#xfb;" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM369 1155l180 250h98l182 -250h-145l-86 133l-86 -133h-143z" />
+<glyph unicode="&#xfc;" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM354 1155v174h187v-174h-187zM657 1155v174h187v-174h-187z" />
+<glyph unicode="&#xfd;" horiz-adv-x="1073" d="M63 1022h201l275 -766l270 766h201l-420 -1131q-109 -294 -317 -294h-1q-68 0 -143 16v168q61 -16 115 -17q127 0 205 236zM467 1155l127 240h184l-194 -240h-117z" />
+<glyph unicode="&#xfe;" horiz-adv-x="1142" d="M170 -377v1774h186v-404q106 31 219 31q88 0 164 -21.5t149 -75t114.5 -159.5t41.5 -258t-41.5 -258.5t-114.5 -159.5t-148.5 -74.5t-164.5 -21.5q-113 0 -219 31v-404h-186zM356 205q104 -43 219 -43q61 0 106.5 12t88.5 46t65.5 106.5t22.5 183.5t-22.5 183.5 t-65.5 106.5t-88 46t-107 12q-115 0 -219 -43v-610z" />
+<glyph unicode="&#xff;" horiz-adv-x="1069" d="M63 1022h201l275 -766l270 766h201l-420 -1131q-109 -294 -317 -294h-1q-68 0 -143 16v168q61 -16 115 -17q127 0 205 236zM291 1155v174h186v-174h-186zM594 1155v174h186v-174h-186z" />
+<glyph unicode="&#x152;" horiz-adv-x="2056" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q152 0 272 -45v27h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891v29q-121 -45 -272 -45q-94 0 -177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5t103.5 -88 t117.5 -42t127 -9q174 0 272 67v922q-97 67 -267 67h-5q-72 0 -127 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5z" />
+<glyph unicode="&#x153;" horiz-adv-x="1886" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170.5 22.5q256 0 381 -159q112 163 359 163h3q205 0 328 -134t123 -359q0 -74 -7 -141h-704q35 -260 289 -261q201 0 352 74v-168q-158 -67 -357 -67h-6q-260 0 -378 166q-121 -166 -383 -166q-92 0 -170 22.5t-152 76.5 t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM1063 575h524q-27 297 -262 297q-129 0 -189.5 -66.5 t-72.5 -230.5z" />
+<glyph unicode="&#x178;" horiz-adv-x="1277" d="M25 1366h229l383 -559l383 559h231l-520 -754v-612h-188v610zM393 1483v174h187v-174h-187zM696 1483v174h187v-174h-187z" />
+<glyph unicode="&#x2c6;" horiz-adv-x="882" d="M207 1155l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
+<glyph unicode="&#x2dc;" horiz-adv-x="1079" d="M215 1153v135q57 68 124 74q11 1 22 1q55 0 105 -25l122 -61q50 -25 103 -25q12 0 23 2q66 6 124 73v-135q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74z" />
+<glyph unicode="&#x2000;" horiz-adv-x="887" />
+<glyph unicode="&#x2001;" horiz-adv-x="1774" />
+<glyph unicode="&#x2002;" horiz-adv-x="887" />
+<glyph unicode="&#x2003;" horiz-adv-x="1774" />
+<glyph unicode="&#x2004;" horiz-adv-x="591" />
+<glyph unicode="&#x2005;" horiz-adv-x="443" />
+<glyph unicode="&#x2006;" horiz-adv-x="295" />
+<glyph unicode="&#x2007;" horiz-adv-x="295" />
+<glyph unicode="&#x2008;" horiz-adv-x="221" />
+<glyph unicode="&#x2009;" horiz-adv-x="354" />
+<glyph unicode="&#x200a;" horiz-adv-x="98" />
+<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
+<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
+<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 516v160h860v-160h-860z" />
+<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 516v160h1501v-160h-1501z" />
+<glyph unicode="&#x2018;" horiz-adv-x="552" d="M96 1384h193l151 -401h-172z" />
+<glyph unicode="&#x2019;" horiz-adv-x="552" d="M96 983l152 401h192l-172 -401h-172z" />
+<glyph unicode="&#x201a;" horiz-adv-x="643" d="M147 -229l152 401h193l-173 -401h-172z" />
+<glyph unicode="&#x201c;" horiz-adv-x="843" d="M96 1384h193l151 -401h-172zM401 1384h193l151 -401h-172z" />
+<glyph unicode="&#x201d;" horiz-adv-x="827" d="M96 983l152 401h192l-172 -401h-172zM401 983l152 401h192l-172 -401h-172z" />
+<glyph unicode="&#x201e;" horiz-adv-x="940" d="M147 -229l152 401h193l-173 -401h-172zM453 -229l151 401h193l-172 -401h-172z" />
+<glyph unicode="&#x2022;" horiz-adv-x="864" d="M170 684q0 104 73.5 177t178.5 73q102 0 176 -73t74 -177t-74 -178t-176 -74q-104 0 -178 74t-74 178z" />
+<glyph unicode="&#x2026;" horiz-adv-x="1607" d="M219 0v174h187v-174h-187zM711 0v174h186v-174h-186zM1200 0v174h186v-174h-186z" />
+<glyph unicode="&#x202f;" horiz-adv-x="354" />
+<glyph unicode="&#x2039;" horiz-adv-x="733" d="M63 522l347 389h223l-365 -389l365 -391h-223z" />
+<glyph unicode="&#x203a;" horiz-adv-x="792" d="M137 131l365 391l-365 389h223l347 -389l-347 -391h-223z" />
+<glyph unicode="&#x205f;" horiz-adv-x="443" />
+<glyph unicode="&#x20ac;" horiz-adv-x="1400" d="M102 451v141h142q-2 29 -2 92q0 33 4 94h-144v142h162q29 135 94.5 231t153.5 144t176 68.5t189 20.5q213 0 417 -81v-177q-192 86 -395 86q-184 0 -290.5 -70.5t-145.5 -221.5h614v-142h-635q-4 -61 -4 -94q0 -63 4 -92h635v-141h-614q39 -154 145.5 -224.5t290.5 -70.5 q203 0 395 86v-176q-205 -82 -417 -82q-512 0 -613 467h-162z" />
+<glyph unicode="&#x2122;" horiz-adv-x="1568" d="M100 1278v88h555v-88h-231v-594h-94v594h-230zM762 684v682h102l223 -295l226 295h102v-682h-94v539l-234 -308l-231 308v-539h-94z" />
+<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
+<glyph unicode="&#xfb01;" horiz-adv-x="1202" d="M82 858v166h143v47q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-47h264v-166h-264v-858h-187v858h-143zM874 1180v198h201v-198h-201zM881 0v1022h188v-1022h-188z" />
+<glyph unicode="&#xfb02;" horiz-adv-x="1220" d="M82 858v166h143v47q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-47h264v-166h-264v-858h-187v858h-143zM881 0v1411h188v-1411h-188z" />
+<glyph unicode="&#xfb03;" horiz-adv-x="1939" d="M82 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM757 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5 t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM1545 1180v198h200v-198h-200zM1551 0v1022h188v-1022h-188z" />
+<glyph unicode="&#xfb04;" horiz-adv-x="1952" d="M82 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM757 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5 t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM1557 0v1411h188v-1411h-188z" />
+</font>
+</defs></svg> 
\ No newline at end of file
diff --git a/motioneye/static/fnt/mavenpro-medium-webfont.ttf b/motioneye/static/fnt/mavenpro-medium-webfont.ttf
new file mode 100644 (file)
index 0000000..a73e903
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-medium-webfont.ttf differ
diff --git a/motioneye/static/fnt/mavenpro-medium-webfont.woff b/motioneye/static/fnt/mavenpro-medium-webfont.woff
new file mode 100644 (file)
index 0000000..9a0ed4d
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-medium-webfont.woff differ
diff --git a/motioneye/static/fnt/mavenpro-regular-webfont.eot b/motioneye/static/fnt/mavenpro-regular-webfont.eot
new file mode 100644 (file)
index 0000000..b41ecdd
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-regular-webfont.eot differ
diff --git a/motioneye/static/fnt/mavenpro-regular-webfont.svg b/motioneye/static/fnt/mavenpro-regular-webfont.svg
new file mode 100644 (file)
index 0000000..074ad9f
--- /dev/null
@@ -0,0 +1,243 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="maven_proregular" horiz-adv-x="1529" >
+<font-face units-per-em="2048" ascent="1638" descent="-410" />
+<missing-glyph horiz-adv-x="692" />
+<glyph horiz-adv-x="2048" />
+<glyph horiz-adv-x="2048" />
+<glyph unicode="&#xd;" horiz-adv-x="682" />
+<glyph unicode=" "  horiz-adv-x="692" />
+<glyph unicode="&#x09;" horiz-adv-x="692" />
+<glyph unicode="&#xa0;" horiz-adv-x="692" />
+<glyph unicode="!" horiz-adv-x="761" d="M303 0v164h164v-164h-164zM311 1366h148l-15 -1022h-118z" />
+<glyph unicode="&#x22;" horiz-adv-x="780" d="M166 1384h170l-35 -411h-98zM440 1384h170l-37 -411h-98z" />
+<glyph unicode="#" horiz-adv-x="1210" d="M145 258v123h189l55 326h-186v122h209l41 242h125l-41 -242h270l43 242h125l-43 -242h147v-122h-168l-57 -326h166v-123h-186l-37 -219h-125l37 219h-271l-39 -219h-125l39 219h-168zM459 381h270l57 326h-272z" />
+<glyph unicode="$" horiz-adv-x="1181" d="M154 1004q0 143 95 249.5t294 126.5v209h110v-205q166 -8 314 -81v-152q-150 84 -314 86v-469q88 -29 147.5 -55.5t124 -73.5t97.5 -120t33 -171q0 -156 -105.5 -250t-296.5 -110v-209h-110v205q-207 6 -377 96v160q158 -104 377 -109v520q-76 23 -117 37t-101.5 44 t-92 63.5t-55 87t-23.5 121.5zM301 1004q0 -78 61.5 -121t180.5 -80v428q-242 -34 -242 -227zM653 135q254 33 254 213q0 106 -65.5 165.5t-188.5 102.5v-481z" />
+<glyph unicode="%" horiz-adv-x="1777" d="M145 983q0 111 27 189.5t73 116.5t93 54.5t104 16.5q123 0 209 -83t86 -294q0 -113 -26.5 -190.5t-71.5 -116.5t-93 -55.5t-104 -16.5q-57 0 -104 16.5t-93 55.5t-73 116.5t-27 190.5zM258 983q0 -162 50 -217t134 -55t133.5 55t49.5 217t-49.5 216t-133.5 54 q-86 0 -135 -54t-49 -216zM395 0l858 1366h131l-858 -1366h-131zM1042 385q0 113 27 190.5t73 116.5t93 55.5t104 16.5q55 0 103.5 -16.5t93.5 -55.5t71.5 -116.5t26.5 -190.5t-26.5 -190.5t-71.5 -116.5t-93.5 -55.5t-103.5 -16.5q-57 0 -104 16.5t-93 55.5t-73 116.5 t-27 190.5zM1155 385q0 -162 50 -217t134 -55t133.5 55t49.5 217t-49.5 217t-133.5 55t-134 -55t-50 -217z" />
+<glyph unicode="&#x26;" horiz-adv-x="1310" d="M111 377q0 119 56 220t220 191q-135 168 -135 283q0 127 91 218t218 91q131 0 221 -87t90 -214q0 -109 -59 -183.5t-192 -152.5l297 -354q57 100 83 205h134q-33 -177 -121 -322l227 -272h-192l-125 152q-162 -168 -385 -168q-111 0 -198 38.5t-134 101t-71.5 127 t-24.5 126.5zM256 381q0 -33 12.5 -71t42 -82t90 -72.5t144.5 -28.5q172 0 291 129l-357 428q-78 -41 -128 -85t-68.5 -87t-22.5 -67.5t-4 -63.5zM395 1069q0 -72 129 -213q131 70 171 116t40 105q0 68 -49 112t-121 44q-70 0 -120 -48t-50 -116z" />
+<glyph unicode="'" horiz-adv-x="509" d="M166 1384h170l-35 -411h-98z" />
+<glyph unicode="(" horiz-adv-x="706" d="M190 625q1 450 228 858h155q-29 -47 -59.5 -112.5t-74.5 -180.5t-72.5 -264.5t-28.5 -300.5q0 -150 28.5 -300.5t72.5 -264t76 -180t58 -111.5h-155q-227 408 -228 856z" />
+<glyph unicode=")" horiz-adv-x="706" d="M133 -231q29 47 59.5 111.5t75.5 180t74 265t29 301.5q0 195 -53.5 396.5t-98.5 296.5t-86 163h158q227 -408 227 -856q0 -451 -227 -858h-158z" />
+<glyph unicode="*" horiz-adv-x="841" d="M172 1112l178 78l-178 80l55 94l156 -111l-21 187h111l-16 -187l153 111l56 -94l-177 -80l177 -78l-56 -98l-153 112l16 -184h-111l21 184l-156 -112z" />
+<glyph unicode="+" horiz-adv-x="1128" d="M199 551v123h303v303h123v-303h301v-123h-301v-301h-123v301h-303z" />
+<glyph unicode="," horiz-adv-x="641" d="M152 -229l151 401h168l-172 -401h-147z" />
+<glyph unicode="-" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
+<glyph unicode="." horiz-adv-x="671" d="M260 0v164h164v-164h-164z" />
+<glyph unicode="/" horiz-adv-x="827" d="M-29 -201l752 1749h135l-754 -1749h-133z" />
+<glyph unicode="0" horiz-adv-x="1345" d="M133 677.5q0 700.5 538.5 700.5t538.5 -700.5t-538.5 -700.5t-538.5 700.5zM281 678q0 -145 23.5 -250.5t60 -165t92 -94.5t104.5 -45t110.5 -10t110.5 10t104.5 45t92.5 94.5t60.5 164.5t23.5 251q0 178 -34 298t-95.5 174t-121.5 73.5t-140 19.5t-140.5 -19.5 t-122 -73.5t-95 -174t-33.5 -298z" />
+<glyph unicode="1" horiz-adv-x="923" d="M211 1032v174l252 160h168v-1366h-148v1212z" />
+<glyph unicode="2" horiz-adv-x="1189" d="M158 0v129q102 76 208.5 169t223.5 215t189.5 252t72.5 234q0 238 -322 238q-190 0 -358 -119v162q172 104 371 104q233 0 344.5 -112.5t111.5 -272.5q0 -111 -56 -232.5t-151.5 -236t-190 -206t-200.5 -177.5h619v-147h-862z" />
+<glyph unicode="3" horiz-adv-x="1193" d="M166 70v151q160 -90 346 -90q182 0 278.5 67.5t96.5 194.5t-98.5 194.5t-284.5 67.5h-150v142h156q150 0 228.5 56t78.5 162.5t-83 164t-240 57.5q-152 0 -295 -72v150q145 69 306 69h5q233 0 344 -108.5t111 -259.5q0 -172 -148 -260l-29 -17q10 -4 26.5 -12t58.5 -39 t75 -67.5t59.5 -99t26.5 -132.5q0 -78 -27.5 -147.5t-84 -129t-157.5 -94t-235 -34.5q-197 0 -364 86z" />
+<glyph unicode="4" horiz-adv-x="1210" d="M78 410v157l645 799h166v-819h194v-137h-194v-410h-148v410h-663zM242 547h499v608z" />
+<glyph unicode="5" horiz-adv-x="1202" d="M178 735l70 631h702v-145h-575l-39 -406q121 37 266 37q84 0 153.5 -18.5t136 -63.5t105.5 -135t39 -217t-39 -217t-105.5 -135.5t-136 -63.5t-153.5 -18q-236 0 -391 84v153q150 -94 381 -94q80 0 138 19.5t89 46t47.5 69.5t20.5 77t4 79q0 92 -24.5 152.5t-71.5 88 t-93.5 37t-109.5 9.5q-96 0 -177 -16.5t-112 -33.5l-33 -14z" />
+<glyph unicode="6" horiz-adv-x="1193" d="M133 625q0 141 25.5 257.5t65.5 193.5t96.5 135.5t111.5 90t118.5 51t111 25.5t94.5 6q109 0 198 -26v-137q-98 24 -176 24h-6q-47 0 -92 -6t-113.5 -32.5t-122 -73t-97.5 -136.5t-58 -213q133 90 309 90q215 0 340 -115.5t125 -314.5q0 -84 -25.5 -162.5t-78 -147 t-145.5 -109.5t-216 -41q-465 0 -465 641zM281 621q2 -141 27.5 -241.5t57 -149t83 -75t79 -29.5t70.5 -3q102 0 172 38t98.5 98t37.5 103.5t9 83.5q0 139 -81.5 215t-235.5 76q-201 0 -317 -116z" />
+<glyph unicode="7" horiz-adv-x="1175" d="M143 1219v147h932v-137l-592 -1229h-164l596 1219h-772z" />
+<glyph unicode="8" horiz-adv-x="1191" d="M125 406q0 203 184 307q51 25 51 24l-20 11q-12 6 -44 30.5t-56.5 55t-44 84t-19.5 112.5q0 145 103.5 249.5t318.5 104.5t318.5 -103t103.5 -251q0 -184 -148 -274l-30 -17q10 -4 25.5 -11t54.5 -34.5t69.5 -62.5t55 -95.5t24.5 -129.5q0 -78 -25.5 -149t-79 -134.5 t-148.5 -101t-220 -37.5q-240 0 -356.5 127t-116.5 295zM272 406q0 -27 4.5 -55.5t23.5 -71.5t52 -76t96.5 -56.5t149.5 -23.5t149.5 23.5t96.5 56.5t52 76t23.5 71.5t4.5 55.5q0 123 -84 188t-242 65t-242 -65.5t-84 -187.5zM324 1030q0 -111 72.5 -173t201.5 -62 t201.5 62.5t72.5 172.5q0 209 -274 209t-274 -209z" />
+<glyph unicode="9" horiz-adv-x="1202" d="M119 924q0 84 25.5 162.5t77.5 147.5t145.5 109.5t216.5 40.5q465 0 465 -643q0 -195 -48.5 -340t-116 -223t-160.5 -124t-160.5 -58t-137.5 -12q-104 0 -199 24v139q81 -24 175 -24h8q47 0 92 6t113.5 32.5t120.5 73t97.5 136.5t57.5 213q-121 -90 -307 -90 q-215 0 -340 115.5t-125 314.5zM266 922q0 -139 82 -215t236 -76q207 0 317 117q-2 141 -27.5 241.5t-57.5 148.5t-83 74.5t-78.5 29.5t-70.5 3q-102 0 -172 -38t-98.5 -98t-38 -103t-9.5 -84z" />
+<glyph unicode=":" horiz-adv-x="800" d="M324 98v164h163v-164h-163zM324 754v164h163v-164h-163z" />
+<glyph unicode=";" horiz-adv-x="854" d="M199 -229l151 401h168l-172 -401h-147zM352 754v164h164v-164h-164z" />
+<glyph unicode="&#x3c;" horiz-adv-x="1351" d="M246 481v140l844 383v-164l-672 -289l672 -291v-162z" />
+<glyph unicode="=" horiz-adv-x="1306" d="M260 381v143h789v-143h-789zM260 698v142h789v-142h-789z" />
+<glyph unicode="&#x3e;" horiz-adv-x="1329" d="M270 98v162l670 291l-670 289v164l842 -383v-140z" />
+<glyph unicode="?" horiz-adv-x="993" d="M164 1145v160q113 77 285 77q61 0 120.5 -14t129 -50t112.5 -117t43 -197q0 -82 -31.5 -142.5t-76.5 -98.5l-91 -76q-45 -38 -76.5 -101.5t-31.5 -151.5v-90h-148v100q0 104 32 181t77 120t90 78t77 79t32 102q0 231 -269 231q-156 0 -274 -90zM391 0v164h164v-164h-164z " />
+<glyph unicode="@" horiz-adv-x="1869" d="M170 371q0 244 133 445.5t334 309t410 107.5q82 0 171 -21.5t180 -73t162.5 -126t116.5 -189t45 -254.5q0 -221 -91 -362t-242 -141q-137 0 -191 100q-96 -100 -295 -100q-143 0 -225 62t-82 171q0 49 16.5 94t59.5 94.5t138 79t230 29.5q74 0 152 -10q2 14 2 43 q0 35 -6 59.5t-23.5 50t-60.5 39t-109 13.5q-143 0 -256 -43l17 118q123 41 239 41q156 0 237 -70.5t81 -207.5q0 -59 -11.5 -142.5t-17.5 -137.5q-4 -40 -4 -81v-26q2 -54 30.5 -79.5t86.5 -25.5q90 0 169 114t79 312q0 145 -55.5 262t-144.5 186.5t-191.5 106.5t-206.5 37 q-311 0 -554 -230.5t-243 -553.5q0 -154 51 -270.5t123 -177t163 -98.5t153.5 -47t113.5 -9q254 0 430 100l-18 -92q-172 -82 -428 -82q-86 0 -174.5 22.5t-178.5 74.5t-158.5 127t-112.5 192.5t-44 259.5zM715 300q0 -116 188 -116q49 0 88 8.5t65.5 17.5t47 31.5t31 37 t18.5 46t11 46t6.5 51.5t5.5 47q-55 8 -136 8q-325 0 -325 -177z" />
+<glyph unicode="A" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625z" />
+<glyph unicode="B" horiz-adv-x="1271" d="M178 0v1366h453q454 0 454 -365q0 -57 -15 -106t-36.5 -78t-42 -50.5t-37.5 -29.5l-14 -10q10 -2 24.5 -8t54.5 -33t69.5 -61.5t54 -98t24.5 -139.5q0 -387 -495 -387h-494zM326 137h358q156 0 246 54.5t90 203.5q0 63 -21.5 108.5t-52.5 71t-80 39t-91 17.5t-97 4h-352 v-498zM326 774h338q125 0 199.5 43t74.5 172q0 133 -72.5 184.5t-220.5 51.5h-319v-451z" />
+<glyph unicode="C" horiz-adv-x="1286" d="M125 684q0 700 633 700q219 0 416 -79v-142q-195 82 -400 82q-260 0 -381 -135t-121 -426t121 -426t381 -135q209 0 400 80v-142q-196 -77 -411 -77h-5q-633 0 -633 700z" />
+<glyph unicode="D" horiz-adv-x="1372" d="M178 0v1366h428q641 0 641 -682q0 -684 -641 -684h-428zM326 139h270q262 0 383 131t121 414t-121 414t-383 131h-270v-1090z" />
+<glyph unicode="E" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862z" />
+<glyph unicode="F" horiz-adv-x="1097" d="M178 0v1366h838v-139h-690v-471h606v-144h-606v-612h-148z" />
+<glyph unicode="G" horiz-adv-x="1386" d="M125 684q0 700 633 700q227 0 430 -86v-143q-199 88 -414 88q-260 0 -381 -135t-121 -424q0 -291 121 -425t381 -134q164 0 313 49v426h-329v143h477v-653q-221 -106 -477 -106q-633 0 -633 700z" />
+<glyph unicode="H" horiz-adv-x="1380" d="M178 0v1366h148v-612h727v612h147v-1366h-147v612h-727v-612h-148z" />
+<glyph unicode="I" horiz-adv-x="501" d="M178 0v1366h148v-1366h-148z" />
+<glyph unicode="J" horiz-adv-x="915" d="M59 55v154q117 -82 252 -82q139 0 200.5 68.5t61.5 224.5v946h148v-958q0 -125 -35 -213.5t-95.5 -131.5t-124 -61t-136.5 -18q-147 0 -271 71z" />
+<glyph unicode="K" horiz-adv-x="1294" d="M178 0v1366h148v-704l632 704h199l-586 -639l633 -727h-194l-535 623l-149 -164v-459h-148z" />
+<glyph unicode="L" horiz-adv-x="1067" d="M178 0v1366h148v-1223h698v-143h-846z" />
+<glyph unicode="M" horiz-adv-x="1619" d="M178 0v1366h160l469 -694l471 694h160v-1366h-148v1124l-483 -696l-481 696v-1124h-148z" />
+<glyph unicode="N" horiz-adv-x="1490" d="M178 0v1366h168l815 -1151v1151h148v-1366h-166l-817 1151v-1151h-148z" />
+<glyph unicode="O" d="M125 684q0 700 633 700q635 0 635 -700t-635 -700q-633 0 -633 700zM272 684q0 -291 117 -426t369 -135q254 0 370.5 135t116.5 426t-116.5 426t-370.5 135q-252 0 -369 -135t-117 -426z" />
+<glyph unicode="P" horiz-adv-x="1220" d="M176 0v1366h486q229 0 351 -108.5t122 -313.5t-122 -313.5t-351 -108.5h-338v-522h-148zM324 664h319q109 0 181.5 23.5t105.5 68.5t45 88t12 100t-12 100t-45 88.5t-105.5 69t-181.5 23.5h-319v-561z" />
+<glyph unicode="Q" d="M125 684q0 700 633 700q635 0 635 -700q0 -651 -553 -698q41 -170 268 -176v-134q-113 0 -196 30t-127 81t-65.5 98t-29.5 101q-565 37 -565 698zM272 684q0 -291 117 -426t369 -135q254 0 370.5 135t116.5 426t-116.5 426t-370.5 135q-252 0 -369 -135t-117 -426z" />
+<glyph unicode="R" horiz-adv-x="1271" d="M178 0v1366h502q223 0 343 -101.5t120 -295.5q0 -88 -28 -160t-63.5 -112t-84.5 -69.5t-80 -39t-58 -15.5l375 -573h-174l-356 551h-348v-551h-148zM326 690h346q102 0 170.5 23.5t99.5 65.5t42 83t11 96.5t-11 96.5t-42 83t-99.5 65.5t-170.5 23.5h-346v-537z" />
+<glyph unicode="S" horiz-adv-x="1150" d="M125 1004q0 158 114.5 269t356.5 111q174 0 342 -81v-152q-152 86 -328 86q-164 0 -251 -60t-87 -173q0 -70 56.5 -117t139.5 -69.5t181.5 -57.5t181 -76t139 -127t56.5 -209q0 -170 -128 -267t-357 -97q-223 0 -404 96v160q162 -109 393 -109q348 0 349 217 q0 92 -56.5 154.5t-139.5 91.5l-181 60q-98 32 -181 65t-139.5 105.5t-56.5 179.5z" />
+<glyph unicode="T" horiz-adv-x="1163" d="M25 1223v143h1112v-143h-482v-1223h-147v1223h-483z" />
+<glyph unicode="U" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5z" />
+<glyph unicode="V" horiz-adv-x="1251" d="M25 1366h157l441 -1155l438 1155h158l-525 -1366h-145z" />
+<glyph unicode="W" horiz-adv-x="1947" d="M25 1366h155l359 -1143l360 1143h148l358 -1143l360 1143h156l-440 -1366h-150l-358 1114l-359 -1114h-151z" />
+<glyph unicode="X" horiz-adv-x="1206" d="M25 0l485 692l-473 674h176l387 -549l385 549h180l-475 -676l484 -690h-177l-397 563l-395 -563h-180z" />
+<glyph unicode="Y" horiz-adv-x="1224" d="M25 1366h178l405 -596l404 596h178l-508 -739v-627h-147v627z" />
+<glyph unicode="Z" horiz-adv-x="1247" d="M131 0v129l780 1090h-759v147h952v-125l-785 -1094h785v-147h-973z" />
+<glyph unicode="[" horiz-adv-x="784" d="M244 -238v1663h370v-139h-223v-1384h223v-140h-370z" />
+<glyph unicode="\" horiz-adv-x="753" d="M-76 1548h135l752 -1749h-133z" />
+<glyph unicode="]" horiz-adv-x="780" d="M172 -98h221v1384h-221v139h369v-1663h-369v140z" />
+<glyph unicode="^" horiz-adv-x="1146" d="M274 969l213 444h113l213 -444h-123l-145 325l-148 -325h-123z" />
+<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-90h-1118v90z" />
+<glyph unicode="`" horiz-adv-x="448" d="M94 1391h150l116 -236h-96z" />
+<glyph unicode="a" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -317.5v-307q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203z" />
+<glyph unicode="b" horiz-adv-x="1167" d="M170 307v1106h147v-414q123 41 252 41q70 0 131.5 -12t125 -47t109.5 -90t74.5 -149.5t28.5 -217.5q0 -135 -31.5 -236.5t-77.5 -157.5t-110.5 -91t-119 -45t-111.5 -10q-111 0 -193 27.5t-123 63.5t-66.5 86t-30.5 82.5t-5 63.5zM317 299q1 -180 271 -180q57 0 103 13 t95.5 51t77 125t27.5 216q0 199 -76 289t-246 90q-127 0 -252 -51v-553z" />
+<glyph unicode="c" horiz-adv-x="966" d="M119 512q0 156 43 265.5t116.5 163.5t150.5 76.5t169 22.5q154 0 303 -57v-141q-139 57 -287 57q-182 0 -265 -92t-83 -295q0 -205 83 -296t265 -91q154 0 287 57v-143q-143 -55 -303 -55q-72 0 -134.5 12t-128 48t-111.5 92.5t-75.5 152.5t-29.5 223z" />
+<glyph unicode="d" horiz-adv-x="1150" d="M129 524q0 152 41 258.5t113.5 159.5t147.5 74.5t165 21.5q135 0 254 -39v412h147v-1106q0 -41 -12 -84t-50 -102.5t-130 -97t-227 -37.5q-57 0 -111.5 10t-118 45t-109.5 91t-78 157.5t-32 236.5zM276 524q0 -129 28 -216t77 -125t94 -51t103 -13q272 0 272 180v553 q-125 51 -254 51q-168 0 -244 -90t-76 -289z" />
+<glyph unicode="e" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -109q8 -62 8 -78v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267z" />
+<glyph unicode="f" horiz-adv-x="675" d="M82 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176z" />
+<glyph unicode="g" horiz-adv-x="1101" d="M123 516q0 160 43 271.5t115.5 162.5t138 70.5t137.5 19.5q131 0 220 -36.5t125 -94t48.5 -99.5t12.5 -83v-770q0 -68 -19.5 -125t-64.5 -111.5t-137.5 -85t-223.5 -30.5q-143 0 -291 37v139q131 -37 275 -37q98 0 164.5 17.5t96.5 50t41 64.5t11 77v102 q-115 -37 -240 -37q-68 0 -127 11.5t-120.5 45.5t-105.5 88t-71.5 144t-27.5 209zM270 516q0 -115 24.5 -189.5t72 -110.5t95.5 -48t113 -12q123 0 240 47v530q0 172 -258 172q-55 0 -98 -13t-90 -50t-73 -120t-26 -206z" />
+<glyph unicode="h" horiz-adv-x="1181" d="M193 0v1411h147v-436q155 65 309 65q188 0 290.5 -113.5t102.5 -320.5v-606h-147v606q0 287 -262 287q-143 0 -293 -62v-831h-147z" />
+<glyph unicode="i" horiz-adv-x="540" d="M193 1204v162h163v-162h-163zM201 0h147v1022h-147v-1022z" />
+<glyph unicode="j" horiz-adv-x="514" d="M6 -258q127 2 168 50t41 157v1073h147v-1081q0 -336 -356 -336v137zM207 1204v162h164v-162h-164z" />
+<glyph unicode="k" horiz-adv-x="1124" d="M188 0v1411h148v-891l459 502h200l-436 -469l492 -553h-197l-391 449l-127 -138v-311h-148z" />
+<glyph unicode="l" horiz-adv-x="552" d="M207 0v1411h147v-1411h-147z" />
+<glyph unicode="m" horiz-adv-x="1824" d="M193 0v942q197 98 409 98q168 0 270 -84q197 84 379 84q186 0 288.5 -98t102.5 -276v-666h-147v666q0 43 -8 76.5t-32.5 71.5t-79 58.5t-134.5 20.5q-129 0 -283 -51q33 -74 33 -176v-666h-147v666q0 45 -8.5 78.5t-33 70.5t-80.5 57.5t-138 20.5q-125 0 -244 -37v-856 h-147z" />
+<glyph unicode="n" horiz-adv-x="1189" d="M168 0v940q201 100 432 100q193 0 299.5 -98t106.5 -276v-666h-148v666q0 43 -9 76.5t-35 71.5t-84 58.5t-146 20.5q-129 0 -269 -41v-852h-147z" />
+<glyph unicode="o" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t150.5 76.5t169 22.5t170 -22.5t151.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-151.5 -76.5t-170 -22.5t-169 22.5t-150.5 76.5t-117 163.5t-43 265.5zM248 512q0 -205 79 -299t253 -94t253.5 93t79.5 300t-79.5 300 t-253.5 93t-253 -94t-79 -299z" />
+<glyph unicode="p" horiz-adv-x="1159" d="M168 -379v1096q0 41 12.5 84t50 103.5t130 98t227.5 37.5q57 0 111.5 -10t118 -45t109.5 -92t77.5 -158.5t31.5 -236.5q0 -152 -41 -258.5t-113.5 -159.5t-147.5 -74.5t-165 -21.5q-125 0 -254 39v-402h-147zM315 172q117 -51 254 -51q168 0 244 89t76 290 q0 129 -27.5 215t-77 125t-94.5 52t-102 13q-272 0 -273 -182v-551z" />
+<glyph unicode="q" horiz-adv-x="1150" d="M115 498q0 135 31.5 236.5t77.5 158.5t109.5 92t118 45t111.5 10q111 0 193 -27.5t123 -63.5t66.5 -86t30.5 -82.5t5 -63.5v-1096h-147v402q-117 -39 -252 -39q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM262 500q0 -201 76 -290t244 -89q135 0 252 51v551 q0 182 -271 182q-57 0 -102 -13t-94.5 -52t-77 -125t-27.5 -215z" />
+<glyph unicode="r" horiz-adv-x="702" d="M166 0v940q231 98 496 98v-141q-168 -4 -349 -53v-844h-147z" />
+<glyph unicode="s" horiz-adv-x="872" d="M100 743q0 123 89.5 210t273.5 87q131 0 262 -63v-148q-117 70 -246 70q-231 0 -231 -156q0 -53 56 -85.5t137 -54t162 -51.5t137.5 -101.5t56.5 -182.5q0 -133 -99.5 -208.5t-275.5 -75.5q-170 0 -309 77v156q147 -90 303 -90q233 0 233 141q0 76 -56 119t-137 64.5 t-162 47t-137.5 86t-56.5 158.5z" />
+<glyph unicode="t" horiz-adv-x="706" d="M49 887v135h178v289h148v-289h264v-135h-264v-623q0 -78 32.5 -109.5t114.5 -31.5q63 0 117 24v-139q-65 -24 -130 -24h-5q-41 0 -80 9t-88 35.5t-79 89t-30 154.5v615h-178z" />
+<glyph unicode="u" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5z" />
+<glyph unicode="v" horiz-adv-x="1062" d="M66 1022h157l309 -838l310 838h157l-393 -1022h-149z" />
+<glyph unicode="w" horiz-adv-x="1603" d="M78 1022h155l261 -791l235 791h141l238 -791l260 791h156l-340 -1022h-150l-233 764l-234 -764h-149z" />
+<glyph unicode="x" horiz-adv-x="1032" d="M57 0l365 518l-354 504h174l268 -379l268 379h180l-356 -504l-2 -4l363 -514h-177l-274 389l-274 -389h-181z" />
+<glyph unicode="y" horiz-adv-x="1028" d="M63 1022h158l291 -809l295 809h158l-418 -1122q-109 -295 -322 -295q-49 0 -96 18v135q41 -18 88 -18q39 0 77 28.5t62.5 68.5t45 81t28.5 70l10 28z" />
+<glyph unicode="z" horiz-adv-x="950" d="M92 0v133l557 748h-543v141h734v-129l-561 -752h561v-141h-748z" />
+<glyph unicode="{" horiz-adv-x="763" d="M186 551v100q129 33 129 148v319q0 150 79 229.5t229 79.5v-139q-82 -4 -121 -36.5t-39 -123.5v-329q0 -119 -119 -199q119 -80 119 -199v-329q0 -88 39 -122t121 -38v-137q-150 0 -229 78.5t-79 228.5v319q0 111 -129 150z" />
+<glyph unicode="|" horiz-adv-x="907" d="M379 -203v1565h147v-1565h-147z" />
+<glyph unicode="}" horiz-adv-x="753" d="M156 -88q82 4 120.5 38t38.5 122v329q0 117 121 199q-121 82 -121 199v329q0 90 -37.5 123t-121.5 37v139q150 0 228.5 -79.5t78.5 -229.5v-319q0 -115 129 -148v-100q-129 -39 -129 -150v-319q0 -150 -79 -228.5t-228 -78.5v137z" />
+<glyph unicode="~" horiz-adv-x="1024" d="M201 473v125q57 72 123 79q11 1 22 1q54 0 104 -27l124 -65q50 -27 103 -27q12 0 23 1q66 7 123 79v-123q-57 -72 -123 -79q-11 -1 -22 -1q-54 0 -104 27l-124 65q-50 25 -103 25q-12 0 -23 -1q-66 -7 -123 -79z" />
+<glyph unicode="&#xa1;" horiz-adv-x="741" d="M303 860v162h164v-162h-164zM311 -344l15 1022h118l15 -1022h-148z" />
+<glyph unicode="&#xa2;" horiz-adv-x="976" d="M117 682q0 270 116.5 390t294.5 136v158h103v-156q131 -2 266 -55v-141q-125 51 -266 55v-772q143 4 266 57v-143q-125 -49 -266 -55v-156h-103v158q-411 41 -411 524zM264 684q0 -178 63.5 -270t200.5 -111v762q-137 -20 -200.5 -111.5t-63.5 -269.5z" />
+<glyph unicode="&#xa3;" horiz-adv-x="1136" d="M104 -31v150q80 39 177 45v411h-177v136h177v293q0 158 105.5 269t328.5 111q162 0 309 -79v-150q-139 88 -297 88q-299 0 -299 -233v-299h412v-136h-412v-415q51 -6 145.5 -25.5t154.5 -30.5q47 -8 107 -8q18 0 36 1q83 3 153 30v-150q-80 -31 -180 -31q-24 0 -49 2 q-129 9 -230.5 37.5t-229.5 31.5h-14q-120 0 -217 -48z" />
+<glyph unicode="&#xa5;" horiz-adv-x="1232" d="M37 1366h180l404 -588l403 588h180l-444 -645h256v-123h-322v-192h322v-123h-322v-283h-147v283h-322v123h322v192h-322v123h256z" />
+<glyph unicode="&#xa7;" horiz-adv-x="1112" d="M119 727q0 164 186 223q-82 51 -82 144q0 119 100.5 204.5t309.5 85.5q156 0 297 -61v-145q-129 69 -281 69h-4q-274 0 -274 -153q0 -25 8 -40.5t68.5 -44t179.5 -55.5q84 -18 140 -38.5t116.5 -56.5t92.5 -94t32 -136q0 -76 -51.5 -130.5t-125.5 -78.5q72 -59 72 -158 q0 -131 -111.5 -204.5t-310.5 -73.5q-203 0 -350 73v152q135 -84 340 -84q285 0 285 137q0 61 -48.5 102.5t-218.5 79.5q-184 43 -277 101.5t-93 181.5zM266 727q0 -25 8.5 -40t70 -44t179.5 -55q135 -29 219 -74q117 35 117 115q0 61 -49 102t-217 80q-98 23 -176 49 q-152 -29 -152 -133z" />
+<glyph unicode="&#xa8;" horiz-adv-x="788" d="M162 1155v162h162v-162h-162zM465 1155v162h162v-162h-162z" />
+<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 682q0 291 206 495.5t495 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -495 204.5t-206 493.5zM172 682q0 -258 184.5 -442.5t442.5 -184.5q260 0 443 184.5t183 442.5q0 260 -183 444.5t-443 184.5q-258 0 -442.5 -184.5 t-184.5 -444.5zM449 682q0 410 368 410q129 0 244 -45v-82q-109 45 -234 45q-152 0 -222 -79t-70 -249t70.5 -249t221.5 -79q121 0 234 47v-82q-115 -45 -244 -45q-369 0 -368 408z" />
+<glyph unicode="&#xaa;" horiz-adv-x="806" d="M156 979q0 186 297 186l92 -6q0 143 -168 144q-92 0 -164 -29v80q72 28 168 28q246 0 246 -235v-172q0 -72 -46 -115t-92.5 -53t-95.5 -10t-96 10t-94 54t-47 118zM238 982q0 -110 155 -110q152 1 152 105v108q-37 6 -101 7q-206 0 -206 -110z" />
+<glyph unicode="&#xab;" horiz-adv-x="1132" d="M63 522l347 389h198l-364 -389l364 -391h-198zM453 522l346 389h198l-364 -389l364 -391h-198z" />
+<glyph unicode="&#xac;" horiz-adv-x="1273" d="M199 528v136h876v-340h-135v204h-741z" />
+<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
+<glyph unicode="&#xae;" horiz-adv-x="1593" d="M98 682q0 291 206 495.5t495 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -495 204.5t-206 493.5zM172 682q0 -258 184.5 -442.5t442.5 -184.5q260 0 443 184.5t183 442.5q0 260 -183 444.5t-443 184.5q-258 0 -442.5 -184.5 t-184.5 -444.5zM545 283v798h293q131 0 201.5 -58t70.5 -173q0 -63 -22.5 -111.5t-57.5 -72t-58 -33t-46 -15.5l219 -335h-103l-206 323h-205v-323h-86zM631 688h203q113 0 150.5 43t37.5 112.5t-38 112.5t-150 43h-203v-311z" />
+<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1180v108h479v-108h-479z" />
+<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1142q0 228 223 228q109 0 166 -58.5t57 -168.5q0 -227 -223 -227q0 -1 -1 -1q-222 0 -222 227zM242 1143q0 -82 34.5 -118t114.5 -36q78 0 114 36t36 118q0 80 -36 116.5t-114 36.5q-80 0 -114.5 -36.5t-34.5 -116.5z" />
+<glyph unicode="&#xb1;" horiz-adv-x="1198" d="M231 68v122h727v-122h-727zM231 598v123h304v301h122v-301h301v-123h-301v-303h-122v303h-304z" />
+<glyph unicode="&#xb2;" horiz-adv-x="671" d="M109 948v70q133 98 252.5 230t119.5 239q0 125 -172 125q-106 0 -192 -62v86q90 55 198 56q125 0 185.5 -60.5t60.5 -145.5q0 -197 -321 -458h331v-80h-462z" />
+<glyph unicode="&#xb3;" horiz-adv-x="661" d="M109 985v82q90 -49 186 -49q203 0 203 141t-207 141h-80v76h84q164 0 164 119q0 117 -174 117q-88 0 -158 -37v80q82 37 168 37q125 0 184.5 -58.5t59.5 -138.5q0 -49 -23.5 -86t-48.5 -49l-23 -14q6 -2 14.5 -6.5t31 -20.5t40 -36.5t31.5 -54.5t14 -71 q0 -90 -66.5 -153.5t-203.5 -63.5q-102 0 -196 45z" />
+<glyph unicode="&#xb4;" horiz-adv-x="483" d="M123 1155l117 236h151l-170 -236h-98z" />
+<glyph unicode="&#xb5;" horiz-adv-x="1247" d="M203 -430v1452h147v-664q0 -45 9.5 -78.5t35 -70.5t83 -57.5t145.5 -20.5q135 0 268 39v852h147v-938q-193 -100 -430 -100q-152 2 -258 82v-496h-147z" />
+<glyph unicode="&#xb6;" horiz-adv-x="911" d="M78 946q0 203 121 311.5t350 108.5h203v-1366h-136v524h-67q-229 0 -350 108.5t-121 313.5z" />
+<glyph unicode="&#xb7;" horiz-adv-x="636" d="M238 475v164h161v-164h-161z" />
+<glyph unicode="&#xb8;" horiz-adv-x="518" d="M115 -246l168 236h116l-129 -236h-155z" />
+<glyph unicode="&#xb9;" horiz-adv-x="491" d="M109 1503v94l135 86h90v-735h-78v651z" />
+<glyph unicode="&#xba;" horiz-adv-x="854" d="M156 1090q0 86 23.5 147t64.5 92t84 43t94 12q266 0 266 -294q0 -293 -266 -293q-113 0 -189.5 64.5t-76.5 228.5zM238 1089.5q0 -114.5 44 -166t140 -51.5q98 0 141 51.5t43 166t-43 167t-141 52.5q-96 0 -140 -52.5t-44 -167z" />
+<glyph unicode="&#xbb;" horiz-adv-x="1132" d="M137 131l363 391l-363 389h197l348 -389l-348 -391h-197zM526 131l363 391l-363 389h197l348 -389l-348 -391h-197z" />
+<glyph unicode="&#xbc;" horiz-adv-x="1433" d="M106 1200v94l134 86h92v-733h-80v651zM147 -45l895 1448h97l-895 -1448h-97zM788 221v84l347 428h90v-438h104v-74h-104v-221h-80v221h-357zM877 295h268v326z" />
+<glyph unicode="&#xbd;" horiz-adv-x="1460" d="M106 1200v94l134 86h92v-733h-80v651zM147 -45l895 1448h97l-895 -1448h-97zM870 0v70q133 98 252 229t119 238q0 127 -172 127q-104 0 -192 -64v88q98 55 198 55q127 0 186.5 -60t59.5 -146q0 -195 -322 -457h332v-80h-461z" />
+<glyph unicode="&#xbe;" horiz-adv-x="1654" d="M135 684v80q94 -47 189 -47q200 0 200 140q0 69 -53 105.5t-154 36.5h-79v74h84q164 0 164 118q0 120 -175 120q-84 0 -157 -39v82q82 37 168 37q125 0 184 -58.5t59 -140.5q0 -49 -23.5 -86t-45.5 -49l-25 -15q6 -2 14.5 -6t31 -20.5t39.5 -35.5t31.5 -53t14.5 -71 q0 -90 -65.5 -154.5t-204.5 -64.5q-98 0 -197 47zM369 -45l895 1448h96l-895 -1448h-96zM1010 221v84l346 428h90v-438h104v-74h-104v-221h-80v221h-356zM1098 295h268v326z" />
+<glyph unicode="&#xbf;" horiz-adv-x="989" d="M147 20q0 82 32 141.5t77 97.5l90 76q45 38 77 101.5t32 151.5v92h147v-102q0 -104 -31.5 -181.5t-76.5 -119t-90.5 -77.5t-77 -80t-31.5 -100q0 -233 270 -233q141 0 275 90v-160q-115 -77 -287 -77q-47 0 -93 7t-105.5 31.5t-102.5 64.5t-74 111.5t-31 165.5zM446 860 v162h164v-162h-164z" />
+<glyph unicode="&#xc0;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM438 1718h152l117 -235h-99z" />
+<glyph unicode="&#xc1;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM614 1483l117 235h152l-170 -235h-99z" />
+<glyph unicode="&#xc2;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM455 1483l163 248h87l163 -248h-114l-92 145l-93 -145h-114z" />
+<glyph unicode="&#xc3;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM373 1485v117q53 66 115 72h19q51 0 97 -24l112 -60q46 -24 97 -24q10 0 21 1q61 6 114 71v-112q-53 -66 -114 -73q-12 -1 -24 -1q-49 0 -94 24l-112 59q-44 24 -92 24q-12 0 -24 -1q-62 -8 -115 -73z M422 567h479l-239 625z" />
+<glyph unicode="&#xc4;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM428 1483v164h164v-164h-164zM731 1483v164h164v-164h-164z" />
+<glyph unicode="&#xc5;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM496 1591q0 82 43 125t122.5 43t122.5 -43t43 -125t-43 -125t-122.5 -43t-122.5 43t-43 125zM569 1591.5q0 -53.5 21 -76t72 -22.5q49 0 70.5 22.5t21.5 76t-21.5 77t-70.5 23.5 q-51 0 -72 -23.5t-21 -77z" />
+<glyph unicode="&#xc6;" horiz-adv-x="1939" d="M51 0l811 1366h955v-141h-691v-471h607v-144h-607v-469h691v-141h-838v428h-506l-252 -428h-170zM559 575h420v644h-45z" />
+<glyph unicode="&#xc7;" horiz-adv-x="1286" d="M125 686q0 700 633 700q217 0 416 -77v-142q-187 80 -400 80q-260 0 -381 -134t-121 -425t121 -426t381 -135q209 0 400 80v-141q-172 -68 -357 -78l-125 -232h-155l163 232q-575 35 -575 698z" />
+<glyph unicode="&#xc8;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM444 1718h152l117 -235h-99z" />
+<glyph unicode="&#xc9;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM565 1483l115 235h151l-169 -235h-97z" />
+<glyph unicode="&#xca;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM412 1483l166 248h84l165 -248h-114l-92 145l-95 -145h-114z" />
+<glyph unicode="&#xcb;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM395 1483v164h162v-164h-162zM698 1483v164h162v-164h-162z" />
+<glyph unicode="&#xcc;" horiz-adv-x="501" d="M33 1718h151l117 -235h-98zM178 0v1366h148v-1366h-148z" />
+<glyph unicode="&#xcd;" horiz-adv-x="501" d="M178 0h148v1366h-148v-1366zM203 1483l116 235h152l-170 -235h-98z" />
+<glyph unicode="&#xce;" horiz-adv-x="501" d="M45 1483l164 248h86l164 -248h-115l-92 145l-92 -145h-115zM178 0h148v1366h-148v-1366z" />
+<glyph unicode="&#xcf;" horiz-adv-x="501" d="M18 1483v164h164v-164h-164zM178 0h148v1366h-148v-1366zM322 1483v164h163v-164h-163z" />
+<glyph unicode="&#xd0;" horiz-adv-x="1468" d="M109 621v122h159v623h428q641 0 641 -682q0 -684 -641 -684h-428v621h-159zM416 139h270q262 0 383 131t121 414t-121 414t-383 131h-270v-486h350v-122h-350v-482z" />
+<glyph unicode="&#xd1;" horiz-adv-x="1490" d="M178 0v1366h168l815 -1151v1151h148v-1366h-166l-817 1151v-1151h-148zM455 1485v117q53 66 115 72h19q51 0 98 -24l113 -60q46 -24 97 -24q10 0 21 1q61 6 114 71v-112q-53 -66 -114 -73q-12 -1 -24 -1q-49 0 -94 24l-113 59q-45 24 -93 24q-12 0 -24 -1 q-62 -8 -115 -73z" />
+<glyph unicode="&#xd2;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM563 1718h150l116 -235h-96z" />
+<glyph unicode="&#xd3;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM686 1483l115 235h151l-170 -235h-96z" />
+<glyph unicode="&#xd4;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM551 1483l164 248h86l164 -248h-115l-92 145l-92 -145h-115z" />
+<glyph unicode="&#xd5;" d="M125 684q0 700 633 700q635 0 635 -700t-635 -700q-633 0 -633 700zM272 684q0 -291 117 -426t369 -135q254 0 370.5 135t116.5 426t-116.5 426t-370.5 135q-252 0 -369 -135t-117 -426zM471 1485v117q53 66 113 72h19q51 0 99 -24l112 -60q47 -24 97 -24q10 0 21 1 q61 6 115 71v-112q-53 -66 -115 -73q-12 -1 -24 -1q-49 0 -94 24l-112 59q-46 24 -93 24q-12 0 -25 -1q-60 -8 -113 -73z" />
+<glyph unicode="&#xd6;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM524 1483v164h164v-164h-164zM827 1483v164h164v-164h-164z" />
+<glyph unicode="&#xd7;" horiz-adv-x="985" d="M158 160l272 364l-272 365h131l207 -277l209 277h131l-273 -365l273 -364h-131l-209 276l-207 -276h-131z" />
+<glyph unicode="&#xd8;" d="M102 0l158 188q-135 172 -135 496q0 700 633 700q256 0 411 -118l86 100h160l-160 -188q137 -174 138 -494q0 -702 -635 -702q-254 0 -410 118l-86 -100h-160zM272 684q0 -252 84 -383l721 856q-115 88 -319 88q-252 0 -369 -135t-117 -426zM440 209q115 -88 318 -88 q254 0 370.5 135t116.5 428q0 244 -84 381z" />
+<glyph unicode="&#xd9;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM520 1718h152l116 -235h-96z" />
+<glyph unicode="&#xda;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM678 1483l117 235h151l-170 -235h-98z" />
+<glyph unicode="&#xdb;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM526 1483l164 248h86l164 -248h-115l-92 145l-92 -145h-115z" />
+<glyph unicode="&#xdc;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM500 1483v164h164v-164h-164zM803 1483v164h164v-164h-164z" />
+<glyph unicode="&#xdd;" horiz-adv-x="1224" d="M25 1366h178l405 -596l404 596h178l-508 -739v-627h-147v627zM539 1483l116 235h150l-170 -235h-96z" />
+<glyph unicode="&#xde;" horiz-adv-x="1167" d="M176 0v1366h148v-207h266q227 0 349 -109.5t122 -314.5t-122 -313.5t-349 -108.5h-266v-313h-148zM324 455h245q109 0 181.5 23.5t105.5 68.5t45 88t12 100.5t-12 100.5t-45 88t-105.5 68.5t-181.5 23.5h-245v-561z" />
+<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M180 0v868q0 113 23.5 203t73 162t136.5 111.5t208 39.5q229 0 341.5 -112.5t112.5 -267.5q0 -51 -19.5 -100.5t-46 -84.5t-54 -61.5t-46.5 -40.5l-20 -12q10 -4 27.5 -12.5t61.5 -39t76.5 -66.5t60.5 -94.5t28 -123.5q0 -170 -150.5 -269.5t-384.5 -99.5v133 q162 0 274.5 61.5t112.5 188.5q0 129 -120.5 188.5t-266.5 59.5v129q145 0 232.5 62.5t87.5 181.5q0 123 -85 183t-222 60q-141 0 -217 -87t-76 -300v-860h-148z" />
+<glyph unicode="&#xe0;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM356 1391h150l117 -236h-97z" />
+<glyph unicode="&#xe1;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM467 1155l117 236h151l-170 -236h-98z" />
+<glyph unicode="&#xe2;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM362 1155l164 246h86l164 -246h-114l-93 143l-92 -143h-115z" />
+<glyph unicode="&#xe3;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t93 59t101 36t105.5 17.5t85 7t59.5 1l166 -10q0 137 -75.5 195.5t-223.5 58.5q-150 0 -299 -47v141q141 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 315q0 -194 278 -194h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -201zM268 1157v117q53 66 114 72q11 1 21 1q50 0 97 -25l114 -60q46 -24 97 -24q10 0 21 1q61 6 114 72v-113q-53 -66 -114 -73q-12 -1 -24 -1q-49 0 -94 24 l-114 60q-45 23 -94 23q-11 0 -23 -1q-61 -7 -115 -73z" />
+<glyph unicode="&#xe4;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM336 1155v162h164v-162h-164zM639 1155v162h164v-162h-164z" />
+<glyph unicode="&#xe5;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM393 1264q0 82 43 125t123 43t123 -43t43 -125t-43 -125t-123 -43t-123 43t-43 125zM467 1263.5q0 -53.5 20.5 -77t71.5 -23.5q49 0 70.5 23.5t21.5 77 t-21.5 76t-70.5 22.5q-51 0 -71.5 -22.5t-20.5 -76z" />
+<glyph unicode="&#xe6;" horiz-adv-x="1824" d="M111 313q0 70 25.5 125.5t62.5 90t93 59t101 36t105.5 17.5t85 7t59.5 1l166 -10q0 137 -76 195.5t-223 58.5q-166 0 -299 -49v143q152 53 305 53q268 0 377 -168q112 168 362 168q94 0 171 -29.5t122 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-746 q14 -154 94 -223.5t240 -69.5q219 0 350 76v-152q-152 -71 -360 -71h-6q-248 0 -365 151q-103 -151 -367 -151h-3q-47 0 -96.5 6t-112 27.5t-108.5 55t-78.5 96t-32.5 144.5zM258 313q0 -192 278 -192h3q270 0 270 186v197q-66 12 -180 12q-371 0 -371 -203zM956 555h598 q0 35 -8 78t-33.5 110.5t-92 112.5t-165.5 45q-150 0 -219.5 -79t-79.5 -267z" />
+<glyph unicode="&#xe7;" horiz-adv-x="966" d="M119 512q0 156 43 265.5t116.5 163.5t150.5 76.5t169 22.5q154 0 303 -57v-141q-140 57 -287 57q-182 0 -265 -92t-83 -295q0 -205 83 -296t265 -91q154 0 287 57v-143q-115 -41 -225 -53l-127 -230h-156l164 228q-86 4 -157.5 30.5t-138 83t-104.5 162t-38 252.5z" />
+<glyph unicode="&#xe8;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM395 1391h152l117 -236h-99z" />
+<glyph unicode="&#xe9;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM506 1155l117 236h151l-170 -236h-98z" />
+<glyph unicode="&#xea;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM371 1155l164 246h86l163 -246h-114l-92 143l-93 -143h-114z" />
+<glyph unicode="&#xeb;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM344 1155v162h164v-162h-164zM647 1155v162h164v-162h-164z" />
+<glyph unicode="&#xec;" horiz-adv-x="552" d="M57 1391h152l117 -236h-99zM203 0v1022h147v-1022h-147z" />
+<glyph unicode="&#xed;" horiz-adv-x="552" d="M203 0h147v1022h-147v-1022zM227 1155l115 236h152l-170 -236h-97z" />
+<glyph unicode="&#xee;" horiz-adv-x="552" d="M68 1155l165 246h84l166 -246h-114l-93 143l-94 -143h-114zM203 0h147v1022h-147v-1022z" />
+<glyph unicode="&#xef;" horiz-adv-x="552" d="M43 1155v162h162v-162h-162zM203 0h147v1022h-147v-1022zM346 1155v162h162v-162h-162z" />
+<glyph unicode="&#xf0;" horiz-adv-x="1193" d="M119 444q0 199 125 314.5t340 115.5q186 0 307 -90q-25 244 -174 365l-230 -129l-55 98l170 94q-86 33 -192 33q-41 0 -111 -8v137q66 10 127 10q184 0 328 -86l190 107l53 -98l-145 -84q197 -193 197 -596q0 -643 -465 -643q-123 0 -216.5 41t-145.5 109.5t-77.5 147 t-25.5 162.5zM266 446q0 -41 9.5 -84t38 -103t98.5 -98t172 -38q43 0 70.5 3t78.5 29.5t83 75t57.5 148.5t27.5 242q-110 116 -314 116h-3q-154 0 -236 -75.5t-82 -215.5z" />
+<glyph unicode="&#xf1;" horiz-adv-x="1189" d="M168 0v940q201 100 432 100q193 0 299.5 -98t106.5 -276v-666h-148v666q0 43 -9 76.5t-35 71.5t-84 58.5t-146 20.5q-129 0 -269 -41v-852h-147zM299 1157v117q53 66 113 72q11 1 21 1q49 0 97 -25l112 -60q47 -24 97 -24q10 0 21 1q61 6 114 72v-113q-53 -66 -114 -73 q-12 -1 -24 -1q-49 0 -94 24l-112 60q-46 23 -94 23q-12 0 -24 -1q-60 -7 -113 -73z" />
+<glyph unicode="&#xf2;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM362 1391h152l117 -236h-99z" />
+<glyph unicode="&#xf3;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM530 1155l117 236h152l-170 -236h-99z" />
+<glyph unicode="&#xf4;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM373 1155l166 246h84l165 -246h-114l-92 143l-95 -143h-114z" />
+<glyph unicode="&#xf5;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t150.5 76.5t169 22.5t170 -22.5t151.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-151.5 -76.5t-170 -22.5t-169 22.5t-150.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 79 -300t253 -93t253.5 93t79.5 300t-79.5 300 t-253.5 93t-253 -93t-79 -300zM301 1159v115q53 66 113 73q12 1 23 1q49 0 95 -24l112 -60q46 -25 95 -25q11 0 23 1q61 7 115 73v-115q-53 -66 -115 -73q-12 -1 -23 -1q-50 0 -95 24l-112 60q-47 24 -97 24q-10 0 -21 -1q-60 -6 -113 -72z" />
+<glyph unicode="&#xf6;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM348 1155v162h162v-162h-162zM651 1155v162h162v-162h-162z" />
+<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M227 465h832v123h-832v-123zM561 152v163h164v-163h-164zM561 735v164h164v-164h-164z" />
+<glyph unicode="&#xf8;" horiz-adv-x="1175" d="M76 0l127 152q-96 133 -97 360q0 156 43.5 265.5t117 163.5t151.5 76.5t170 22.5q180 0 297 -82l53 64h160l-125 -150q94 -131 94 -360q0 -156 -43 -265.5t-116.5 -163.5t-150.5 -76.5t-169 -22.5q-182 0 -297 79l-55 -63h-160zM254 512q0 -158 45 -246l496 586 q-78 53 -207 53q-174 0 -254 -93t-80 -300zM381 170q76 -51 207 -51q174 0 253 93t79 300q0 156 -46 246z" />
+<glyph unicode="&#xf9;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM383 1391h152l116 -236h-98z" />
+<glyph unicode="&#xfa;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM541 1155l116 236h150l-170 -236h-96z" />
+<glyph unicode="&#xfb;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM389 1155l164 246h84l166 -246h-115l-92 143l-92 -143h-115z" />
+<glyph unicode="&#xfc;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM362 1155v162h164v-162h-164zM666 1155v162h163v-162h-163z" />
+<glyph unicode="&#xfd;" horiz-adv-x="1028" d="M63 1022h158l293 -809l293 809h158l-418 -1122q-109 -295 -320 -295q-51 0 -98 18v135q41 -18 88 -18q63 0 118.5 68.5t80.5 138.5l24 69zM449 1155l116 236h152l-170 -236h-98z" />
+<glyph unicode="&#xfe;" horiz-adv-x="1136" d="M170 -377v1776h147v-414q123 41 252 41q70 0 131.5 -12.5t125 -47t109.5 -90t74.5 -149.5t28.5 -217q0 -152 -42 -258.5t-114.5 -159.5t-148.5 -74.5t-164 -21.5q-129 0 -252 41v-414h-147zM317 184q117 -51 252 -51q170 0 246 89t76 288t-76 289t-246 90 q-135 0 -252 -51v-654z" />
+<glyph unicode="&#xff;" horiz-adv-x="1028" d="M63 1022h158l293 -809l293 809h158l-418 -1122q-109 -295 -320 -295q-51 0 -98 18v135q41 -18 88 -18q63 0 118.5 68.5t80.5 138.5l24 69zM279 1155v162h161v-162h-161zM582 1155v162h161v-162h-161z" />
+<glyph unicode="&#x152;" horiz-adv-x="2029" d="M123 684q0 700 633 700q160 0 274 -45v27h864v-139h-716v-471h628v-144h-628v-473h716v-139h-864v27q-117 -43 -274 -43q-633 0 -633 700zM270 684q0 -291 117 -426t369 -135q170 0 274 59v1004q-109 59 -274 59q-252 0 -369 -135t-117 -426z" />
+<glyph unicode="&#x153;" horiz-adv-x="1921" d="M102 512q0 156 43 265.5t117 163.5t150.5 76.5t169.5 22.5q279 0 397 -186q113 186 377 186q94 0 170 -29.5t122 -69.5t78.5 -104.5t47 -109.5t22.5 -108.5t8 -78.5v-44l-2 -72h-745q14 -154 94 -223.5t238 -69.5q219 0 350 76v-152q-148 -71 -359 -71h-6 q-276 0 -393 190q-119 -190 -399 -190q-92 0 -169 22.5t-151 76.5t-117 163.5t-43 265.5zM250 512q0 -207 79 -300t253 -93t253.5 93t79.5 300t-79.5 300t-253.5 93t-253 -93t-79 -300zM1055 555h600q0 33 -8.5 77t-35 111.5t-93 112.5t-162.5 45q-152 0 -221.5 -79 t-79.5 -267z" />
+<glyph unicode="&#x178;" horiz-adv-x="1224" d="M25 1366h178l405 -596l404 596h178l-508 -739v-627h-147v627zM375 1483v164h164v-164h-164zM678 1483v164h164v-164h-164z" />
+<glyph unicode="&#x2c6;" horiz-adv-x="823" d="M207 1155l166 246h84l164 -246h-115l-92 143l-92 -143h-115z" />
+<glyph unicode="&#x2dc;" horiz-adv-x="1038" d="M215 1157v117q53 66 113 72q11 1 21 1q49 0 97 -25l112 -60q47 -24 97 -24q10 0 21 1q61 6 115 72v-113q-53 -66 -115 -73q-12 -1 -23 -1q-50 0 -95 24l-112 60q-46 23 -94 23q-12 0 -24 -1q-60 -7 -113 -73z" />
+<glyph unicode="&#x2000;" horiz-adv-x="879" />
+<glyph unicode="&#x2001;" horiz-adv-x="1759" />
+<glyph unicode="&#x2002;" horiz-adv-x="879" />
+<glyph unicode="&#x2003;" horiz-adv-x="1759" />
+<glyph unicode="&#x2004;" horiz-adv-x="586" />
+<glyph unicode="&#x2005;" horiz-adv-x="439" />
+<glyph unicode="&#x2006;" horiz-adv-x="293" />
+<glyph unicode="&#x2007;" horiz-adv-x="293" />
+<glyph unicode="&#x2008;" horiz-adv-x="219" />
+<glyph unicode="&#x2009;" horiz-adv-x="351" />
+<glyph unicode="&#x200a;" horiz-adv-x="97" />
+<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
+<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
+<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
+<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 528v127h860v-127h-860z" />
+<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 528v136h1501v-136h-1501z" />
+<glyph unicode="&#x2018;" horiz-adv-x="522" d="M96 1384h168l152 -401h-148z" />
+<glyph unicode="&#x2019;" horiz-adv-x="522" d="M96 983l152 401h168l-172 -401h-148z" />
+<glyph unicode="&#x201a;" horiz-adv-x="614" d="M147 -229l152 401h166l-170 -401h-148z" />
+<glyph unicode="&#x201c;" horiz-adv-x="800" d="M96 1384h168l152 -401h-148zM375 1384h168l151 -401h-147z" />
+<glyph unicode="&#x201d;" horiz-adv-x="792" d="M96 983l152 401h168l-172 -401h-148zM375 983l151 401h168l-172 -401h-147z" />
+<glyph unicode="&#x201e;" horiz-adv-x="892" d="M147 -229l152 401h166l-170 -401h-148zM426 -229l152 401h167l-172 -401h-147z" />
+<glyph unicode="&#x2022;" horiz-adv-x="849" d="M184 684q0 98 70 167t168 69q96 0 165.5 -69t69.5 -167t-69.5 -168t-165.5 -70q-98 0 -168 70t-70 168z" />
+<glyph unicode="&#x2026;" horiz-adv-x="1576" d="M219 0v164h164v-164h-164zM711 0v164h161v-164h-161zM1200 0v164h162v-164h-162z" />
+<glyph unicode="&#x202f;" horiz-adv-x="351" />
+<glyph unicode="&#x2039;" horiz-adv-x="710" d="M63 522l347 389h198l-364 -389l364 -391h-198z" />
+<glyph unicode="&#x203a;" horiz-adv-x="776" d="M137 131l363 391l-363 389h197l348 -389l-348 -391h-197z" />
+<glyph unicode="&#x205f;" horiz-adv-x="439" />
+<glyph unicode="&#x20ac;" horiz-adv-x="1390" d="M102 467v123h136q-4 61 -4 94q-1 3 -1 6q0 33 5 88h-136v123h150q90 483 614 483q215 0 416 -79v-142q-194 82 -399 82q-207 0 -325 -84t-157 -260h658v-123h-674q-4 -61 -4 -94q0 -37 4 -94h674v-123h-658q39 -176 157 -260t325 -84q209 0 399 80v-142 q-200 -77 -411 -77h-5q-524 0 -614 483h-150z" />
+<glyph unicode="&#x2122;" horiz-adv-x="1550" d="M100 1294v72h557v-72h-241v-610h-74v610h-242zM762 684v682h80l235 -346l236 346h80v-682h-74v561l-242 -348l-241 348v-561h-74z" />
+<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
+<glyph unicode="&#xfb01;" horiz-adv-x="1202" d="M84 887v137h176v53q0 106 28.5 182t63.5 112t89.5 56.5t82 22.5t66.5 2q53 0 100 -10v-133q-41 8 -74 8q-39 0 -59 -2t-54 -14.5t-52.5 -35t-33 -70.5t-14.5 -116v-55h287v-137h-287v-887h-143v887h-176zM885 1204v162h164v-162h-164zM893 0h147v1022h-147v-1022z" />
+<glyph unicode="&#xfb02;" horiz-adv-x="1220" d="M84 887v137h176v53q0 106 28.5 182t63.5 112t89.5 56.5t82 22.5t66.5 2q53 0 100 -10v-133q-41 8 -74 8q-39 0 -59 -2t-54 -14.5t-52.5 -35t-33 -70.5t-14.5 -116v-55h287v-137h-287v-887h-143v887h-176zM893 0h147v1411h-147v-1411z" />
+<glyph unicode="&#xfb03;" horiz-adv-x="1890" d="M82 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM757 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2 q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM1543 1204v162h163v-162h-163zM1551 0h147v1022h-147v-1022z" />
+<glyph unicode="&#xfb04;" horiz-adv-x="1902" d="M82 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM757 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2 q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM1557 0v1411h147v-1411h-147z" />
+</font>
+</defs></svg> 
\ No newline at end of file
diff --git a/motioneye/static/fnt/mavenpro-regular-webfont.ttf b/motioneye/static/fnt/mavenpro-regular-webfont.ttf
new file mode 100644 (file)
index 0000000..8fb8aac
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-regular-webfont.ttf differ
diff --git a/motioneye/static/fnt/mavenpro-regular-webfont.woff b/motioneye/static/fnt/mavenpro-regular-webfont.woff
new file mode 100644 (file)
index 0000000..16f0e00
Binary files /dev/null and b/motioneye/static/fnt/mavenpro-regular-webfont.woff differ
diff --git a/motioneye/static/img/apply-progress.gif b/motioneye/static/img/apply-progress.gif
new file mode 100644 (file)
index 0000000..610f733
Binary files /dev/null and b/motioneye/static/img/apply-progress.gif differ
diff --git a/motioneye/static/img/arrows.svg b/motioneye/static/img/arrows.svg
new file mode 100644 (file)
index 0000000..f1239a7
--- /dev/null
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="48"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="arrows.svg"
+   inkscape:export-filename="/media/data/projects/motioneye/static/img/settings.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="5.0625"
+     inkscape:cx="43.687152"
+     inkscape:cy="14.670355"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1030"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:snap-intersection-paths="true"
+     showguides="true"
+     inkscape:guide-bbox="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1004.3622)">
+    <path
+       style="fill:none;stroke:#3498db;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 30,1012.3622 -17,17 17,15"
+       id="path3755"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccc" />
+    <path
+       sodipodi:nodetypes="ccc"
+       inkscape:connector-curvature="0"
+       id="path3757"
+       d="m 66,1012.3622 17,17 -17,15"
+       style="fill:none;stroke:#3498db;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+  </g>
+</svg>
diff --git a/motioneye/static/img/camera-progress.gif b/motioneye/static/img/camera-progress.gif
new file mode 100644 (file)
index 0000000..748c0c7
Binary files /dev/null and b/motioneye/static/img/camera-progress.gif differ
diff --git a/motioneye/static/img/combo-box-arrow.svg b/motioneye/static/img/combo-box-arrow.svg
new file mode 100644 (file)
index 0000000..5f57b93
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="20"
+   height="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="combo-box-arrow.svg"
+   inkscape:export-filename="/media/data/projects/motioneye/static/img/combo-box-arrow.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="7.1473113"
+     inkscape:cy="7.817561"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1027"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:snap-intersection-paths="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1036.3622)">
+    <path
+       style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+       d="m 1,1040.8622 14,0 -7,8 z"
+       id="path3809"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/motioneye/static/img/logout.svg b/motioneye/static/img/logout.svg
new file mode 100644 (file)
index 0000000..cda383d
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg2"
+   xml:space="preserve"
+   viewBox="0 0 64 64"
+   version="1.1"
+   inkscape:version="0.48.5 r10040"
+   width="100%"
+   height="100%"
+   sodipodi:docname="logout.svg"><defs
+     id="defs8" /><sodipodi:namedview
+     pagecolor="#666666"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1077"
+     inkscape:window-height="782"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="5.1943359"
+     inkscape:cx="24.357877"
+     inkscape:cy="26.948648"
+     inkscape:window-x="258"
+     inkscape:window-y="33"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" /><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><path
+     sodipodi:nodetypes="ssccccccsccccc"
+     style="fill:#3498db;fill-opacity:1"
+     id="path3"
+     d="m 21.267338,42.732446 c 3.741665,3.741354 9.807034,3.741354 13.548699,0 2.776803,-2.77657 3.490747,-6.8354 2.144089,-10.273324 l 5.763494,-5.763661 c 1.49694,-1.496847 1.496489,-3.922567 0,-5.419481 -1.496034,-1.496525 -3.922182,-1.496525 -5.419118,0 l -5.763946,5.764034 c -3.437493,-1.346908 -7.495965,-0.633431 -10.27367,2.144832 -3.740312,3.740676 -3.740765,9.806754 4.52e-4,13.5476 z m 6.435428,-6.434662 c 0.898073,0.897093 0.898073,2.353493 0,3.2516 -0.897621,0.897939 -2.35394,0.897939 -3.251561,0 -0.89717,-0.898107 -0.89717,-2.354507 0,-3.2516 0.897621,-0.898107 2.353489,-0.898107 3.251561,0 z"
+     inkscape:connector-curvature="0" /><path
+     sodipodi:type="arc"
+     style="fill:none;stroke:#3498db;stroke-width:1.80392128;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+     id="path4097"
+     sodipodi:cx="16"
+     sodipodi:cy="16.5"
+     sodipodi:rx="14"
+     sodipodi:ry="12.5"
+     d="m 30,16.5 a 14,12.5 0 1 1 -28,0 14,12.5 0 1 1 28,0 z"
+     transform="matrix(1.5714286,0,0,1.7600001,6.8571426,2.9599978)" /></svg>
\ No newline at end of file
diff --git a/motioneye/static/img/main-loading-progress.gif b/motioneye/static/img/main-loading-progress.gif
new file mode 100644 (file)
index 0000000..4f7a78e
Binary files /dev/null and b/motioneye/static/img/main-loading-progress.gif differ
diff --git a/motioneye/static/img/modal-progress.gif b/motioneye/static/img/modal-progress.gif
new file mode 100644 (file)
index 0000000..df1657c
Binary files /dev/null and b/motioneye/static/img/modal-progress.gif differ
diff --git a/motioneye/static/img/motioneye-icon.svg b/motioneye/static/img/motioneye-icon.svg
new file mode 100644 (file)
index 0000000..93dfd9c
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   width="64"
+   height="64"
+   xml:space="preserve"
+   sodipodi:docname="motioneye-icon.svg"
+   inkscape:export-filename="/home/ccrisan/projects/motioneye/static/img/motioneye-logo.png"
+   inkscape:export-xdpi="960"
+   inkscape:export-ydpi="960"><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs6" /><sodipodi:namedview
+     pagecolor="#969696"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1030"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="3.2304687"
+     inkscape:cx="-17.830274"
+     inkscape:cy="37.141088"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g10"
+     showguides="true"
+     inkscape:guide-bbox="true" /><g
+     id="g10"
+     inkscape:groupmode="layer"
+     inkscape:label="ink_ext_XXXXXX"
+     transform="matrix(1.25,0,0,-1.25,0,64)"><path
+       style="fill:#515151;fill-opacity:1;stroke:none"
+       d="m 32,6 c -5.264933,0 -10.15584,1.684955 -14.25,4.375 4.426748,0.474546 8.794165,1.616509 13.75,4.0625 5.014819,-2.5549 10.152222,-3.621792 14.78125,-4.15625 C 42.179879,7.5768125 37.280255,6 32,6 z m 17,5.59375 c -5.21085,0.03453 -12.330126,1.666208 -17.5,4.03125 -5.435679,-1.964064 -11.012887,-4.147356 -17.3125,-4 -0.419974,0.0099 -0.823167,0.03114 -1.25,0.0625 L 8,12.09375 l 4.6875,1.5625 c 6.429941,2.095118 12.084993,4.395085 17.0625,13.03125 1.154473,0 3.425649,5e-4 4.5,5e-4 4.957862,-8.587269 10.904708,-10.983331 17.03125,-13.03175 l 4.625,-1.5 -4.8125,-0.46875 C 50.437916,11.625136 49.744407,11.588818 49,11.59375 z M 12.65625,14.6875 C 8.5126665,19.293305 6,25.316864 6,32 6,46.359404 17.640596,58 32,58 46.359404,58 58,46.359404 58,32 58,25.341351 55.491547,19.287285 51.375,14.6875 49.406504,15.410957 47.442694,16.253379 45.5,17.34375 49.260018,18.43176 52,21.91463 52,26 c 0,4.945036 -4.027245,9 -9,9 -4.217962,0 -7.765819,-2.914031 -8.75,-6.8125 -1.243768,0 -3.31109,0 -4.5,0 C 28.774204,32.087724 25.219217,35 21,35 c -4.972755,0 -9.03125,-4.054964 -9.03125,-9 0,-4.133444 2.851912,-7.644 6.6875,-8.6875 -1.945667,-1.058808 -3.952016,-1.881003 -6,-2.625 z M 21,23 c -1.656854,0 -3,1.343146 -3,3 0,1.656854 1.343146,3 3,3 1.656854,0 3,-1.343146 3,-3 0,-1.656854 -1.343146,-3 -3,-3 z m 22,0 c -1.656854,0 -3,1.343146 -3,3 0,1.656854 1.343146,3 3,3 1.656854,0 3,-1.343146 3,-3 0,-1.656854 -1.343146,-3 -3,-3 z m -11,8 c 0.618234,2.299881 1.278866,4.581981 4,6 l -4,6 -4,-6 c 2.383634,-1.6888 3.483037,-3.758112 4,-6 z"
+       transform="matrix(0.8,0,0,-0.8,0,51.2)"
+       id="path3938"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="scccssccccccccccscsssccssccssccssssssssssccccc" /></g></svg>
\ No newline at end of file
diff --git a/motioneye/static/img/motioneye-logo.svg b/motioneye/static/img/motioneye-logo.svg
new file mode 100644 (file)
index 0000000..ea469a3
--- /dev/null
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 64 64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><g id="g10" transform="matrix(1.25,0,0,-1.25,0,64)"><path id="path3938" d="m32,6c-5.2649,0-10.156,1.685-14.25,4.375,4.4267,0.47455,8.7942,1.6165,13.75,4.0625,5.0148-2.5549,10.152-3.6218,14.781-4.1562-4.101-2.7052-9.001-4.282-14.281-4.282zm17,5.5938c-5.2108,0.03453-12.33,1.6662-17.5,4.0312-5.4357-1.9641-11.013-4.1474-17.312-4-0.41997,0.0099-0.82317,0.03114-1.25,0.0625l-4.938,0.406,4.6875,1.5625c6.4299,2.0951,12.085,4.3951,17.062,13.031,1.1545,0,3.4256,0.0005,4.5,0.0005,4.9579-8.5873,10.905-10.983,17.031-13.032l4.625-1.5-4.8125-0.46875c-0.656-0.062-1.35-0.098-2.094-0.093zm-36.344,3.094c-4.1433,4.605-6.656,10.629-6.656,17.312,0,14.359,11.641,26,26,26s26-11.641,26-26c0-6.659-2.508-12.713-6.625-17.312-1.968,0.723-3.932,1.565-5.875,2.656,3.76,1.088,6.5,4.571,6.5,8.656,0,4.945-4.0272,9-9,9-4.218,0-7.7658-2.914-8.75-6.8125h-4.5c-0.976,3.9-4.531,6.812-8.75,6.812-4.9728,0-9.0312-4.055-9.0312-9,0-4.1334,2.8519-7.644,6.6875-8.6875-1.9457-1.0588-3.952-1.881-6-2.625zm8.344,8.312c-1.6569,0-3,1.3431-3,3s1.3431,3,3,3,3-1.3431,3-3-1.3431-3-3-3zm22,0c-1.6569,0-3,1.3431-3,3s1.3431,3,3,3,3-1.3431,3-3-1.3431-3-3-3zm-11,8c0.61823,2.2999,1.2789,4.582,4,6l-4,6-4-6c2.3836-1.6888,3.483-3.7581,4-6z" transform="matrix(0.8,0,0,-0.8,0,51.2)" fill="#FFF"/></g></svg>
diff --git a/motioneye/static/img/no-camera.svg b/motioneye/static/img/no-camera.svg
new file mode 100644 (file)
index 0000000..b7f1800
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   width="160"
+   height="160"
+   xml:space="preserve"
+   sodipodi:docname="no-camera.svg"><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+     id="defs6" /><sodipodi:namedview
+     pagecolor="#969696"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1027"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="1"
+     inkscape:cx="194.39325"
+     inkscape:cy="58.632885"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g10" /><g
+     id="g10"
+     inkscape:groupmode="layer"
+     inkscape:label="ink_ext_XXXXXX"
+     transform="matrix(1.25,0,0,-1.25,0,160)"><g
+       id="g3916"
+       transform="matrix(0.23474178,0,0,0.23474178,-6.596479,-6.504096)"><path
+         id="path28"
+         transform="matrix(0.8,0,0,-0.8,0,599)"
+         d="m 370.46875,230.75 c -25.42532,0 -48.59369,9.59353 -66.125,25.34375 l 57.96875,57.96875 c 3.96533,-0.60105 8.02296,-0.9375 12.15625,-0.9375 44.43125,0 80.4375,36.00625 80.4375,80.4375 0,4.1334 -0.33645,8.19036 -0.9375,12.15625 L 554.5,506.25 c 14.98807,-4.91141 25.8125,-18.99541 25.8125,-35.625 l 0,-162.5 c 0,-20.71 -16.79,-37.5 -37.5,-37.5 l -20.875,0 0,-7.1875 c 0,-8.9975 -7.31375,-16.28125 -16.3125,-16.28125 -8.99625,0 -16.28125,7.28375 -16.28125,16.28125 l 0,7.1875 -39.4375,0 C 431.86011,246.414 402.99373,230.75 370.46875,230.75 z m -13.59375,13 27.5,0 c 8.63,0 15.625,6.99625 15.625,15.625 0,4.41759 -1.83365,8.40792 -4.78125,11.25 l -49.1875,0 c -2.94775,-2.84208 -4.78125,-6.83241 -4.78125,-11.25 0,-8.62875 6.99625,-15.625 15.625,-15.625 z"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         inkscape:connector-curvature="0" /><path
+         id="path3901"
+         d="m 164.25,382.5 c -16.569,0 -30,-13.432 -30,-30 l 0,-130 c 0,-16.568 13.431,-30 30,-30 l 203.075,0 -35.8,35.775 c -9.41466,-5.39918 -20.31889,-8.5 -31.95,-8.5 -35.543,0 -64.35,28.831 -64.35,64.375 0,11.62338 3.11098,22.51462 8.5,31.925 L 177.3,382.5 l -13.05,0 z"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         inkscape:connector-curvature="0" /><path
+         id="path36"
+         transform="matrix(0.8,0,0,-0.8,0,599)"
+         d="m 374.0625,54.96875 c -176.1485,0 -319.5,143.28875 -319.5,319.4375 0,176.15678 143.35153,319.5 319.5,319.5 176.15077,0 319.5,-143.34335 319.5,-319.5 0,-176.14864 -143.3492,-319.4375 -319.5,-319.4375 z m 0,50 c 149.1292,0 269.5,120.31114 269.5,269.4375 0,64.41395 -22.47282,123.45239 -60,169.75 L 204.34375,164.9375 c 46.29727,-37.51599 105.31199,-59.96875 169.71875,-59.96875 z m -209.5,99.71875 379.21875,379.21875 c -46.2979,37.52569 -105.30826,60 -169.71875,60 -149.12653,0 -269.5,-120.36428 -269.5,-269.5 0,-64.4098 22.47246,-123.42872 60,-169.71875 z"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         inkscape:connector-curvature="0" /></g></g></svg>
\ No newline at end of file
diff --git a/motioneye/static/img/no-preview.svg b/motioneye/static/img/no-preview.svg
new file mode 100644 (file)
index 0000000..17c39f3
--- /dev/null
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   width="64"
+   height="64"
+   xml:space="preserve"
+   sodipodi:docname="no-preview.svg"
+   inkscape:export-filename="/home/ccrisan/projects/motioneye/static/img/motioneye-logo.png"
+   inkscape:export-xdpi="960"
+   inkscape:export-ydpi="960"><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+     id="defs6" /><sodipodi:namedview
+     pagecolor="#969696"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="1"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1030"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="5.0625"
+     inkscape:cx="53.237951"
+     inkscape:cy="28.106834"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g10"
+     showguides="true"
+     inkscape:guide-bbox="true" /><g
+     id="g10"
+     inkscape:groupmode="layer"
+     inkscape:label="ink_ext_XXXXXX"
+     transform="matrix(1.25,0,0,-1.25,0,64)"><g
+       id="g3757"
+       style="opacity:0.7"><rect
+         transform="scale(1,-1)"
+         y="-42.400002"
+         x="4"
+         height="34.400002"
+         width="43.200001"
+         id="rect3751"
+         style="fill:none;stroke:#ffffff;stroke-width:1.60000001999999997;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /><path
+         sodipodi:nodetypes="cccccc"
+         transform="matrix(0.8,0,0,-0.8,0,51.2)"
+         inkscape:connector-curvature="0"
+         id="path3753"
+         d="m 11.5,46.5 11,-14 6,6 11,-14 13,22 z"
+         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /><path
+         transform="matrix(1.0251562,0,0,-1.0251562,-4.199999,56.75)"
+         d="m 20.484682,24.142662 a 2.3411067,2.3411067 0 1 1 -4.682213,0 2.3411067,2.3411067 0 1 1 4.682213,0 z"
+         sodipodi:ry="2.3411067"
+         sodipodi:rx="2.3411067"
+         sodipodi:cy="24.142662"
+         sodipodi:cx="18.143576"
+         id="path3755"
+         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.56073773000000005;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+         sodipodi:type="arc" /></g></g></svg>
\ No newline at end of file
diff --git a/motioneye/static/img/settings.svg b/motioneye/static/img/settings.svg
new file mode 100644 (file)
index 0000000..c11c28e
--- /dev/null
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="96"
+   height="48"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="settings.svg"
+   inkscape:export-filename="/media/data/projects/motioneye/static/img/settings.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="17.085938"
+     inkscape:cx="93.239037"
+     inkscape:cy="25.695745"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1027"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:snap-intersection-paths="true"
+     showguides="true"
+     inkscape:guide-bbox="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1004.3622)">
+    <path
+       style="fill:#3498db;fill-opacity:1;stroke:none"
+       d="m 23,30 c -2.045693,0 -3.788228,1.240478 -4.5625,3 L 5,33 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 13.4375,0 c 0.774272,1.759522 2.516807,3 4.5625,3 2.045693,0 3.788228,-1.240478 4.5625,-3 L 43,37 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 L 27.5625,33 C 26.788228,31.240478 25.045693,30 23,30 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
+       transform="translate(0,1004.3622)"
+       id="rect3755"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:#3498db;fill-opacity:1;stroke:none"
+       d="m 36,20 c -2.045693,0 -3.788228,1.240478 -4.5625,3 L 5,23 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 26.4375,0 c 0.774272,1.759522 2.516807,3 4.5625,3 2.045693,0 3.788228,-1.240478 4.5625,-3 L 43,27 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 l -2.4375,0 C 39.788228,21.240478 38.045693,20 36,20 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
+       transform="translate(0,1004.3622)"
+       id="rect3757"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:#3498db;fill-opacity:1;stroke:none"
+       d="m 15,10 c -2.045692,0 -3.788228,1.240478 -4.5625,3 L 5,13 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 5.4375,0 c 0.774272,1.759522 2.516808,3 4.5625,3 2.045692,0 3.788228,-1.240478 4.5625,-3 L 43,17 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 L 19.5625,13 C 18.788228,11.240478 17.045692,10 15,10 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
+       transform="translate(0,1004.3622)"
+       id="rect3759"
+       inkscape:connector-curvature="0" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#3498db;stroke-width:2.6408689;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="path3764"
+       sodipodi:cx="67.901237"
+       sodipodi:cy="6.4814816"
+       sodipodi:rx="20.098766"
+       sodipodi:ry="19.518518"
+       d="m 88.000004,6.4814816 a 20.098766,19.518518 0 1 1 -40.197533,0 20.098766,19.518518 0 1 1 40.197533,0 z"
+       transform="matrix(0.74631447,0,0,0.76850096,21.324322,1023.3813)" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path3766"
+       d="m 82,1030.3622 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 -13.004,0 -8.7461,0 -20,0 -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 12.4816,0 8.4097,0 20,0 z"
+       style="fill:#3498db;fill-opacity:1;stroke:none"
+       sodipodi:nodetypes="cssccsscc" />
+  </g>
+</svg>
diff --git a/motioneye/static/img/slider-arrow.svg b/motioneye/static/img/slider-arrow.svg
new file mode 100644 (file)
index 0000000..543e0e9
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="11"
+   height="18"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="slider-arrow.svg"
+   inkscape:export-filename="/media/data/projects/motioneye/static/img/slider-arrow.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="20.539051"
+     inkscape:cy="-1.1141645"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1027"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:snap-intersection-paths="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1034.3622)">
+    <path
+       style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       d="m 1,1040.3622 0,10 9,0 0,-10 -4.5,-4.5 z"
+       id="path3809"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccc" />
+  </g>
+</svg>
diff --git a/motioneye/static/img/small-progress.gif b/motioneye/static/img/small-progress.gif
new file mode 100644 (file)
index 0000000..e0be29f
Binary files /dev/null and b/motioneye/static/img/small-progress.gif differ
diff --git a/motioneye/static/img/top-bar-buttons.svg b/motioneye/static/img/top-bar-buttons.svg
new file mode 100644 (file)
index 0000000..a10c006
--- /dev/null
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="80"
+   height="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="top-bar-buttons.svg"
+   inkscape:export-filename="/media/data/projects/motioneye/static/img/top-bar-buttons.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#b39a9a"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.42745098"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.25"
+     inkscape:cx="111.39282"
+     inkscape:cy="5.668431"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1030"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:snap-intersection-paths="true"
+     showguides="true"
+     inkscape:guide-bbox="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1036.3622)">
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="path3752"
+       sodipodi:cx="7.8036885"
+       sodipodi:cy="8.2353296"
+       sodipodi:rx="5.6186557"
+       sodipodi:ry="5.3845448"
+       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+       transform="matrix(1.1568604,0,0,1.2071587,-1.0277779,1034.4208)" />
+    <path
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+       d="m 5.5,5.5 5,5"
+       id="path3756"
+       inkscape:connector-curvature="0"
+       transform="translate(0,1036.3622)"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path3758"
+       d="m 10.5,1041.8622 -5,5"
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="path3792"
+       sodipodi:cx="7.8036885"
+       sodipodi:cy="8.2353296"
+       sodipodi:rx="5.6186557"
+       sodipodi:ry="5.3845448"
+       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+       transform="matrix(-1.1568604,0,0,1.2071587,33.027778,1034.4209)" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path3794"
+       d="m 21.5,1046.8622 3,-3"
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+    <path
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+       d="m 24.5,1043.8622 0,-2"
+       id="path3796"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path3798"
+       d="m 24.5,1043.8622 2,0"
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+    <path
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+       d="m 27.5,1042.8622 -1,1"
+       id="path3800"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+    <path
+       sodipodi:nodetypes="cc"
+       inkscape:connector-curvature="0"
+       id="path3802"
+       d="m 25.5,1040.8622 -1,1"
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+    <path
+       transform="matrix(-1.1568604,0,0,1.2071587,49.027778,1034.4209)"
+       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+       sodipodi:ry="5.3845448"
+       sodipodi:rx="5.6186557"
+       sodipodi:cy="8.2353296"
+       sodipodi:cx="7.8036885"
+       id="path3759"
+       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       sodipodi:type="arc" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path3761"
+       d="m 36.5,1041.8622 7,0 0,5 -7,0 z"
+       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+    <path
+       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#3498db;fill-opacity:1;stroke:none;stroke-width:8.06000042;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+       d="m 56.025,1041.1703 c -0.717378,0 -1.358863,0.3074 -1.769063,0.7819 l -1.988437,0 c -0.436275,0 -0.7875,0.3512 -0.7875,0.7875 l 0,3.375 c 0,0.4363 0.351225,0.7875 0.7875,0.7875 l 7.425,0 c 0.436275,0 0.7875,-0.3512 0.7875,-0.7875 l 0,-3.375 c 0,-0.4363 -0.351225,-0.7875 -0.7875,-0.7875 l -0.2925,0 0,-0.1012 a 0.450045,0.450045 0 1 0 -0.9,0 l 0,0.1012 -0.705938,0 c -0.409047,-0.4745 -1.051682,-0.7819 -1.769062,-0.7819 z m -0.36,0.3319 0.72,0 c 0.12465,0 0.225,0.1004 0.225,0.225 l 0,0.09 c 0,0.1247 -0.10035,0.225 -0.225,0.225 l -0.72,0 c -0.12465,0 -0.225,-0.1003 -0.225,-0.225 l 0,-0.09 c 0,-0.1246 0.10035,-0.225 0.225,-0.225 z m 0.36,1.35 c 0.919555,0 1.665,0.7455 1.665,1.665 0,0.9196 -0.745445,1.665 -1.665,1.665 -0.919553,0 -1.665,-0.7454 -1.665,-1.665 0,-0.9195 0.745447,-1.665 1.665,-1.665 z"
+       id="path3803"
+       inkscape:connector-curvature="0" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="path3815"
+       sodipodi:cx="7.8036885"
+       sodipodi:cy="8.2353296"
+       sodipodi:rx="5.6186557"
+       sodipodi:ry="5.3845448"
+       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+       transform="matrix(-1.1568604,0,0,1.2071587,65.027778,1034.4209)" />
+    <path
+       transform="matrix(-1.1568604,0,0,1.2071587,81.027778,1034.4209)"
+       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+       sodipodi:ry="5.3845448"
+       sodipodi:rx="5.6186557"
+       sodipodi:cy="8.2353296"
+       sodipodi:cx="7.8036885"
+       id="path3817"
+       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       sodipodi:type="arc" />
+    <path
+       style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+       d="m 69.5,1040.8622 6,3.5 -6,3.5 z"
+       id="path3819"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/motioneye/static/img/validation-error.svg b/motioneye/static/img/validation-error.svg
new file mode 100644 (file)
index 0000000..6c4d5da
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="20"
+   height="20"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="validation-error.svg"
+   inkscape:export-filename="/media/data/projects/motioneye/static/img/validation-error.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.390625"
+     inkscape:cx="-0.48299409"
+     inkscape:cy="28.256052"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1027"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:snap-intersection-paths="true"
+     showguides="true"
+     inkscape:guide-bbox="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1032.3622)">
+    <path
+       style="fill:#c11c00;fill-opacity:1;stroke:none"
+       d="M 7 3 C 3.1340068 3 0 6.1340067 0 10 C -5.9211895e-16 13.865993 3.1340068 17 7 17 C 10.865993 17 14 13.865993 14 10 C 14 6.1340067 10.865993 3 7 3 z M 6 5.25 L 8.125 5.25 L 8.125 8.625 L 7.8125 11.0625 L 6.3125 11.0625 L 6 8.625 L 6 5.25 z M 6 11.90625 L 8.125 11.90625 L 8.125 14 L 6 14 L 6 11.90625 z "
+       transform="translate(0,1032.3622)"
+       id="path3751" />
+    <g
+       style="font-size:13px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans Bold"
+       id="text3753" />
+  </g>
+</svg>
diff --git a/motioneye/static/js/css-browser-selector.js b/motioneye/static/js/css-browser-selector.js
new file mode 100644 (file)
index 0000000..79759df
--- /dev/null
@@ -0,0 +1,14 @@
+/*
+CSS Browser Selector js v0.5.3 (July 2, 2013)
+
+-- original --
+Rafael Lima (http://rafael.adm.br)
+http://rafael.adm.br/css_browser_selector
+License: http://creativecommons.org/licenses/by/2.5/
+Contributors: http://rafael.adm.br/css_browser_selector#contributors
+-- /original --
+
+Fork project: http://code.google.com/p/css-browser-selector/
+Song Hyo-Jin (shj at xenosi.de)
+*/
+function css_browser_selector(n){var b=n.toLowerCase(),f=function(c){return b.indexOf(c)>-1},h="gecko",k="webkit",p="safari",j="chrome",d="opera",e="mobile",l=0,a=window.devicePixelRatio?(window.devicePixelRatio+"").replace(".","_"):"1";var i=[(!(/opera|webtv/.test(b))&&/msie\s(\d+)/.test(b)&&(l=RegExp.$1*1))?("ie ie"+l+((l==6||l==7)?" ie67 ie678 ie6789":(l==8)?" ie678 ie6789":(l==9)?" ie6789 ie9m":(l>9)?" ie9m":"")):(/firefox\/(\d+)\.(\d+)/.test(b)&&(re=RegExp))?h+" ff ff"+re.$1+" ff"+re.$1+"_"+re.$2:f("gecko/")?h:f(d)?d+(/version\/(\d+)/.test(b)?" "+d+RegExp.$1:(/opera(\s|\/)(\d+)/.test(b)?" "+d+RegExp.$2:"")):f("konqueror")?"konqueror":f("blackberry")?e+" blackberry":(f(j)||f("crios"))?k+" "+j:f("iron")?k+" iron":!f("cpu os")&&f("applewebkit/")?k+" "+p:f("mozilla/")?h:"",f("android")?e+" android":"",f("tablet")?"tablet":"",f("j2me")?e+" j2me":f("ipad; u; cpu os")?e+" chrome android tablet":f("ipad;u;cpu os")?e+" chromedef android tablet":f("iphone")?e+" ios iphone":f("ipod")?e+" ios ipod":f("ipad")?e+" ios ipad tablet":f("mac")?"mac":f("darwin")?"mac":f("webtv")?"webtv":f("win")?"win"+(f("windows nt 6.0")?" vista":""):f("freebsd")?"freebsd":(f("x11")||f("linux"))?"linux":"",(a!="1")?" retina ratio"+a:"","js portrait"].join(" ");if(window.jQuery&&!window.jQuery.browser){window.jQuery.browser=l?{msie:1,version:l}:{}}return i}(function(j,b){var c=css_browser_selector(navigator.userAgent);var g=j.documentElement;g.className+=" "+c;var a=c.replace(/^\s*|\s*$/g,"").split(/ +/);b.CSSBS=1;for(var f=0;f<a.length;f++){b["CSSBS_"+a[f]]=1}var e=function(d){return j.documentElement[d]||j.body[d]};if(b.jQuery){(function(q){var h="portrait",k="landscape";var i="smartnarrow",u="smartwide",x="tabletnarrow",r="tabletwide",w=i+" "+u+" "+x+" "+r+" pc";var v=q(g);var s=0,o=0;function d(){try{var l=e("clientWidth"),p=e("clientHeight");if(l>p){v.removeClass(h).addClass(k)}else{v.removeClass(k).addClass(h)}if(l==o){return}o=l;clearTimeout(s)}catch(m){}s=setTimeout(n,100)}function n(){try{v.removeClass(w);v.addClass((o<=360)?i:(o<=640)?u:(o<=768)?x:(o<=1024)?r:"pc")}catch(l){}}q(b).on("resize orientationchange",d).trigger("resize")})(b.jQuery)}})(document,window);
diff --git a/motioneye/static/js/frame.js b/motioneye/static/js/frame.js
new file mode 100644 (file)
index 0000000..276e100
--- /dev/null
@@ -0,0 +1,150 @@
+
+var refreshDisabled = false;
+
+
+    /* camera frame */
+
+function setupCameraFrame() {
+    var cameraFrameDiv = $('div.camera-frame')
+    var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
+    var cameraProgress = cameraFrameDiv.find('div.camera-progress');
+    var cameraImg = cameraFrameDiv.find('img.camera');
+    var cameraId = cameraFrameDiv.attr('id').substring(6);
+    var progressImg = cameraFrameDiv.find('img.camera-progress');
+    var body = $('body');
+    
+    cameraFrameDiv[0].refreshDivider = 0;
+    cameraFrameDiv[0].streamingFramerate = parseInt(cameraFrameDiv.attr('streaming_framerate')) || 1;
+    cameraFrameDiv[0].streamingServerResize = cameraFrameDiv.attr('streaming_server_resize') == 'True';
+    cameraFrameDiv[0].proto = cameraFrameDiv.attr('proto');
+    cameraFrameDiv[0].url = cameraFrameDiv.attr('url');
+    progressImg.attr('src', staticUrl + 'img/camera-progress.gif');
+    
+    cameraProgress.addClass('visible');
+    cameraPlaceholder.css('opacity', '0');
+    
+    /* fade in */
+    cameraFrameDiv.animate({'opacity': 1}, 100);
+    
+    /* error and load handlers */
+    cameraImg.error(function () {
+        this.error = true;
+        this.loading = 0;
+        
+        cameraImg.addClass('error').removeClass('loading');
+        cameraPlaceholder.css('opacity', 1);
+        cameraProgress.removeClass('visible');
+        cameraFrameDiv.removeClass('motion-detected');
+    });
+    cameraImg.load(function () {
+        if (refreshDisabled) {
+            return; /* refresh temporarily disabled for updating */
+        }
+        
+        this.error = false;
+        this.loading = 0;
+        
+        cameraImg.removeClass('error').removeClass('loading');
+        cameraPlaceholder.css('opacity', 0);
+        cameraProgress.removeClass('visible');
+        
+        /* there's no point in looking for a cookie update more often than once every second */
+        var now = new Date().getTime();
+        if ((!this.lastCookieTime || now - this.lastCookieTime > 1000) && (cameraFrameDiv[0].proto != 'mjpeg')) {
+            if (getCookie('motion_detected_' + cameraId) == 'true') {
+                cameraFrameDiv.addClass('motion-detected');
+            }
+            else {
+                cameraFrameDiv.removeClass('motion-detected');
+            }
+            
+            this.lastCookieTime = now;
+        }
+
+        if (this.naturalWidth / this.naturalHeight > body.width() / body.height()) {
+            cameraImg.css('width', '100%');
+            cameraImg.css('height', 'auto');
+        }
+        else {
+            cameraImg.css('width', 'auto');
+            cameraImg.css('height', '100%');
+        }
+    });
+    
+    cameraImg.addClass('loading');
+}
+
+function refreshCameraFrame() {
+    var $cameraFrame = $('div.camera-frame');
+    var cameraFrame = $cameraFrame[0];
+    var img = $cameraFrame.find('img.camera')[0];
+    var cameraId = cameraFrame.id.substring(6);
+    
+    if (cameraFrame.proto == 'mjpeg') {
+        /* no manual refresh for simple mjpeg cameras */
+        var url = cameraFrame.url.replace('127.0.0.1', window.location.host.split(':')[0]);
+        url += (url.indexOf('?') > 0 ? '&' : '?') + '_=' + new Date().getTime();
+        img.src = url;
+        return; 
+    }
+    
+    /* at a refresh interval of 50ms, the refresh rate is limited to 20 fps */
+    var count = 1000 / (refreshInterval * cameraFrame.streamingFramerate);
+    if (count <= 2) {
+        /* skipping frames (showing the same frame twice) at this rate won't be visible,
+         * while the effective framerate will be as close as possible to the motion's one */
+        count -= 1;
+    }
+    
+    if (img.error) {
+        /* in case of error, decrease the refresh rate to 1 fps */
+        count = 1000 / refreshInterval;
+    }
+    
+    if (cameraFrame.refreshDivider < count) {
+        cameraFrame.refreshDivider++;
+    }
+    else {
+        (function () {
+            if (refreshDisabled) {
+                /* camera refreshing disabled, retry later */
+                
+                return;
+            }
+            
+            if (img.loading) {
+                img.loading++; /* increases each time the camera would refresh but is still loading */
+                
+                if (img.loading > 2 * 1000 / refreshInterval) { /* limits the retry at one every two seconds */
+                    img.loading = 0;
+                }
+                else {
+                    return; /* wait for the previous frame to finish loading */
+                }
+            }
+            
+            var timestamp = new Date().getTime();
+            var uri = baseUri + 'picture/' + cameraId + '/current/?_=' + timestamp;
+            if (cameraFrame.serverSideResize) {
+                uri += '&width=' + img.width;
+            }
+            
+            uri = addAuthParams('GET', uri);
+            img.src = uri;
+            img.loading = 1;
+            
+            cameraFrame.refreshDivider = 0;
+        })();
+    }
+
+    setTimeout(refreshCameraFrame, refreshInterval);
+}
+
+
+    /* startup function */
+
+$(document).ready(function () {
+    setupCameraFrame();
+    refreshCameraFrame();
+});
+
diff --git a/motioneye/static/js/jquery.min.js b/motioneye/static/js/jquery.min.js
new file mode 100644 (file)
index 0000000..ce1b6b6
--- /dev/null
@@ -0,0 +1,5 @@
+/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+*/
+(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
+}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
+u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/motioneye/static/js/jquery.mousewheel.js b/motioneye/static/js/jquery.mousewheel.js
new file mode 100644 (file)
index 0000000..9d65c71
--- /dev/null
@@ -0,0 +1,117 @@
+/*! Copyright (c) 2013 Brandon Aaron (http://brandonaaron.net)
+ * Licensed under the MIT License (LICENSE.txt).
+ *
+ * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
+ * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
+ * Thanks to: Seamus Leahy for adding deltaX and deltaY
+ *
+ * Version: 3.1.3
+ *
+ * Requires: 1.2.2+
+ */
+
+(function (factory) {
+    if ( typeof define === 'function' && define.amd ) {
+        // AMD. Register as an anonymous module.
+        define(['jquery'], factory);
+    } else if (typeof exports === 'object') {
+        // Node/CommonJS style for Browserify
+        module.exports = factory;
+    } else {
+        // Browser globals
+        factory(jQuery);
+    }
+}(function ($) {
+
+    var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];
+    var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
+    var lowestDelta, lowestDeltaXY;
+
+    if ( $.event.fixHooks ) {
+        for ( var i = toFix.length; i; ) {
+            $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;
+        }
+    }
+
+    $.event.special.mousewheel = {
+        setup: function() {
+            if ( this.addEventListener ) {
+                for ( var i = toBind.length; i; ) {
+                    this.addEventListener( toBind[--i], handler, false );
+                }
+            } else {
+                this.onmousewheel = handler;
+            }
+        },
+
+        teardown: function() {
+            if ( this.removeEventListener ) {
+                for ( var i = toBind.length; i; ) {
+                    this.removeEventListener( toBind[--i], handler, false );
+                }
+            } else {
+                this.onmousewheel = null;
+            }
+        }
+    };
+
+    $.fn.extend({
+        mousewheel: function(fn) {
+            return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
+        },
+
+        unmousewheel: function(fn) {
+            return this.unbind("mousewheel", fn);
+        }
+    });
+
+
+    function handler(event) {
+        var orgEvent = event || window.event,
+            args = [].slice.call(arguments, 1),
+            delta = 0,
+            deltaX = 0,
+            deltaY = 0,
+            absDelta = 0,
+            absDeltaXY = 0,
+            fn;
+        event = $.event.fix(orgEvent);
+        event.type = "mousewheel";
+
+        // Old school scrollwheel delta
+        if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; }
+        if ( orgEvent.detail )     { delta = orgEvent.detail * -1; }
+
+        // New school wheel delta (wheel event)
+        if ( orgEvent.deltaY ) {
+            deltaY = orgEvent.deltaY * -1;
+            delta  = deltaY;
+        }
+        if ( orgEvent.deltaX ) {
+            deltaX = orgEvent.deltaX;
+            delta  = deltaX * -1;
+        }
+
+        // Webkit
+        if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; }
+        if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX * -1; }
+
+        // Look for lowest delta to normalize the delta values
+        absDelta = Math.abs(delta);
+        if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; }
+        absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX));
+        if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; }
+
+        // Get a whole value for the deltas
+        fn = delta > 0 ? 'floor' : 'ceil';
+        delta  = Math[fn](delta / lowestDelta);
+        deltaX = Math[fn](deltaX / lowestDeltaXY);
+        deltaY = Math[fn](deltaY / lowestDeltaXY);
+
+        // Add event and delta to the front of the arguments
+        args.unshift(event, delta, deltaX, deltaY);
+
+        return ($.event.dispatch || $.event.handle).apply(this, args);
+    }
+
+}));
diff --git a/motioneye/static/js/jquery.timepicker.min.js b/motioneye/static/js/jquery.timepicker.min.js
new file mode 100644 (file)
index 0000000..72b6b11
--- /dev/null
@@ -0,0 +1 @@
+!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){function b(a){if(a.minTime&&(a.minTime=s(a.minTime)),a.maxTime&&(a.maxTime=s(a.maxTime)),a.durationTime&&"function"!=typeof a.durationTime&&(a.durationTime=s(a.durationTime)),a.disableTimeRanges.length>0){for(var b in a.disableTimeRanges)a.disableTimeRanges[b]=[s(a.disableTimeRanges[b][0]),s(a.disableTimeRanges[b][1])];a.disableTimeRanges=a.disableTimeRanges.sort(function(a,b){return a[0]-b[0]})}return a}function c(b){var c=b.data("timepicker-settings"),d=b.data("timepicker-list");d&&d.length&&(d.remove(),b.data("timepicker-list",!1)),d=a("<ul />",{"class":"ui-timepicker-list"});var e=a("<div />",{"class":"ui-timepicker-wrapper",tabindex:-1});e.css({display:"none",position:"absolute"}).append(d),c.className&&e.addClass(c.className),null===c.minTime&&null===c.durationTime||!c.showDuration||e.addClass("ui-timepicker-with-duration");var f=c.minTime;"function"==typeof c.durationTime?f=s(c.durationTime()):null!==c.durationTime&&(f=c.durationTime);var h=null!==c.minTime?c.minTime:0,j=null!==c.maxTime?c.maxTime:h+v-1;h>=j&&(j+=v);for(var k=c.disableTimeRanges,l=0,m=k.length,n=h;j>=n;n+=60*c.step){var o=n%v,t=a("<li />");if(t.data("time",o),t.text(r(o,c.timeFormat)),(null!==c.minTime||null!==c.durationTime)&&c.showDuration){var u=a("<span />");u.addClass("ui-timepicker-duration"),u.text(" ("+q(n-f)+")"),t.append(u)}m>l&&(o>=k[l][1]&&(l+=1),k[l]&&o>=k[l][0]&&o<k[l][1]&&t.addClass("ui-timepicker-disabled")),d.append(t)}e.data("timepicker-input",b),b.data("timepicker-list",e);var w=c.appendTo;"string"==typeof w?w=a(w):"function"==typeof w&&(w=w(b)),w.append(e),i(b,d),d.on("click","li",function(){b.off("focus.timepicker"),b.on("focus.timepicker-ie-hack",function(){b.off("focus.timepicker-ie-hack"),b.on("focus.timepicker",y.show)}),g(b)||b[0].focus(),d.find("li").removeClass("ui-timepicker-selected"),a(this).addClass("ui-timepicker-selected"),p(b)&&(b.trigger("hideTimepicker"),e.hide())})}function d(){return new Date(1970,1,1,0,0,0)}function e(b){"ontouchstart"in document?a("body").on("touchstart.ui-timepicker",f):(a("body").on("mousedown.ui-timepicker",f),b.closeOnWindowScroll&&a(window).on("scroll.ui-timepicker",f))}function f(b){var c=a(b.target),d=c.closest(".ui-timepicker-input");0===d.length&&0===c.closest(".ui-timepicker-wrapper").length&&(y.hide(),a("body").unbind(".ui-timepicker"),a(window).unbind(".ui-timepicker"))}function g(a){var b=a.data("timepicker-settings");return(window.navigator.msMaxTouchPoints||"ontouchstart"in document)&&b.disableTouchKeyboard}function h(b,c,d){if(!d&&0!==d)return!1;var e=b.data("timepicker-settings"),f=!1,g=30*e.step;return c.find("li").each(function(b,c){var e=a(c),h=e.data("time")-d;return Math.abs(h)<g||h==g?(f=e,!1):void 0}),f}function i(a,b){b.find("li").removeClass("ui-timepicker-selected");var c=s(k(a));if(c){var d=h(a,b,c);if(d){var e=d.offset().top-b.offset().top;(e+d.outerHeight()>b.outerHeight()||0>e)&&b.scrollTop(b.scrollTop()+d.position().top-d.outerHeight()),d.addClass("ui-timepicker-selected")}}}function j(){if(""!==this.value){var b=a(this),c=b.data("timepicker-list");if(!c||!c.is(":visible")){var d=s(this.value);if(null===d)return b.trigger("timeFormatError"),void 0;var e=b.data("timepicker-settings"),f=!1;if(null!==e.minTime&&d<e.minTime?f=!0:null!==e.maxTime&&d>e.maxTime&&(f=!0),a.each(e.disableTimeRanges,function(){return d>=this[0]&&d<this[1]?(f=!0,!1):void 0}),e.forceRoundTime){var g=d%(60*e.step);g>=30*e.step?d+=60*e.step-g:d-=g}var h=r(d,e.timeFormat);f?l(b,h,"error")&&b.trigger("timeRangeError"):l(b,h)}}}function k(a){return a.is("input")?a.val():a.data("ui-timepicker-value")}function l(a,b,c){return a.data("ui-timepicker-value")!=b?(a.data("ui-timepicker-value",b),a.is("input")&&a.val(b),"select"==c?a.trigger("selectTime").trigger("changeTime").trigger("change"):"error"!=c&&a.trigger("changeTime"),!0):(a.trigger("selectTime"),!1)}function m(b){var c=a(this),d=c.data("timepicker-list");if(!d||!d.is(":visible")){if(40!=b.keyCode)return n(b,c);g(c)||c.focus()}switch(b.keyCode){case 13:return p(c)&&y.hide.apply(this),b.preventDefault(),!1;case 38:var e=d.find(".ui-timepicker-selected");return e.length?e.is(":first-child")||(e.removeClass("ui-timepicker-selected"),e.prev().addClass("ui-timepicker-selected"),e.prev().position().top<e.outerHeight()&&d.scrollTop(d.scrollTop()-e.outerHeight())):(d.find("li").each(function(b,c){return a(c).position().top>0?(e=a(c),!1):void 0}),e.addClass("ui-timepicker-selected")),!1;case 40:return e=d.find(".ui-timepicker-selected"),0===e.length?(d.find("li").each(function(b,c){return a(c).position().top>0?(e=a(c),!1):void 0}),e.addClass("ui-timepicker-selected")):e.is(":last-child")||(e.removeClass("ui-timepicker-selected"),e.next().addClass("ui-timepicker-selected"),e.next().position().top+2*e.outerHeight()>d.outerHeight()&&d.scrollTop(d.scrollTop()+e.outerHeight())),!1;case 27:d.find("li").removeClass("ui-timepicker-selected"),y.hide();break;case 9:y.hide();break;default:return n(b,c)}}function n(a,b){return!b.data("timepicker-settings").disableTextInput||a.ctrlKey||a.altKey||a.metaKey||2!=a.keyCode&&a.keyCode<46}function o(b){var c=a(this),d=c.data("timepicker-list");if(!d||!d.is(":visible"))return!0;switch(b.keyCode){case 96:case 97:case 98:case 99:case 100:case 101:case 102:case 103:case 104:case 105:case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 65:case 77:case 80:case 186:case 8:case 46:i(c,d);break;default:return}}function p(a){var b=a.data("timepicker-settings"),c=a.data("timepicker-list"),d=null,e=c.find(".ui-timepicker-selected");if(e.hasClass("ui-timepicker-disabled"))return!1;if(e.length?d=e.data("time"):k(a)&&(d=s(k(a)),i(a,c)),null!==d){var f=r(d,b.timeFormat);l(a,f,"select")}return!0}function q(a){var b,c=Math.round(a/60);if(Math.abs(c)<60)b=[c,x.mins];else if(60==c)b=["1",x.hr];else{var d=(c/60).toFixed(1);"."!=x.decimal&&(d=d.replace(".",x.decimal)),b=[d,x.hrs]}return b.join(" ")}function r(a,b){if(null!==a){for(var c,d,e=new Date(u.valueOf()+1e3*a),f="",g=0;g<b.length;g++)switch(d=b.charAt(g)){case"a":f+=e.getHours()>11?"pm":"am";break;case"A":f+=e.getHours()>11?"PM":"AM";break;case"g":c=e.getHours()%12,f+=0===c?"12":c;break;case"G":f+=e.getHours();break;case"h":c=e.getHours()%12,0!==c&&10>c&&(c="0"+c),f+=0===c?"12":c;break;case"H":c=e.getHours(),f+=c>9?c:"0"+c;break;case"i":var h=e.getMinutes();f+=h>9?h:"0"+h;break;case"s":a=e.getSeconds(),f+=a>9?a:"0"+a;break;default:f+=d}return f}}function s(a){if(""===a)return null;if(!a||a+0==a)return a;"object"==typeof a&&(a=a.getHours()+":"+t(a.getMinutes())+":"+t(a.getSeconds())),a=a.toLowerCase(),new Date(0);var b;if(-1===a.indexOf(":")?(b=a.match(/^([0-9]):?([0-5][0-9])?:?([0-5][0-9])?\s*([pa]?)m?$/),b||(b=a.match(/^([0-2][0-9]):?([0-5][0-9])?:?([0-5][0-9])?\s*([pa]?)m?$/))):b=a.match(/^(\d{1,2})(?::([0-5][0-9]))?(?::([0-5][0-9]))?\s*([pa]?)m?$/),!b)return null;var c,d=parseInt(1*b[1],10);c=b[4]?12==d?"p"==b[4]?12:0:d+("p"==b[4]?12:0):d;var e=1*b[2]||0,f=1*b[3]||0;return 3600*c+60*e+f}function t(a){return("0"+a).slice(-2)}var u=d(),v=86400,w={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,timeFormat:"g:ia",scrollDefaultNow:!1,scrollDefaultTime:!1,selectOnBlur:!1,disableTouchKeyboard:!0,forceRoundTime:!1,appendTo:"body",disableTimeRanges:[],closeOnWindowScroll:!1,disableTextInput:!1},x={decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},y={init:function(c){return this.each(function(){var d=a(this);if("SELECT"==d[0].tagName){for(var e={type:"text",value:d.val()},f=d[0].attributes,g=0;g<f.length;g++)e[f[g].nodeName]=f[g].nodeValue;var h=a("<input />",e);d.replaceWith(h),d=h}var i=a.extend({},w);c&&(i=a.extend(i,c)),i.lang&&(x=a.extend(x,i.lang)),i=b(i),d.data("timepicker-settings",i),d.prop("autocomplete","off"),d.on("click.timepicker focus.timepicker",y.show),d.on("change.timepicker",j),d.on("keydown.timepicker",m),d.on("keyup.timepicker",o),d.addClass("ui-timepicker-input"),j.call(d.get(0))})},show:function(){var b=a(this),d=b.data("timepicker-settings");g(b)&&b.blur();var f=b.data("timepicker-list");if(!b.prop("readonly")&&(f&&0!==f.length&&"function"!=typeof d.durationTime||(c(b),f=b.data("timepicker-list")),!f.is(":visible"))){y.hide(),f.show(),b.offset().top+b.outerHeight(!0)+f.outerHeight()>a(window).height()+a(window).scrollTop()?f.offset({left:b.offset().left+parseInt(f.css("marginLeft").replace("px",""),10),top:b.offset().top-f.outerHeight()+parseInt(f.css("marginTop").replace("px",""),10)}):f.offset({left:b.offset().left+parseInt(f.css("marginLeft").replace("px",""),10),top:b.offset().top+b.outerHeight()+parseInt(f.css("marginTop").replace("px",""),10)});var i=f.find(".ui-timepicker-selected");if(i.length||(k(b)?i=h(b,f,s(k(b))):d.scrollDefaultNow?i=h(b,f,s(new Date)):d.scrollDefaultTime!==!1&&(i=h(b,f,s(d.scrollDefaultTime)))),i&&i.length){var j=f.scrollTop()+i.position().top-i.outerHeight();f.scrollTop(j)}else f.scrollTop(0);e(d),b.trigger("showTimepicker")}},hide:function(){a(".ui-timepicker-wrapper:visible").each(function(){var b=a(this),c=b.data("timepicker-input"),d=c.data("timepicker-settings");d&&d.selectOnBlur&&p(c),b.hide(),c.trigger("hideTimepicker")})},option:function(c,d){var e=this,f=e.data("timepicker-settings"),g=e.data("timepicker-list");if("object"==typeof c)f=a.extend(f,c);else if("string"==typeof c&&"undefined"!=typeof d)f[c]=d;else if("string"==typeof c)return f[c];return f=b(f),e.data("timepicker-settings",f),g&&(g.remove(),e.data("timepicker-list",!1)),e},getSecondsFromMidnight:function(){return s(k(this))},getTime:function(){var a=this,b=new Date;return b.setHours(0,0,0,0),new Date(b.valueOf()+1e3*s(k(a)))},setTime:function(a){var b=this,c=r(s(a),b.data("timepicker-settings").timeFormat);l(b,c)},remove:function(){var a=this;a.hasClass("ui-timepicker-input")&&(a.removeAttr("autocomplete","off"),a.removeClass("ui-timepicker-input"),a.removeData("timepicker-settings"),a.off(".timepicker"),a.data("timepicker-list")&&a.data("timepicker-list").remove(),a.removeData("timepicker-list"))}};a.fn.timepicker=function(b){return y[b]?y[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?(a.error("Method "+b+" does not exist on jQuery.timepicker"),void 0):y.init.apply(this,arguments)}});
\ No newline at end of file
diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js
new file mode 100644 (file)
index 0000000..1048701
--- /dev/null
@@ -0,0 +1,3928 @@
+
+var pushConfigs = {};
+var pushConfigReboot = false;
+var refreshDisabled = {}; /* dictionary indexed by cameraId, tells if refresh is disabled for a given camera */
+var fullScreenCameraId = null;
+var inProgress = false;
+var refreshInterval = 50; /* milliseconds */
+var username = '';
+var password = '';
+var baseUri = null;
+var signatureRegExp = new RegExp('[^a-zA-Z0-9/?_.=&{}\\[\\]":, _-]', 'g');
+var initialConfigFetched = false; /* used to workaround browser extensions that trigger stupid change events */
+
+
+    /* utils */
+
+var sha1 = (function () {
+    function hash(msg) {
+        var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
+
+        msg += String.fromCharCode(0x80);
+
+        var l = msg.length / 4 + 2;
+        var N = Math.ceil(l / 16);
+        var M = new Array(N);
+
+        for (var i = 0; i < N; i++) {
+            M[i] = new Array(16);
+            for (var j = 0; j < 16; j++) {
+                M[i][j] = (msg.charCodeAt(i * 64 + j * 4) << 24) | (msg.charCodeAt(i * 64 + j * 4 + 1) << 16) | 
+                (msg.charCodeAt(i * 64 + j * 4 + 2) << 8) | (msg.charCodeAt(i * 64 + j * 4 + 3));
+            }
+        }
+        M[N-1][14] = ((msg.length-1) * 8) / Math.pow(2, 32);
+        M[N-1][14] = Math.floor(M[N-1][14]);
+        M[N-1][15] = ((msg.length-1) * 8) & 0xffffffff;
+
+        var H0 = 0x67452301;
+        var H1 = 0xefcdab89;
+        var H2 = 0x98badcfe;
+        var H3 = 0x10325476;
+        var H4 = 0xc3d2e1f0;
+
+        var W = new Array(80);
+        var a, b, c, d, e;
+        for (var i = 0; i < N; i++) {
+            for (var t = 0; t < 16; t++) W[t] = M[i][t];
+            for (var t = 16; t < 80; t++) W[t] = ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
+
+            a = H0; b = H1; c = H2; d = H3; e = H4;
+
+            for (var t = 0; t < 80; t++) {
+                var s = Math.floor(t / 20);
+                var T = (ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t]) & 0xffffffff;
+                e = d;
+                d = c;
+                c = ROTL(b, 30);
+                b = a;
+                a = T;
+            }
+
+            H0 = (H0 + a) & 0xffffffff;
+            H1 = (H1 + b) & 0xffffffff; 
+            H2 = (H2 + c) & 0xffffffff; 
+            H3 = (H3 + d) & 0xffffffff; 
+            H4 = (H4 + e) & 0xffffffff;
+        }
+
+        return toHexStr(H0) + toHexStr(H1) + toHexStr(H2) + toHexStr(H3) + toHexStr(H4);
+    }
+
+    function f(s, x, y, z)  {
+        switch (s) {
+            case 0: return (x & y) ^ (~x & z);
+            case 1: return x ^ y ^ z;
+            case 2: return (x & y) ^ (x & z) ^ (y & z);
+            case 3: return x ^ y ^ z;
+        }
+    }
+
+    function ROTL(x, n) {
+        return (x << n) | (x >>> (32 - n));
+    }
+
+    function toHexStr(n) {
+        var s = "", v;
+        for (var i = 7; i >= 0; i--) {
+            v = (n >>> (i * 4)) & 0xf;
+            s += v.toString(16);
+        }
+        return s;
+    }
+    
+    return hash;
+}());
+
+function splitUrl(url) {
+    if (!url) {
+        url = window.location.href;
+    }
+    
+    var parts = url.split('?');
+    if (parts.length < 2 || parts[1].length === 0) {
+        return {baseUrl: parts[0], params: {}};
+    }
+    
+    var baseUrl = parts[0];
+    var paramStr = parts[1];
+    
+    parts = paramStr.split('&');
+    var params = {};
+    
+    for (var i = 0; i < parts.length; i++) {
+        var pair = parts[i].split('=');
+        params[pair[0]] = pair[1];
+    }
+    
+    return {baseUrl: baseUrl, params: params};
+}
+
+function qualifyUrl(url) {
+    var a = document.createElement('a');
+    a.href = url;
+    return a.href;
+}
+
+function qualifyUri(uri) {
+    var url = qualifyUrl(uri);
+    var pos = url.indexOf('//');
+    if (pos === -1) { /* not a full url */
+        return url;
+    }
+    
+    url = url.substring(pos + 2);
+    pos = url.indexOf('/');
+    if (pos === -1) { /* root with no trailing slash */
+        return '';
+    }
+    
+    return url.substring(pos);
+}
+        
+function computeSignature(method, uri, body) {
+    uri = qualifyUri(uri);
+    
+    var parts = splitUrl(uri);
+    var query = parts.params;
+    var baseUrl = parts.baseUrl;
+    
+    /* sort query arguments alphabetically */
+    query = Object.keys(query).map(function (key) {return {key: key, value: decodeURIComponent(query[key])};});
+    query = query.filter(function (q) {return q.key !== '_signature';});
+    query.sortKey(function (q) {return q.key;});
+    query = query.map(function (q) {return q.key + '=' + encodeURIComponent(q.value);}).join('&');
+    uri = baseUrl + '?' + query;
+    uri = uri.replace(signatureRegExp, '-');
+    body = body && body.replace(signatureRegExp, '-');
+    var password = window.password.replace(signatureRegExp, '-');
+    
+    return sha1(method + ':' + uri + ':' + (body || '') + ':' + password).toLowerCase();
+}
+
+function addAuthParams(method, url, body) {
+    if (!window.username) {
+        return url;
+    }
+
+    if (url.indexOf('?') < 0) {
+        url += '?';
+    }
+    else {
+        url += '&';
+    }
+    
+    url += '_username=' + window.username;
+    if (window._loginDialogSubmitted) {
+        url += '&_login=true';
+        delete _loginDialogSubmitted;
+    }
+    var signature = computeSignature(method, url, body);
+    url += '&_signature=' + signature;
+
+    return url;
+}
+
+function isAdmin() {
+    return username === adminUsername;
+}
+
+function ajax(method, url, data, callback, error, timeout) {
+    var origUrl = url;
+    var origData = data;
+    
+    if (url.indexOf('?') < 0) {
+        url += '?';
+    }
+    else {
+        url += '&';
+    }
+    
+    url += '_=' + new Date().getTime();
+
+    var json = false;
+    var processData = true;
+    if (method == 'POST') {
+        if (window.FormData && (data instanceof FormData)) {
+            json = false;
+            processData = false;
+        }
+        else if (typeof data == 'object') {
+            data = JSON.stringify(data);
+            json = true;
+        }
+    }
+    else { /* assuming GET */
+        if (data) {
+            url += '&' + $.param(data);
+            data = null;
+        }
+    }
+    
+    url = addAuthParams(method, url, processData ? data : null);
+    
+    var options = {
+        type: method,
+        url: url,
+        data: data,
+        timeout: timeout || 300 * 1000,
+        success: function (data) {
+            if (data && data.error == 'unauthorized') {
+                if (data.prompt) {
+                    runLoginDialog(function () {
+                        ajax(method, origUrl, origData, callback, error);
+                    });
+                }
+                
+                window._loginRetry = true;
+            }
+            else {
+                delete window._loginRetry;
+                if (callback) {
+                    $('body').toggleClass('admin', isAdmin());
+                    callback(data);
+                }
+            }
+        },
+        contentType: json ? 'application/json' : false,
+        processData: processData,
+        error: error || function (request, options, error) {
+            showErrorMessage();
+            if (callback) {
+                callback();
+            }
+        }
+    };
+    
+    $.ajax(options);
+}
+
+Object.keys = Object.keys || (function () {
+    var hasOwnProperty = Object.prototype.hasOwnProperty;
+    var hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString');
+    var dontEnums = [
+        'toString',
+        'toLocaleString',
+        'valueOf',
+        'hasOwnProperty',
+        'isPrototypeOf',
+        'propertyIsEnumerable',
+        'constructor'
+    ];
+    var dontEnumsLength = dontEnums.length;
+
+    return function (obj) {
+        if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
+            return [];
+        }
+
+        var result = [];
+        for (var prop in obj) {
+            if (hasOwnProperty.call(obj, prop)) {
+                result.push(prop);
+            }
+        }
+
+        if (hasDontEnumBug) {
+            for (var i = 0; i < dontEnumsLength; i++) {
+                if (hasOwnProperty.call(obj, dontEnums[i])) {
+                    result.push(dontEnums[i]);
+                }
+            }
+        }
+        
+        return result;
+    };
+})();
+
+Object.update = function (dest, source) {
+    for (var key in source) {
+        if (!source.hasOwnProperty(key)) {
+            continue;
+        }
+        
+        dest[key] = source[key];
+    }
+};
+
+Array.prototype.indexOf = Array.prototype.indexOf || function (obj) {
+    for (var i = 0; i < this.length; i++) {
+        if (this[i] === obj) {
+            return i;
+        }
+    }
+    
+    return -1;
+};
+
+Array.prototype.forEach = Array.prototype.forEach || function (callback, thisArg) {
+    for (var i = 0; i < this.length; i++) {
+        callback.call(thisArg, this[i], i, this);
+    }
+};
+
+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) {
+        if (uniqueElements.indexOf(element, Utils.equals) === -1) {
+            uniqueElements.push(element);
+        }
+    });
+    
+    return uniqueElements;
+};
+
+Array.prototype.filter = function (func, thisArg) {
+    var filtered = [];
+    for (var i = 0; i < this.length; i++) {
+        if (func.call(thisArg, this[i], i, this)) {
+            filtered.push(this[i]);
+        }
+    }
+    
+    return filtered;
+};
+
+Array.prototype.map = function (func, thisArg) {
+    var mapped = [];
+    for (var i = 0; i < this.length; i++) {
+        mapped.push(func.call(thisArg, this[i], i, this));
+    }
+    
+    return mapped;
+};
+
+Array.prototype.sortKey = function (keyFunc, reverse) {
+    this.sort(function (e1, e2) {
+        var k1 = keyFunc(e1);
+        var k2 = keyFunc(e2);
+        
+        if ((k1 < k2 && !reverse) || (k1 > k2 && reverse)) {
+            return -1;
+        }
+        else if ((k1 > k2 && !reverse) || (k1 < k2 && reverse)) {
+            return 1;
+        }
+        else {
+            return 0;
+        }
+    });
+};
+
+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) {
+        s = s.substring(0, p) + newStr + s.substring(p + oldStr.length, s.length);
+    }
+    
+    return s.toString();
+};
+
+function getCookie(name) {
+    if (document.cookie.length <= 0) {
+        return null;
+    }
+
+    var start = document.cookie.indexOf(name + '=');
+    if (start == -1) {
+        return null;
+    }
+     
+    var start = start + name.length + 1;
+    var end = document.cookie.indexOf(';', start);
+    if (end == -1) {
+        end = document.cookie.length;
+    }
+    
+    return unescape(document.cookie.substring(start, end));
+}
+
+function setCookie(name, value, days) {
+    var date, expires;
+    if (days) {
+        date = new Date();
+        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+        expires = 'expires=' + date.toGMTString();
+    }
+    else {
+        expires = '';
+    }
+
+    document.cookie = name + '=' + value + '; ' + expires + '; path=/';
+}
+
+function remCookie(name) {
+    document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
+}
+
+function showErrorMessage(message) {
+    if (message == null || message == true) {
+        message = 'An error occurred. Refreshing is recommended.';
+    }
+    
+    showPopupMessage(message, 'error');
+}
+
+function doLogout() {
+    setCookie('username', '_');
+    window.location.reload(true);
+}
+
+
+/* UI initialization */
+
+function initUI() {
+    /* checkboxes */
+    makeCheckBox($('input[type=checkbox].styled'));
+
+    /* sliders */
+    $('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($('div.progress-bar'));
+
+    /* text validators */
+    makeTextValidator($('tr[required=true] input[type=text]'), true);
+    makeTextValidator($('tr[required=true] input[type=password]'), true);
+
+    /* number validators */
+    $('input[type=text].number').each(function () {
+        var $this = $(this);
+        var $tr = $this.parent().parent();
+        makeNumberValidator($this, Number($tr.attr('min')), Number($tr.attr('max')),
+                Boolean($tr.attr('floating')), Boolean($tr.attr('sign')), Boolean($tr.attr('required')));
+    });
+
+    /* time validators */
+    makeTimeValidator($('input[type=text].time'));
+    
+    /* custom validators */
+    makeCustomValidator($('#deviceNameEntry'), function (value) {
+        if (!value) {
+            return 'this field is required';
+        }
+
+        if (!value.toLowerCase().match(new RegExp('^[a-z0-9\-\_\+\ ]*$'))) {
+            return "special characters are not allowed in camera's name";
+        }
+        
+        return true;
+    }, '');
+    makeCustomValidator($('#rootDirectoryEntry'), function (value) {
+        if ($('#storageDeviceSelect').val() == 'custom-path' && String(value).trim() == '/') {
+            return 'files cannot be created directly on the root of your system';
+        }
+        
+        return true;
+    }, '');
+    makeCustomValidator($('#emailAddressesEntry'), function (value) {
+        if (!value.toLowerCase().match(new RegExp('^[a-z0-9\-\_\+\.\@\^\~\, ]+$'))) {
+            return 'enter a list of comma-separated valid email addresses';
+        }
+        
+        return true;
+    }, '');
+    $('tr[validate] input[type=text]').each(function () {
+        var $this = $(this);
+        var $tr = $this.parent().parent();
+        var required = $tr.attr('required');
+        var validate = $tr.attr('validate');
+        if (!validate) {
+            return;
+        }
+
+        makeCustomValidator($this, function (value) {
+            if (!value && required) {
+                return 'this field is required';
+            }
+
+            if (!value.toLowerCase().match(new RegExp(validate))) {
+                return 'enter a valid value';
+            }
+
+            return true;
+        }, '');
+    });
+    
+    /* input value processors */
+    makeStrippedInput($('tr[strip=true] input[type=text]'));
+    makeStrippedInput($('tr[strip=true] input[type=password]'));
+    
+    function checkMinimizeSection() {
+        var $switch = $(this);
+        var $sectionDiv = $switch.parents('div.settings-section-title:eq(0)');
+        
+        var $minimizeSpan = $switch.parent().find('span.minimize');
+        if ($switch.is(':checked') && !$minimizeSpan.hasClass('open')) {
+            $minimizeSpan.addClass('open');
+        }
+        else if (!$switch.is(':checked') && $minimizeSpan.hasClass('open') && !$sectionDiv.attr('minimize-switch-independent')) {
+            $minimizeSpan.removeClass('open');
+        }
+    }
+
+    /* ui elements that enable/disable other ui elements */
+    $('#motionEyeSwitch').change(updateConfigUi);
+    $('#showAdvancedSwitch').change(updateConfigUi);
+    $('#storageDeviceSelect').change(updateConfigUi);
+    $('#resolutionSelect').change(updateConfigUi);
+    $('#leftTextSelect').change(updateConfigUi);
+    $('#rightTextSelect').change(updateConfigUi);
+    $('#captureModeSelect').change(updateConfigUi);
+    $('#autoNoiseDetectSwitch').change(updateConfigUi);
+    $('#videoDeviceSwitch').change(checkMinimizeSection).change(updateConfigUi);
+    $('#textOverlaySwitch').change(checkMinimizeSection).change(updateConfigUi);
+    $('#videoStreamingSwitch').change(checkMinimizeSection).change(updateConfigUi);
+    $('#streamingServerResizeSwitch').change(updateConfigUi);
+    $('#stillImagesSwitch').change(checkMinimizeSection).change(updateConfigUi);
+    $('#preservePicturesSelect').change(updateConfigUi);
+    $('#motionDetectionSwitch').change(checkMinimizeSection).change(updateConfigUi);
+    $('#motionMoviesSwitch').change(checkMinimizeSection).change(updateConfigUi);
+    $('#preserveMoviesSelect').change(updateConfigUi);
+    $('#emailNotificationsSwitch').change(updateConfigUi);
+    $('#webHookNotificationsSwitch').change(updateConfigUi);
+    $('#commandNotificationsSwitch').change(updateConfigUi);
+    $('#workingScheduleSwitch').change(checkMinimizeSection).change(updateConfigUi);
+    
+    $('#mondayEnabledSwitch').change(updateConfigUi);
+    $('#tuesdayEnabledSwitch').change(updateConfigUi);
+    $('#wednesdayEnabledSwitch').change(updateConfigUi);
+    $('#thursdayEnabledSwitch').change(updateConfigUi);
+    $('#fridayEnabledSwitch').change(updateConfigUi);
+    $('#saturdayEnabledSwitch').change(updateConfigUi);
+    $('#sundayEnabledSwitch').change(updateConfigUi);
+    
+    /* minimizable sections */
+    $('span.minimize').click(function () {
+        $(this).toggleClass('open');
+        
+        /* enable the section switch when unminimizing */
+        if ($(this).hasClass('open')) {
+            var sectionSwitch = $(this).parent().find('input[type=checkbox]');
+            var sectionSwitchDiv = $(this).parent().find('div.check-box');
+            var sectionDiv = $(this).parents('div.settings-section-title:eq(0)');
+            if (sectionSwitch.length && !sectionSwitch.is(':checked') &&
+                !sectionSwitchDiv[0]._hideNull && !sectionDiv.attr('minimize-switch-independent')) {
+
+                sectionSwitch[0].checked = true;
+                sectionSwitch.change();
+            }
+        }
+            
+        updateConfigUi();
+    });
+
+    $('a.settings-section-title').click(function () {
+        $(this).parent().find('span.minimize').click();
+    });
+
+    /* additional configs */
+    var seenDependNames = {};
+    $('tr[depends]').each(function () {
+        var $tr = $(this);
+        var depends = $tr.attr('depends').split(' ');
+        depends.forEach(function (depend) {
+            depend = depend.split('=')[0];
+            depend = depend.replace(new RegExp('[^a-zA-Z0-9_]', 'g'), '');
+            
+            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 = this.value.trim();
+    });
+    
+    $('#rootDirectoryEntry').change(function () {
+        if (this.value.charAt(0) !== '/') {
+            this.value = '/' + this.value;
+        }
+    });
+    
+    /* fetch & push handlers */
+    $('#cameraSelect').focus(function () {
+        /* remember the previously selected index */
+        this._prevSelectedIndex = this.selectedIndex;
+    
+    }).change(function () {
+        if ($('#cameraSelect').val() === 'add') {
+            runAddCameraDialog();
+            this.selectedIndex = this._prevSelectedIndex;
+        }
+        else {
+            this._prevSelectedIndex = this.selectedIndex;
+            beginProgress([$(this).val()]);
+            fetchCurrentCameraConfig(endProgress);
+        }
+    });
+    $('input.main-config, select.main-config, textarea.main-config').change(function () {
+        pushMainConfig($(this).parents('tr:eq(0)').attr('reboot') == 'true');
+    });
+    $('input.camera-config, select.camera-config, textarea.camera-config').change(function () {
+        pushCameraConfig($(this).parents('tr:eq(0)').attr('reboot') == 'true');
+    });
+    
+    /* streaming framerate must be >= device framerate */
+    $('#framerateSlider').change(function (val) {
+        var value = Number($('#framerateSlider').val());
+        var streamingValue = Number($('#streamingFramerateSlider').val());
+        
+        if (streamingValue < value) {
+            $('#streamingFramerateSlider').val(value).change();
+        }
+    });
+    
+    /* preview controls */
+    $('#brightnessSlider').change(function () {pushPreview('brightness');});
+    $('#contrastSlider').change(function () {pushPreview('contrast');});
+    $('#saturationSlider').change(function () {pushPreview('saturation');});
+    $('#hueSlider').change(function () {pushPreview('hue');});
+    
+    /* apply button */
+    $('#applyButton').click(function () {
+        if ($(this).hasClass('progress')) {
+            return; /* in progress */
+        }
+        
+        doApply();
+    });
+    
+    /* shut down button */
+    $('#shutDownButton').click(function () {
+        doShutDown();
+    });
+    
+    /* reboot button */
+    $('#rebootButton').click(function () {
+        doReboot();
+    });
+    
+    /* whenever the window is resized,
+     * if a modal dialog is visible, it should be repositioned */
+    $(window).resize(updateModalDialogPosition);
+    
+    /* remove camera button */
+    $('div.button.rem-camera-button').click(doRemCamera);
+    
+    /* logout button */
+    $('div.button.logout-button').click(doLogout);
+    
+    /* autoselect urls in read-only entries */
+    $('#streamingSnapshotUrlEntry:text, #streamingMjpgUrlEntry:text, #streamingEmbedUrlEntry:text').click(function () {
+        this.select();
+    });
+}
+
+
+    /* settings */
+
+function openSettings(cameraId) {
+    if (cameraId != null) {
+        $('#cameraSelect').val(cameraId).change();
+    }
+    
+    $('div.settings').addClass('open').removeClass('closed');
+    $('div.page-container').addClass('stretched');
+    $('div.settings-top-bar').addClass('open').removeClass('closed');
+    
+    updateConfigUi();
+}
+
+function closeSettings() {
+    hideApply();
+    pushConfigs = {};
+    pushConfigReboot = false;
+    
+    $('div.settings').removeClass('open').addClass('closed');
+    $('div.page-container').removeClass('stretched');
+    $('div.settings-top-bar').removeClass('open').addClass('closed');
+}
+
+function isSettingsOpen() {
+    return $('div.settings').hasClass('open');   
+}
+
+function updateConfigUi() {
+    var objs = $('tr.settings-item, div.advanced-setting, table.advanced-setting, div.settings-section-title, table.settings, ' +
+            'div.check-box.camera-config, div.check-box.main-config');
+    
+    function markHideLogic() {
+        this._hideLogic = true;
+    }
+    
+    function markHideAdvanced() {
+        this._hideAdvanced = true;
+    }
+    
+    function markHideMinimized() {
+        this._hideMinimized = true;
+    }
+    
+    function unmarkHide() {
+        this._hideLogic = false;
+        this._hideAdvanced = false;
+        this._hideMinimized = false;
+    }
+    
+    objs.each(unmarkHide);
+    
+    /* hide sliders that, for some reason, don't have a value */
+    $('input.range').each(function () {
+        if  (this.value == '') {
+            $(this).parents('tr:eq(0)').each(markHideLogic);
+        }
+    });
+
+    /* minimizable sections */
+    $('span.minimize').each(function () {
+        var $this = $(this);
+        if (!$this.hasClass('open')) {
+            $this.parent().next('table.settings').find('tr').each(markHideMinimized);
+        }
+    });
+
+    /* general enable switch */
+    var motionEyeEnabled = $('#motionEyeSwitch').get(0).checked;
+    if (!motionEyeEnabled) {
+        objs.not($('#motionEyeSwitch').parents('div').get(0)).each(markHideLogic);
+    }
+    
+    if ($('#cameraSelect').find('option').length < 2) { /* no camera configured */
+        $('#videoDeviceSwitch').parent().each(markHideLogic);
+        $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHideLogic);
+    }
+    
+    if ($('#videoDeviceSwitch')[0].error) { /* config error */
+        $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHideLogic);
+    }
+        
+    /* advanced settings */
+    var showAdvanced = $('#showAdvancedSwitch').get(0).checked;
+    if (!showAdvanced) {
+        $('tr.advanced-setting, div.advanced-setting, table.advanced-setting').each(markHideAdvanced);
+    }
+    
+    /* hide resolution select if no resolution is selected (none matches) */
+    if ($('#resolutionSelect')[0].selectedIndex == -1) {
+        $('#resolutionSelect').parents('tr:eq(0)').each(markHideLogic);
+    }
+
+    /* storage device */
+    if ($('#storageDeviceSelect').val() !== 'network-share') {
+        $('#networkServerEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#networkUsernameEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#networkPasswordEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#networkShareNameEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* text */
+    if ($('#leftTextSelect').val() !== 'custom-text') {
+        $('#leftTextEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    if ($('#rightTextSelect').val() !== 'custom-text') {
+        $('#rightTextEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* still images capture mode */
+    if ($('#captureModeSelect').val() !== 'interval-snapshots') {
+        $('#snapshotIntervalEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* auto noise level */
+    if ($('#autoNoiseDetectSwitch').get(0).checked) {
+        $('#noiseLevelSlider').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* video device switch */
+    if (!$('#videoDeviceSwitch').get(0).checked) {
+        $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHideLogic);
+    }
+    
+    /* text overlay switch */
+    if (!$('#textOverlaySwitch').get(0).checked) {
+        $('#textOverlaySwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
+    }
+    
+    /* video streaming */
+    if (!$('#videoStreamingSwitch').get(0).checked) {
+        $('#videoStreamingSwitch').parent().next('table.settings').find('tr.settings-item').not('.localhost-streaming').each(markHideLogic);
+    }
+    if (!$('#streamingServerResizeSwitch').get(0).checked) {
+        $('#streamingResolutionSlider').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* still images switch */
+    if (!$('#stillImagesSwitch').get(0).checked) {
+        $('#stillImagesSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
+    }
+    
+    /* preserve pictures */
+    if ($('#preservePicturesSelect').val() != '-1') {
+        $('#picturesLifetimeEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* motion detection switch */
+    if (!$('#motionDetectionSwitch').get(0).checked) {
+        $('#motionDetectionSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
+        
+        /* hide the entire motion movies section */
+        $('#motionMoviesSwitch').parent().each(markHideLogic);
+        $('#motionMoviesSwitch').parent().next('table.settings').each(markHideLogic);
+        
+        /* hide the entire notifications section */
+        $('#emailNotificationsSwitch').parents('table.settings').prev().each(markHideLogic);
+        $('#emailNotificationsSwitch').parents('table.settings').each(markHideLogic);
+        
+        /* hide the entire working schedule section */
+        $('#workingScheduleSwitch').parent().each(markHideLogic);
+        $('#workingScheduleSwitch').parent().next('table.settings').each(markHideLogic);
+    }
+    
+    /* motion movies switch */
+    if (!$('#motionMoviesSwitch').get(0).checked) {
+        $('#motionMoviesSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
+    }
+    
+    /* preserve movies */
+    if ($('#preserveMoviesSelect').val() != '-1') {
+        $('#moviesLifetimeEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    /* event notifications */
+    if (!$('#emailNotificationsSwitch').get(0).checked) {
+        $('#emailAddressesEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#smtpServerEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#smtpPortEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#smtpAccountEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#smtpPasswordEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#smtpTlsSwitch').parents('tr:eq(0)').each(markHideLogic);
+        $('#emailPictureTimeSpanEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+    
+    if (!$('#webHookNotificationsSwitch').get(0).checked) {
+        $('#webHookUrlEntry').parents('tr:eq(0)').each(markHideLogic);
+        $('#webHookHttpMethodSelect').parents('tr:eq(0)').each(markHideLogic);
+    }
+
+    if (!$('#commandNotificationsSwitch').get(0).checked) {
+        $('#commandNotificationsEntry').parents('tr:eq(0)').each(markHideLogic);
+    }
+
+    /* working schedule */
+    if (!$('#workingScheduleSwitch').get(0).checked) {
+        $('#workingScheduleSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
+    }
+    
+    /* additional configs */
+    $('tr[depends]').each(function () {
+        var $tr = $(this);
+        var depends = $tr.attr('depends').split(' ');
+        var conditionOk = true;
+        depends.every(function (depend) {
+            var neg = depend.indexOf('!') >= 0;
+            var parts = depend.split('=');
+            var boolCheck = parts.length == 1;
+            depend = parts[0].replace(new RegExp('[^a-zA-Z0-9_$]', 'g'), '');
+
+            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;
+                }
+            }
+
+            if (boolCheck) {
+                if (neg) {
+                    val = !val;
+                }
+                
+                if (!val) {
+                    conditionOk = false;
+                    return false;
+                }
+            }
+            else { /* comparison */
+                var equal = parts[parts.length - 1] == val;
+                if (equal == neg) {
+                    conditionOk = false;
+                    return false;
+                }
+            }
+
+            return true;
+        });
+        
+        if (!conditionOk) {
+            $tr.each(markHideLogic);
+        }
+    });
+    
+    /* hide sections that have no visible configs and no switch */
+    $('div.settings-section-title').each(function () {
+        var $this = $(this);
+        var $table = $this.next();
+        var controls = $table.find('input, select');
+
+        var switchButton = $this.children('div.check-box');
+        if (switchButton.length && !switchButton[0]._hideNull) {
+            return; /* has visible switch */
+        }
+
+        for (var i = 0; i < controls.length; i++) {
+            var control = $(controls[i]);
+            var tr = control.parents('tr:eq(0)')[0];
+            if (!tr._hideLogic && !tr._hideAdvanced && !tr._hideNull) {
+                return; /* has visible controls */
+            }
+        }
+
+        $table.find('div.settings-item-separator').each(function () {
+            $(this).parent().parent().each(markHideLogic);
+        });
+
+        $this.each(markHideLogic);
+        $table.each(markHideLogic);
+    });
+    
+    /* hide useless separators */
+    $('div.settings-container table.settings').each(function () {
+        var $table = $(this);
+        
+        /* filter visible rows */
+        var visibleTrs = $table.find('tr').filter(function () {
+            return !this._hideLogic && !this._hideAdvanced && !this._hideNull;
+        }).map(function () {
+            var $tr = $(this);
+            $tr.isSeparator = $tr.find('div.settings-item-separator').length > 0;
+            
+            return $tr;
+        }).get();
+
+        for (var i = 1; i < visibleTrs.length; i++) {
+            var $prevTr = visibleTrs[i - 1];
+            var $tr = visibleTrs[i];
+            if ($prevTr.isSeparator && $tr.isSeparator) {
+                $tr.each(markHideLogic);
+            }
+        }
+
+        /* filter visible rows again */
+        visibleTrs = $table.find('tr').filter(function () {
+            return !this._hideLogic && !this._hideAdvanced && !this._hideNull;
+        }).map(function () {
+            var $tr = $(this);
+            $tr.isSeparator = $tr.find('div.settings-item-separator').length > 0;
+            
+            return $tr;
+        }).get();
+
+        if (visibleTrs.length) {
+            /* test first row */
+            if (visibleTrs[0].isSeparator) {
+                visibleTrs[0].each(markHideLogic);
+            }
+            
+            /* test last row */
+            if (visibleTrs[visibleTrs.length - 1].isSeparator) {
+                visibleTrs[visibleTrs.length - 1].each(markHideLogic);
+            }
+        }
+    });
+    
+    var weekDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
+    weekDays.forEach(function (weekDay) {
+        var check = $('#' + weekDay + 'EnabledSwitch');
+        if (check.get(0).checked) {
+            check.parent().find('.time').show();
+        }
+        else {
+            check.parent().find('.time').hide();
+        }
+    });
+    
+    objs.each(function () {
+        if (this._hideLogic || this._hideAdvanced || this._hideMinimized || this._hideNull /* from dict2ui */) {
+            $(this).hide(200);
+        }
+        else {
+            $(this).show(200);
+        }
+    });
+    
+    /* re-validate all the validators */
+    $('div.settings').find('.validator').each(function () {
+        this.validate();
+    });
+    
+    /* update all checkboxes and sliders */
+    $('div.settings').find('input[type=checkbox], input.range').each(function () {
+        this.update();
+    });
+    
+    /* select the first option for the selects with no current selection */
+    $('div.settings').find('select').not('#cameraSelect').each(function () {
+        if (this.selectedIndex === -1) {
+            this.selectedIndex = 0;
+        }
+    });
+}
+
+function configUiValid() {
+    /* re-validate all the validators */
+    $('div.settings').find('.validator').each(function () {
+        this.validate();
+    });
+    
+    var valid = true;
+    $('div.settings input, select').each(function () {
+        if (this.invalid) {
+            valid = false;
+            return false;
+        }
+    });
+    
+    return valid;
+}
+
+function mainUi2Dict() {
+    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()
+    };
+
+    /* 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();
+            if (control.hasClass('number')) {
+                value = Number(value);
+            }
+        }
+        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 = Number(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) {
+    function markHideIfNull(field, elemId) {
+        var elem = $('#' + elemId);
+        var sectionDiv = elem.parents('div.settings-section-title:eq(0)');
+        var hideNull = (field === true) || (typeof field == 'string' && dict[field] == null);
+
+        if (sectionDiv.length) { /* element is a section */
+            sectionDiv.find('div.check-box').each(function () {this._hideNull = hideNull;});
+            if (hideNull) {
+                sectionDiv.find('input[type=checkbox]').each(function () {this.checked = true;});
+            }
+        }
+        else { /* element is a config option */
+            elem.parents('tr:eq(0)').each(function () {this._hideNull = hideNull;});
+        }
+    }
+    
+    $('#motionEyeSwitch')[0].checked = dict['enabled'];
+    
+    $('#showAdvancedSwitch')[0].checked = dict['show_advanced']; markHideIfNull('show_advanced', 'showAdvancedSwitch');
+    $('#adminUsernameEntry').val(dict['admin_username']); markHideIfNull('admin_username', 'adminUsernameEntry');
+    $('#adminPasswordEntry').val(dict['admin_password']); markHideIfNull('admin_password', 'adminPasswordEntry');
+    $('#normalUsernameEntry').val(dict['normal_username']); markHideIfNull('normal_username', 'normalUsernameEntry');
+    $('#normalPasswordEntry').val(dict['normal_password']); markHideIfNull('normal_password', 'normalPasswordEntry');
+
+    /* additional sections */
+    $('input[type=checkbox].additional-section.main-config').each(function () {
+        var name = this.id.substring(0, this.id.length - 6);
+        this.checked = dict[name];
+        markHideIfNull(name, this.id);
+    });
+
+    /* additional configs */
+    $('tr.additional-config').each(function () {
+        var $this = $(this);
+        var control = $this.find('input, select, textarea, div.html');
+        
+        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];
+        }
+        else if (id.endsWith('Html')) {
+            name = id.substring(0, id.length - 4);
+            control.html(dict['_' + name]);
+        }
+        
+        markHideIfNull('_' + name, id);
+    });
+
+    updateConfigUi();
+}
+
+function cameraUi2Dict() {
+    if ($('#videoDeviceSwitch')[0].error) { /* config error */
+        return {
+            'enabled': $('#videoDeviceSwitch')[0].checked,
+        };
+    }
+    
+    var dict = {
+        'enabled': $('#videoDeviceSwitch')[0].checked,
+        'name': $('#deviceNameEntry').val(),
+        'proto': $('#deviceTypeEntry')[0].proto,
+        
+        /* video device */
+        'light_switch_detect': $('#lightSwitchDetectSwitch')[0].checked,
+        'auto_brightness': $('#autoBrightnessSwitch')[0].checked,
+        'rotation': $('#rotationSelect').val(),
+        'framerate': $('#framerateSlider').val(),
+        'extra_options': $('#extraOptionsEntry').val().split(new RegExp('(\n)|(\r\n)|(\n\r)')).map(function (o) {
+            if (!o) {
+                return null;
+            }
+
+            o = o.trim();
+            if (!o.length) {
+                return null;
+            }
+
+            var parts = o.replace(new RegExp('\\s+', 'g'), ' ').split(' ');
+            if (parts.length < 2) {
+                return [parts[0], ''];
+            }
+            else if (parts.length == 2) {
+                return parts;
+            }
+            else {
+                return [parts[0], parts.slice(1).join(' ')];
+            }
+        }).filter(function (e) {return e;}),
+
+        /* file storage */
+        'storage_device': $('#storageDeviceSelect').val(),
+        'network_server': $('#networkServerEntry').val(),
+        'network_share_name': $('#networkShareNameEntry').val(),
+        'network_username': $('#networkUsernameEntry').val(),
+        'network_password': $('#networkPasswordEntry').val(),
+        'root_directory': $('#rootDirectoryEntry').val(),
+        
+        /* text overlay */
+        'text_overlay': $('#textOverlaySwitch')[0].checked,
+        'left_text': $('#leftTextSelect').val(),
+        'custom_left_text': $('#leftTextEntry').val(),
+        'right_text': $('#rightTextSelect').val(),
+        'custom_right_text': $('#rightTextEntry').val(),
+        
+        /* video streaming */
+        'video_streaming': $('#videoStreamingSwitch')[0].checked,
+        'streaming_framerate': $('#streamingFramerateSlider').val(),
+        'streaming_quality': $('#streamingQualitySlider').val(),
+        'streaming_resolution': $('#streamingResolutionSlider').val(),
+        'streaming_server_resize': $('#streamingServerResizeSwitch')[0].checked,
+        'streaming_port': $('#streamingPortEntry').val(),
+        'streaming_auth_mode': $('#streamingAuthModeSelect').val() || 'disabled', /* compatibility with old motion */
+        'streaming_motion': $('#streamingMotion')[0].checked,
+        
+        /* still images */
+        'still_images': $('#stillImagesSwitch')[0].checked,
+        'image_file_name': $('#imageFileNameEntry').val(),
+        'image_quality': $('#imageQualitySlider').val(),
+        'capture_mode': $('#captureModeSelect').val(),
+        'snapshot_interval': $('#snapshotIntervalEntry').val(),
+        'preserve_pictures': $('#preservePicturesSelect').val() >= 0 ? $('#preservePicturesSelect').val() : $('#picturesLifetimeEntry').val(),
+        
+        /* motion detection */
+        'motion_detection': $('#motionDetectionSwitch')[0].checked,
+        'show_frame_changes': $('#showFrameChangesSwitch')[0].checked,
+        'frame_change_threshold': $('#frameChangeThresholdSlider').val(),
+        'auto_noise_detect': $('#autoNoiseDetectSwitch')[0].checked,
+        'noise_level': $('#noiseLevelSlider').val(),
+        'event_gap': $('#eventGapEntry').val(),
+        'pre_capture': $('#preCaptureEntry').val(),
+        'post_capture': $('#postCaptureEntry').val(),
+        'minimum_motion_frames': $('#minimumMotionFramesEntry').val(),
+        
+        /* motion movies */
+        'motion_movies': $('#motionMoviesSwitch')[0].checked,
+        'movie_file_name': $('#movieFileNameEntry').val(),
+        'movie_quality': $('#movieQualitySlider').val(),
+        'max_movie_length': $('#maxMovieLengthEntry').val(),
+        'preserve_movies': $('#preserveMoviesSelect').val() >= 0 ? $('#preserveMoviesSelect').val() : $('#moviesLifetimeEntry').val(),
+        
+        /* motion notifications */
+        'email_notifications_enabled': $('#emailNotificationsSwitch')[0].checked,
+        'email_notifications_addresses': $('#emailAddressesEntry').val(),
+        'email_notifications_smtp_server': $('#smtpServerEntry').val(),
+        'email_notifications_smtp_port': $('#smtpPortEntry').val(),
+        'email_notifications_smtp_account': $('#smtpAccountEntry').val(),
+        'email_notifications_smtp_password': $('#smtpPasswordEntry').val(),
+        'email_notifications_smtp_tls': $('#smtpTlsSwitch')[0].checked,
+        'email_notifications_picture_time_span': $('#emailPictureTimeSpanEntry').val(),
+        'web_hook_notifications_enabled': $('#webHookNotificationsSwitch')[0].checked,
+        'web_hook_notifications_url': $('#webHookUrlEntry').val(),
+        'web_hook_notifications_http_method': $('#webHookHttpMethodSelect').val(),
+        'command_notifications_enabled': $('#commandNotificationsSwitch')[0].checked,
+        'command_notifications_exec': $('#commandNotificationsEntry').val(),
+        
+        /* working schedule */
+        'working_schedule': $('#workingScheduleSwitch')[0].checked,
+        'monday_from': $('#mondayEnabledSwitch')[0].checked ? $('#mondayFromEntry').val() : '',
+        'monday_to':$('#mondayEnabledSwitch')[0].checked ? $('#mondayToEntry').val() : '',
+        'tuesday_from': $('#tuesdayEnabledSwitch')[0].checked ? $('#tuesdayFromEntry').val() : '',
+        'tuesday_to': $('#tuesdayEnabledSwitch')[0].checked ? $('#tuesdayToEntry').val() : '',
+        'wednesday_from': $('#wednesdayEnabledSwitch')[0].checked ? $('#wednesdayFromEntry').val() : '',
+        'wednesday_to': $('#wednesdayEnabledSwitch')[0].checked ? $('#wednesdayToEntry').val() : '',
+        'thursday_from': $('#thursdayEnabledSwitch')[0].checked ? $('#thursdayFromEntry').val() : '',
+        'thursday_to': $('#thursdayEnabledSwitch')[0].checked ? $('#thursdayToEntry').val() : '',
+        'friday_from': $('#fridayEnabledSwitch')[0].checked ? $('#fridayFromEntry').val() : '',
+        'friday_to': $('#fridayEnabledSwitch')[0].checked ? $('#fridayToEntry').val() :'',
+        'saturday_from': $('#saturdayEnabledSwitch')[0].checked ? $('#saturdayFromEntry').val() : '',
+        'saturday_to': $('#saturdayEnabledSwitch')[0].checked ? $('#saturdayToEntry').val() : '',
+        'sunday_from': $('#sundayEnabledSwitch')[0].checked ? $('#sundayFromEntry').val() : '',
+        'sunday_to': $('#sundayEnabledSwitch')[0].checked ? $('#sundayToEntry').val() : '',
+        'working_schedule_type': $('#workingScheduleTypeSelect').val(),
+    };
+
+    if ($('#resolutionSelect')[0].selectedIndex != -1) {
+        dict.resolution = $('#resolutionSelect').val();
+    }
+
+    if ($('#brightnessSlider').val() !== '') {
+        dict.brightness = $('#brightnessSlider').val();
+    }
+
+    if ($('#contrastSlider').val() !== '') {
+        dict.contrast = $('#contrastSlider').val();
+    }
+    
+    if ($('#saturationSlider').val() !== '') {
+        dict.saturation = $('#saturationSlider').val();
+    }
+    
+    if ($('#hueSlider').val() !== '') {
+        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();
+            if (control.hasClass('number')) {
+                value = Number(value);
+            }
+        }
+        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 = Number(control.val());
+        }
+        else if (id.endsWith('Switch')) {
+            name = id.substring(0, id.length - 6);
+            value = control[0].checked;
+        }
+        
+        dict['_' + name] = value;
+    });
+
+    return dict;
+}
+
+function dict2CameraUi(dict) {
+    if (dict == null) {
+        /* errors while getting the configuration */
+        
+        $('#videoDeviceSwitch')[0].error = true;
+        $('#videoDeviceSwitch')[0].checked = true; /* so that the user can explicitly disable the camera */
+        updateConfigUi();
+        
+        return;
+    }
+    else {
+        $('#videoDeviceSwitch')[0].error = false;
+    }
+
+    function markHideIfNull(field, elemId) {
+        var elem = $('#' + elemId);
+        var sectionDiv = elem.parents('div.settings-section-title:eq(0)');
+        var hideNull = (field === true) || (typeof field == 'string' && dict[field] == null);
+
+        if (sectionDiv.length) { /* element is a section */
+            sectionDiv.find('div.check-box').each(function () {this._hideNull = hideNull;});
+            if (hideNull) {
+                sectionDiv.find('input[type=checkbox]').each(function () {this.checked = true;});
+            }
+        }
+        else { /* element is a config option */
+            elem.parents('tr:eq(0)').each(function () {this._hideNull = hideNull;});
+        }
+    }
+    
+    /* video device */
+    var prettyType = '';
+    switch (dict['proto']) {
+        case 'v4l2':
+            prettyType = 'V4L2 Camera';
+            break;
+
+        case 'netcam':
+            prettyType = 'Network Camera';
+            break;
+
+        case 'motioneye':
+            prettyType = 'Remote motionEye Camera';
+            break;
+
+        case 'mjpeg':
+            prettyType = 'Simple MJPEG Camera';
+            break;
+    }
+    
+    $('#videoDeviceSwitch')[0].checked = dict['enabled']; markHideIfNull('enabled', 'videoDeviceSwitch');
+    $('#deviceNameEntry').val(dict['name']); markHideIfNull('name', 'deviceNameEntry');
+    $('#deviceUriEntry').val(dict['device_url']); markHideIfNull('device_url', 'deviceUriEntry');
+    $('#deviceTypeEntry').val(prettyType); markHideIfNull(!prettyType, 'deviceTypeEntry');
+    $('#deviceTypeEntry')[0].proto = dict['proto'];
+    $('#lightSwitchDetectSwitch')[0].checked = dict['light_switch_detect']; markHideIfNull('light_switch_detect', 'lightSwitchDetectSwitch');
+    $('#autoBrightnessSwitch')[0].checked = dict['auto_brightness']; markHideIfNull('auto_brightness', 'autoBrightnessSwitch');
+    
+    $('#brightnessSlider').val(dict['brightness']); markHideIfNull('brightness', 'brightnessSlider');
+    $('#contrastSlider').val(dict['contrast']); markHideIfNull('contrast', 'contrastSlider');
+    $('#saturationSlider').val(dict['saturation']); markHideIfNull('saturation', 'saturationSlider');
+    $('#hueSlider').val(dict['hue']); markHideIfNull('hue', 'hueSlider');
+
+    $('#resolutionSelect').html('');
+    if (dict['available_resolutions']) {
+        dict['available_resolutions'].forEach(function (resolution) {
+            $('#resolutionSelect').append('<option value="' + resolution + '">' + resolution + '</option>');
+        });
+    }
+    $('#resolutionSelect').val(dict['resolution']); markHideIfNull('available_resolutions', 'resolutionSelect');
+    
+    $('#rotationSelect').val(dict['rotation']); markHideIfNull('rotation', 'rotationSelect');
+    $('#framerateSlider').val(dict['framerate']); markHideIfNull('framerate', 'framerateSlider');
+    $('#extraOptionsEntry').val(dict['extra_options'] ? (dict['extra_options'].map(function (o) {
+        return o.join(' ');
+    }).join('\r\n')) : ''); markHideIfNull('extra_options', 'extraOptionsEntry');
+    
+    /* file storage */
+    $('#storageDeviceSelect').empty();
+    dict['available_disks'] = dict['available_disks'] || [];
+    var storageDeviceOptions = {'network-share': true};
+    dict['available_disks'].forEach(function (disk) {
+        disk.partitions.forEach(function (partition) {
+            var target = partition.target.replaceAll('/', '-');
+            var option = 'local-disk' + target;
+            var label = partition.vendor;
+            if (partition.model) {
+                label += ' ' + partition.model;
+            }
+            if (disk.partitions.length > 1) {
+                label += '/part' + partition.part_no;
+            }
+            label += ' (' + partition.target + ')';
+            
+            storageDeviceOptions[option] = true;
+            
+            $('#storageDeviceSelect').append('<option value="' + option + '">' + label + '</option>');
+        });
+    });
+    $('#storageDeviceSelect').append('<option value="custom-path">Custom Path</option>');
+    if (dict['smb_shares']) {
+        $('#storageDeviceSelect').append('<option value="network-share">Network Share</option>');
+    }
+
+    if (storageDeviceOptions[dict['storage_device']]) {
+        $('#storageDeviceSelect').val(dict['storage_device']);
+    }
+    else {
+        $('#storageDeviceSelect').val('custom-path');
+    }
+    markHideIfNull('storage_device', 'storageDeviceSelect');
+    $('#networkServerEntry').val(dict['network_server']); markHideIfNull('network_server', 'networkServerEntry');
+    $('#networkShareNameEntry').val(dict['network_share_name']); markHideIfNull('network_share_name', 'networkShareNameEntry');
+    $('#networkUsernameEntry').val(dict['network_username']); markHideIfNull('network_username', 'networkUsernameEntry');
+    $('#networkPasswordEntry').val(dict['network_password']); markHideIfNull('network_password', 'networkPasswordEntry');
+    $('#rootDirectoryEntry').val(dict['root_directory']); markHideIfNull('root_directory', 'rootDirectoryEntry');
+    var percent = 0;
+    if (dict['disk_total'] != 0) {
+        percent = parseInt(dict['disk_used'] * 100 / dict['disk_total']);
+    }
+    
+    $('#diskUsageProgressBar').each(function () {
+        this.setProgress(percent);
+        this.setText((dict['disk_used'] / 1073741824).toFixed(1)  + '/' + (dict['disk_total'] / 1073741824).toFixed(1) + ' GB (' + percent + '%)');
+    }); markHideIfNull('disk_used', 'diskUsageProgressBar');
+    
+    /* text overlay */
+    $('#textOverlaySwitch')[0].checked = dict['text_overlay']; markHideIfNull('text_overlay', 'textOverlaySwitch');
+    $('#leftTextSelect').val(dict['left_text']); markHideIfNull('left_text', 'leftTextSelect');
+    $('#leftTextEntry').val(dict['custom_left_text']); markHideIfNull('custom_left_text', 'leftTextEntry');
+    $('#rightTextSelect').val(dict['right_text']); markHideIfNull('right_text', 'rightTextSelect');
+    $('#rightTextEntry').val(dict['custom_right_text']); markHideIfNull('custom_right_text', 'rightTextEntry');
+    
+    /* video streaming */
+    $('#videoStreamingSwitch')[0].checked = dict['video_streaming']; markHideIfNull('video_streaming', 'videoStreamingSwitch');
+    $('#streamingFramerateSlider').val(dict['streaming_framerate']); markHideIfNull('streaming_framerate', 'streamingFramerateSlider');
+    $('#streamingQualitySlider').val(dict['streaming_quality']); markHideIfNull('streaming_quality', 'streamingQualitySlider');
+    $('#streamingResolutionSlider').val(dict['streaming_resolution']); markHideIfNull('streaming_resolution', 'streamingResolutionSlider');
+    $('#streamingServerResizeSwitch')[0].checked = dict['streaming_server_resize']; markHideIfNull('streaming_server_resize', 'streamingServerResizeSwitch');
+    $('#streamingPortEntry').val(dict['streaming_port']); markHideIfNull('streaming_port', 'streamingPortEntry');
+    $('#streamingAuthModeSelect').val(dict['streaming_auth_mode']); markHideIfNull('streaming_auth_mode', 'streamingAuthModeSelect');
+    $('#streamingMotion')[0].checked = dict['streaming_motion']; markHideIfNull('streaming_motion', 'streamingMotion');
+    
+    var cameraUrl = location.protocol + '//' + location.host + '/picture/' + dict.id + '/';
+    
+    var snapshotUrl = null;
+    var mjpgUrl = null;
+    var embedUrl = null;
+    
+    if (dict['proto'] == 'mjpeg') {
+        mjpgUrl = dict['url'];
+        mjpgUrl = mjpgUrl.replace('127.0.0.1', window.location.host.split(':')[0]);
+        embedUrl = cameraUrl + 'frame/';
+    }
+    else {
+        snapshotUrl = cameraUrl + 'current/';
+        mjpgUrl = location.protocol + '//' + location.host.split(':')[0] + ':' + dict.streaming_port;
+        embedUrl = cameraUrl + 'frame/';
+    }
+
+    if (dict.proto == 'motioneye') {
+        /* cannot tell the mjpg streaming url for a remote motionEye camera */
+        mjpgUrl = '';
+    }
+
+    if ($('#normalPasswordEntry').val()) { /* anonymous access is disabled */ 
+        if (snapshotUrl) {
+            snapshotUrl = addAuthParams('GET', snapshotUrl);
+        }
+    }
+    
+    $('#streamingSnapshotUrlEntry').val(snapshotUrl); markHideIfNull(!snapshotUrl, 'streamingSnapshotUrlEntry');
+    $('#streamingMjpgUrlEntry').val(mjpgUrl); markHideIfNull(!mjpgUrl, 'streamingMjpgUrlEntry');
+    $('#streamingEmbedUrlEntry').val(embedUrl); markHideIfNull(!embedUrl, 'streamingEmbedUrlEntry');
+    
+    /* still images */
+    $('#stillImagesSwitch')[0].checked = dict['still_images']; markHideIfNull('still_images', 'stillImagesSwitch');
+    $('#imageFileNameEntry').val(dict['image_file_name']); markHideIfNull('image_file_name', 'imageFileNameEntry');
+    $('#imageQualitySlider').val(dict['image_quality']); markHideIfNull('image_quality', 'imageQualitySlider');
+    $('#captureModeSelect').val(dict['capture_mode']); markHideIfNull('capture_mode', 'captureModeSelect');
+    $('#snapshotIntervalEntry').val(dict['snapshot_interval']); markHideIfNull('snapshot_interval', 'snapshotIntervalEntry');
+    $('#preservePicturesSelect').val(dict['preserve_pictures']);
+    if ($('#preservePicturesSelect').val() == null) {
+        $('#preservePicturesSelect').val('-1');
+    }
+    markHideIfNull('preserve_pictures', 'preservePicturesSelect');
+    $('#picturesLifetimeEntry').val(dict['preserve_pictures']); markHideIfNull('preserve_pictures', 'picturesLifetimeEntry');
+    
+    /* motion detection */
+    $('#motionDetectionSwitch')[0].checked = dict['motion_detection']; markHideIfNull('motion_detection', 'motionDetectionSwitch');
+    $('#showFrameChangesSwitch')[0].checked = dict['show_frame_changes']; markHideIfNull('show_frame_changes', 'showFrameChangesSwitch');
+    $('#frameChangeThresholdSlider').val(dict['frame_change_threshold']); markHideIfNull('frame_change_threshold', 'frameChangeThresholdSlider');
+    $('#autoNoiseDetectSwitch')[0].checked = dict['auto_noise_detect']; markHideIfNull('auto_noise_detect', 'autoNoiseDetectSwitch');
+    $('#noiseLevelSlider').val(dict['noise_level']); markHideIfNull('noise_level', 'noiseLevelSlider');
+    $('#eventGapEntry').val(dict['event_gap']); markHideIfNull('event_gap', 'eventGapEntry');
+    $('#preCaptureEntry').val(dict['pre_capture']); markHideIfNull('pre_capture', 'preCaptureEntry');
+    $('#postCaptureEntry').val(dict['post_capture']); markHideIfNull('post_capture', 'postCaptureEntry');
+    $('#minimumMotionFramesEntry').val(dict['minimum_motion_frames']); markHideIfNull('minimum_motion_frames', 'minimumMotionFramesEntry');
+    
+    /* motion movies */
+    $('#motionMoviesSwitch')[0].checked = dict['motion_movies']; markHideIfNull('motion_movies', 'motionMoviesSwitch');
+    $('#movieFileNameEntry').val(dict['movie_file_name']); markHideIfNull('movie_file_name', 'movieFileNameEntry');
+    $('#movieQualitySlider').val(dict['movie_quality']); markHideIfNull('movie_quality', 'movieQualitySlider');
+    $('#maxMovieLengthEntry').val(dict['max_movie_length']); markHideIfNull('max_movie_length', 'maxMovieLengthEntry');
+    $('#preserveMoviesSelect').val(dict['preserve_movies']);
+    if ($('#preserveMoviesSelect').val() == null) {
+        $('#preserveMoviesSelect').val('-1');
+    }
+    markHideIfNull('preserve_movies', 'preserveMoviesSelect');
+    $('#moviesLifetimeEntry').val(dict['preserve_movies']); markHideIfNull('preserve_movies', 'moviesLifetimeEntry');
+    
+    /* motion notifications */
+    $('#emailNotificationsSwitch')[0].checked = dict['email_notifications_enabled']; markHideIfNull('email_notifications_enabled', 'emailNotificationsSwitch');
+    $('#emailAddressesEntry').val(dict['email_notifications_addresses']);
+    $('#smtpServerEntry').val(dict['email_notifications_smtp_server']);
+    $('#smtpPortEntry').val(dict['email_notifications_smtp_port']);
+    $('#smtpAccountEntry').val(dict['email_notifications_smtp_account']);
+    $('#smtpPasswordEntry').val(dict['email_notifications_smtp_password']);
+    $('#smtpTlsSwitch')[0].checked = dict['email_notifications_smtp_tls'];
+    $('#emailPictureTimeSpanEntry').val(dict['email_notifications_picture_time_span']);
+    $('#webHookNotificationsSwitch')[0].checked = dict['web_hook_notifications_enabled']; markHideIfNull('web_hook_notifications_enabled', 'webHookNotificationsSwitch');
+    $('#webHookUrlEntry').val(dict['web_hook_notifications_url']);
+    $('#webHookHttpMethodSelect').val(dict['web_hook_notifications_http_method']);
+    $('#commandNotificationsSwitch')[0].checked = dict['command_notifications_enabled']; markHideIfNull('command_notifications_enabled', 'commandNotificationsSwitch');
+    $('#commandNotificationsEntry').val(dict['command_notifications_exec']);
+
+    /* working schedule */
+    $('#workingScheduleSwitch')[0].checked = dict['working_schedule']; markHideIfNull('working_schedule', 'workingScheduleSwitch');
+    $('#mondayEnabledSwitch')[0].checked = Boolean(dict['monday_from'] && dict['monday_to']); markHideIfNull('monday_from', 'mondayEnabledSwitch');
+    $('#mondayFromEntry').val(dict['monday_from']); markHideIfNull('monday_from', 'mondayFromEntry');
+    $('#mondayToEntry').val(dict['monday_to']); markHideIfNull('monday_to', 'mondayToEntry');
+    
+    $('#tuesdayEnabledSwitch')[0].checked = Boolean(dict['tuesday_from'] && dict['tuesday_to']); markHideIfNull('tuesday_from', 'tuesdayEnabledSwitch');
+    $('#tuesdayFromEntry').val(dict['tuesday_from']); markHideIfNull('tuesday_from', 'tuesdayFromEntry');
+    $('#tuesdayToEntry').val(dict['tuesday_to']); markHideIfNull('tuesday_to', 'tuesdayToEntry');
+    
+    $('#wednesdayEnabledSwitch')[0].checked = Boolean(dict['wednesday_from'] && dict['wednesday_to']); markHideIfNull('wednesday_from', 'wednesdayEnabledSwitch');
+    $('#wednesdayFromEntry').val(dict['wednesday_from']); markHideIfNull('wednesday_from', 'wednesdayFromEntry');
+    $('#wednesdayToEntry').val(dict['wednesday_to']); markHideIfNull('wednesday_to', 'wednesdayToEntry');
+    
+    $('#thursdayEnabledSwitch')[0].checked = Boolean(dict['thursday_from'] && dict['thursday_to']); markHideIfNull('thursday_from', 'thursdayEnabledSwitch');
+    $('#thursdayFromEntry').val(dict['thursday_from']); markHideIfNull('thursday_from', 'thursdayFromEntry');
+    $('#thursdayToEntry').val(dict['thursday_to']); markHideIfNull('thursday_to', 'thursdayToEntry');
+    
+    $('#fridayEnabledSwitch')[0].checked = Boolean(dict['friday_from'] && dict['friday_to']); markHideIfNull('friday_from', 'fridayEnabledSwitch');
+    $('#fridayFromEntry').val(dict['friday_from']); markHideIfNull('friday_from', 'fridayFromEntry');
+    $('#fridayToEntry').val(dict['friday_to']); markHideIfNull('friday_to', 'fridayToEntry');
+    
+    $('#saturdayEnabledSwitch')[0].checked = Boolean(dict['saturday_from'] && dict['saturday_to']); markHideIfNull('saturday_from', 'saturdayEnabledSwitch');
+    $('#saturdayFromEntry').val(dict['saturday_from']); markHideIfNull('saturday_from', 'saturdayFromEntry');
+    $('#saturdayToEntry').val(dict['saturday_to']); markHideIfNull('saturday_to', 'saturdayToEntry');
+    
+    $('#sundayEnabledSwitch')[0].checked = Boolean(dict['sunday_from'] && dict['sunday_to']); markHideIfNull('sunday_from', 'sundayEnabledSwitch');
+    $('#sundayFromEntry').val(dict['sunday_from']); markHideIfNull('sunday_from', 'sundayFromEntry');
+    $('#sundayToEntry').val(dict['sunday_to']); markHideIfNull('sunday_to', 'sundayToEntry');
+    $('#workingScheduleTypeSelect').val(dict['working_schedule_type']); markHideIfNull('working_schedule_type', 'workingScheduleTypeSelect');
+    
+    /* additional sections */
+    $('input[type=checkbox].additional-section.main-config').each(function () {
+        var name = this.id.substring(0, this.id.length - 6);
+        this.checked = dict[name];
+        markHideIfNull(name, this.id);
+    });
+
+    /* additional configs */
+    $('tr.additional-config').each(function () {
+        var $this = $(this);
+        var control = $this.find('input, select, textarea, div.html');
+        
+        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];
+        }
+        else if (id.endsWith('Html')) {
+            name = id.substring(0, id.length - 4);
+            control.html(dict['_' + name]);
+        }
+        
+        markHideIfNull('_' + name, id);
+    });
+
+    updateConfigUi();
+}
+
+    
+    /* progress */
+
+function beginProgress(cameraIds) {
+    if (inProgress) {
+        return; /* already in progress */
+    }
+
+    inProgress = true;
+    
+    /* replace the main page message with a progress indicator */
+    $('div.add-camera-message').replaceWith('<img class="main-loading-progress" src="' + staticUrl + 'img/main-loading-progress.gif">');
+    
+    /* show the apply button progress indicator */
+    $('#applyButton').html('<img class="apply-progress" src="' + staticUrl + 'img/apply-progress.gif">');
+    
+    /* show the camera progress indicators */
+    if (cameraIds) {
+        cameraIds.forEach(function (cameraId) {
+            $('div.camera-frame#camera' + cameraId + ' div.camera-progress').addClass('visible');
+        });
+    }
+    else {
+        $('div.camera-progress').addClass('visible');
+    }
+    
+    /* remove the settings progress lock */
+    $('div.settings-progress').css('width', '100%').css('opacity', '0.9');
+}
+
+function endProgress() {
+    if (!inProgress) {
+        return; /* not in progress */
+    }
+    
+    inProgress = false;
+    
+    /* deal with the apply button */
+    if (Object.keys(pushConfigs).length === 0) {
+        hideApply();
+    }
+    else {
+        showApply();
+    }
+    
+    /* hide the settings progress lock */
+    $('div.settings-progress').css('opacity', '0');
+    
+    /* hide the camera progress indicator */
+    $('div.camera-progress').removeClass('visible');
+
+    setTimeout(function () {
+        $('div.settings-progress').css('width', '0px');
+    }, 500);
+}
+
+function downloadFile(uri) {
+    uri = baseUri + uri;
+
+    var url = window.location.href;
+    var parts = url.split('/');
+    url = parts.slice(0, 3).join('/') + uri;
+    url = addAuthParams('GET', url);
+    
+    /* download the file by creating a temporary iframe */
+    var frame = $('<iframe style="display: none;"></iframe>');
+    frame.attr('src', url);
+    $('body').append(frame);
+}
+
+function uploadFile(uri, input, callback) {
+    if (!window.FormData) {
+        showErrorMessage("Your browser doesn't implement this function!");s
+        callback();
+    }
+
+    var formData = new FormData();
+    var files = input[0].files;
+    formData.append('files', files[0], files[0].name);
+
+    ajax('POST', uri, formData, callback);
+}
+
+
+    /* apply button */
+
+function showApply() {
+    var applyButton = $('#applyButton');
+    
+    applyButton.html('Apply');
+    applyButton.css('display', 'inline-block');
+    applyButton.removeClass('progress');
+    setTimeout(function () {
+        applyButton.css('opacity', '1');
+    }, 10);
+}
+
+function hideApply() {
+    var applyButton = $('#applyButton');
+    
+    applyButton.css('opacity', '0');
+    applyButton.removeClass('progress');
+    
+    setTimeout(function () {
+        applyButton.css('display', 'none');
+    }, 500);
+}
+
+function isApplyVisible() {
+    var applyButton = $('#applyButton');
+    
+    return applyButton.is(':visible');
+}
+
+function doApply() {
+    if (!configUiValid()) {
+        runAlertDialog('Make sure all the configuration options are valid!');
+        return;
+    }
+    
+    function actualApply() {
+        /* gather the affected motion instances */
+        var affectedInstances = {};
+        Object.keys(pushConfigs).forEach(function (key) {
+            var config = pushConfigs[key];
+            if (key === 'main') {
+                return;
+            }
+            
+            var instance;
+            if (config.proto == 'netcam' || config.proto == 'v4l2') {
+                instance = '';
+            }
+            else if (config.proto == 'motioneye') { /* motioneye */
+                instance = config.host || '';
+                if (config.port) {
+                    instance += ':' + config.port;
+                }
+            }
+            
+            affectedInstances[instance] = true;
+        });
+        affectedInstances = Object.keys(affectedInstances);
+        
+        /* compute the affected camera ids */ 
+        var cameraIdsByInstance = getCameraIdsByInstance();
+        var affectedCameraIds = [];
+        
+        affectedInstances.forEach(function (instance) {
+            affectedCameraIds = affectedCameraIds.concat(cameraIdsByInstance[instance] || []);
+        });
+        
+        beginProgress(affectedCameraIds);
+        affectedCameraIds.forEach(function (cameraId) {
+            refreshDisabled[cameraId] |= 0;
+            refreshDisabled[cameraId]++;
+        });
+        
+        ajax('POST', baseUri + 'config/0/set/', pushConfigs, function (data) {
+            affectedCameraIds.forEach(function (cameraId) {
+                refreshDisabled[cameraId]--;
+            });
+            
+            if (data == null || data.error) {
+                endProgress();
+                showErrorMessage(data && data.error);
+                return;
+            }
+            
+            if (data.reboot) {
+                var count = 0;
+                function checkServerReboot() {
+                    ajax('GET', baseUri + 'config/0/get/', null, 
+                        function () {
+                            window.location.reload(true);
+                        },
+                        function () {
+                            if (count < 25) {
+                                count += 1;
+                                setTimeout(checkServerReboot, 2000);
+                            }
+                            else {
+                                window.location.reload(true);
+                            }
+                        }
+                    );
+                }
+                
+                setTimeout(checkServerReboot, 15000);
+                
+                return;
+            }
+            
+            if (data.reload) {
+                window.location.reload(true);
+                return;
+            }
+            
+            /* update the camera name in the device select
+             * and frame title bar */
+            Object.keys(pushConfigs).forEach(function (key) {
+                var config = pushConfigs[key];
+                if (config.key !== 'main') {
+                    $('#cameraSelect').find('option[value=' + key + ']').html(config.name);
+                }
+                
+                $('#camera' + key).find('span.camera-name').html(config.name);
+            });
+    
+            pushConfigs = {};
+            pushConfigReboot = false;
+            endProgress();
+            recreateCameraFrames(); /* a camera could have been disabled */
+        });
+    }
+    
+    if (pushConfigReboot) {
+        runConfirmDialog('This will reboot the system. Continue?', function () {
+            actualApply();
+        });
+    }
+    else {
+        actualApply();
+    }
+}
+
+function doShutDown() {
+    runConfirmDialog('Really shut down?', function () {
+        ajax('POST', baseUri + 'power/shutdown/');
+        setTimeout(function () {
+            refreshInterval = 1000000;
+            showModalDialog('<div class="modal-progress"></div>');
+            
+            function checkServer() {
+                ajax('GET', baseUri, null, 
+                    function () {
+                        setTimeout(checkServer, 1000);
+                    },
+                    function () {
+                        showModalDialog('Powered Off');
+                        setTimeout(function () {
+                            $('div.modal-glass').animate({'opacity': '1', 'background-color': '#212121'}, 200);
+                        },100);
+                    },
+                    10000 /* timeout = 10s */
+                );
+            }
+            
+            checkServer();
+        }, 10);
+    });
+}
+
+function doReboot() {
+    runConfirmDialog('Really reboot?', function () {
+        ajax('POST', baseUri + 'power/reboot/');
+        setTimeout(function () {
+            refreshInterval = 1000000;
+            showModalDialog('<div class="modal-progress"></div>');
+            var shutDown = false;
+            
+            function checkServer() {
+                ajax('GET', baseUri, null, 
+                    function () {
+                        if (!shutDown) {
+                            setTimeout(checkServer, 1000);
+                        }
+                        else {
+                            runAlertDialog('The system has been rebooted!', function () {
+                                window.location.reload(true);
+                            });
+                        }
+                    },
+                    function () {
+                        shutDown = true; /* the first error indicates the system was shut down */
+                        setTimeout(checkServer, 1000);
+                    },
+                    5 * 1000 /* timeout = 5s */
+                );
+            }
+            
+            checkServer();
+        }, 10);
+    });
+}
+
+function doRemCamera() {
+    if (Object.keys(pushConfigs).length) {
+        return runAlertDialog('Please apply the modified settings first!');
+    }
+    
+    var cameraId = $('#cameraSelect').val();
+    if (cameraId == null || cameraId === 'add') {
+        runAlertDialog('No camera to remove!');
+        return;
+    }
+
+    var deviceName = $('#cameraSelect').find('option[value=' + cameraId + ']').text();
+    
+    runConfirmDialog('Remove camera ' + deviceName + '?', function () {
+        /* disable further refreshing of this camera */
+        var img = $('div.camera-frame#camera' + cameraId).find('img.camera');
+        if (img.length) {
+            img[0].loading = 1;
+        }
+
+        beginProgress();
+        ajax('POST', baseUri + 'config/' + cameraId + '/rem/', null, function (data) {
+            if (data == null || data.error) {
+                endProgress();
+                showErrorMessage(data && data.error);
+                return;
+            }
+            
+            fetchCurrentConfig(endProgress);
+        });
+    });
+}
+
+function doUpdate() {
+    if (Object.keys(pushConfigs).length) {
+        return runAlertDialog('Please apply the modified settings first!');
+    }
+    
+    showModalDialog('<div class="modal-progress"></div>');
+    ajax('GET', baseUri + 'update/', null, function (data) {
+        if (data.update_version == null) {
+            runAlertDialog('motionEye is up to date (current version: ' + data.current_version + ')');
+        }
+        else {
+            runConfirmDialog('New version available: ' + data.update_version + '. Update?', function () {
+                refreshInterval = 1000000;
+                showModalDialog('<div style="text-align: center;"><span>Updating. This may take a few minutes.</span><div class="modal-progress"></div></div>');
+                ajax('POST', baseUri + 'update/?version=' + data.update_version, null, function () {
+                    var count = 0;
+                    function checkServer() {
+                        ajax('GET', baseUri + 'config/0/get/', null,
+                            function () {
+                                runAlertDialog('motionEye was successfully updated!', function () {
+                                    window.location.reload(true);
+                                });
+                            },
+                            function () {
+                                if (count < 60) {
+                                    count += 1;
+                                    setTimeout(checkServer, 5000);
+                                }
+                                else {
+                                    runAlertDialog('Update failed!', function () {
+                                        window.location.reload(true);
+                                    });
+                                }
+                            }
+                        );
+                    }
+                    
+                    setTimeout(checkServer, 10000);
+
+                }, function (e) { /* error */
+                    runAlertDialog('The update process has failed!', function () {
+                        window.location.reload(true);
+                    });
+                });
+
+                return false; /* prevents hiding the modal container */
+            });
+        }
+    });
+}
+
+function doBackup() {
+    downloadFile('config/backup/');
+}
+
+function doRestore() {
+    var content = 
+            $('<table class="restore-dialog">' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Backup File</span></td>' +
+                    '<td class="dialog-item-value"><form><input type="file" class="styled" id="fileInput"></form></td>' +
+                    '<td><span class="help-mark" title="the backup file you have previously downloaded">?</span></td>' +
+                '</tr>' +
+            '</table>');
+    
+    /* collect ui widgets */
+    var fileInput = content.find('#fileInput');
+    
+    /* make validators */
+    makeFileValidator(fileInput, true);
+    
+    function uiValid() {
+        /* re-validate all the validators */
+        content.find('.validator').each(function () {
+            this.validate();
+        });
+        
+        var valid = true;
+        var query = content.find('input, select');
+        query.each(function () {
+            if (this.invalid) {
+                valid = false;
+                return false;
+            }
+        });
+
+        return valid;
+    }
+
+    runModalDialog({
+        title: 'Restore Configuration',
+        closeButton: true,
+        buttons: 'okcancel',
+        content: content,
+        onOk: function () {
+            if (!uiValid(true)) {
+                return false;
+            }
+            
+            refreshInterval = 1000000;
+
+            setTimeout(function () {
+                showModalDialog('<div style="text-align: center;"><span>Restoring configuration...</span><div class="modal-progress"></div></div>');
+                uploadFile(baseUri + 'config/restore/', fileInput, function (data) {
+                    if (data && data.ok) {
+                        var count = 0;
+                        function checkServer() {
+                            ajax('GET', baseUri + 'config/0/get/', null,
+                                function () {
+                                    runAlertDialog('The configuration has been restored!', function () {
+                                        window.location.reload(true);
+                                    });
+                                },
+                                function () {
+                                    if (count < 25) {
+                                        count += 1;
+                                        setTimeout(checkServer, 2000);
+                                    }
+                                    else {
+                                        runAlertDialog('Failed to restore the configuration!', function () {
+                                            window.location.reload(true);
+                                        });
+                                    }
+                                }
+                            );
+                        }
+                        
+                        if (data.reboot) {
+                            setTimeout(checkServer, 10000);
+                        }
+                        else {
+                            setTimeout(function () {
+                                window.location.reload();
+                            }, 5000);
+                        }
+                    }
+                    else {
+                        hideModalDialog();
+                        showErrorMessage('Failed to restore the configuration!');
+                    }
+                });
+            }, 10);
+        }
+    });
+}
+
+function doDownloadZipped(cameraId, groupKey) {
+    showModalDialog('<div class="modal-progress"></div>', null, null, true);
+    ajax('GET', baseUri + 'picture/' + cameraId + '/zipped/' + groupKey + '/', null, function (data) {
+        if (data.error) {
+            hideModalDialog(); /* progress */
+            showErrorMessage(data.error);
+        }
+        else {
+            hideModalDialog(); /* progress */
+            downloadFile('picture/' + cameraId + '/zipped/' + groupKey + '/?key=' + data.key);
+        }
+    });
+}
+
+function doDeleteFile(uri, callback) {
+    var url = window.location.href;
+    var parts = url.split('/');
+    url = parts.slice(0, 3).join('/') + uri;
+    
+    runConfirmDialog('Really delete this file?', function () {
+        showModalDialog('<div class="modal-progress"></div>', null, null, true);
+        ajax('POST', url, null, function (data) {
+            hideModalDialog(); /* progress */
+            hideModalDialog(); /* confirm */
+            
+            if (data == null || data.error) {
+                showErrorMessage(data && data.error);
+                return;
+            }
+
+            if (callback) {
+                callback();
+            }
+        });
+        
+        return false;
+    }, {stack: true});
+}
+
+function doDeleteAllFiles(mediaType, cameraId, groupKey, callback) {
+    runConfirmDialog('Really delete all ' + mediaType + 's in ' + groupKey + '?', function () {
+        showModalDialog('<div class="modal-progress"></div>', null, null, true);
+        ajax('POST', baseUri + mediaType + '/' + cameraId + '/delete_all/' + groupKey + '/', null, function (data) {
+            hideModalDialog(); /* progress */
+            hideModalDialog(); /* confirm */
+            
+            if (data == null || data.error) {
+                showErrorMessage(data && data.error);
+                return;
+            }
+
+            if (callback) {
+                callback();
+            }
+        });
+        
+        return false;
+    }, {stack: true});
+}
+
+
+    /* fetch & push */
+
+function fetchCurrentConfig(onFetch) {
+    function fetchCameraList() {
+        /* fetch the camera list */
+        ajax('GET', baseUri + 'config/list/', null, function (data) {
+            if (data == null || data.error) {
+                showErrorMessage(data && data.error);
+                data = {cameras: []};
+                if (onFetch) {
+                    onFetch(null);
+                }
+            }
+            
+            initialConfigFetched = true;
+            
+            var i, cameras = data.cameras;
+            
+            if (isAdmin()) {
+                var cameraSelect = $('#cameraSelect');
+                cameraSelect.html('');
+                for (i = 0; i < cameras.length; i++) {
+                    var camera = cameras[i];
+                    cameraSelect.append('<option value="' + camera['id'] + '">' + camera['name'] + '</option>');
+                }
+                cameraSelect.append('<option value="add">add camera...</option>');
+                
+                var enabledCameras = cameras.filter(function (camera) {return camera['enabled'];});
+                if (enabledCameras.length > 0) { /* prefer the first enabled camera */
+                    cameraSelect[0].selectedIndex = cameras.indexOf(enabledCameras[0]);
+                    fetchCurrentCameraConfig(onFetch);
+                }
+                else if (cameras.length) { /* only disabled cameras */
+                    cameraSelect[0].selectedIndex = 0;
+                    fetchCurrentCameraConfig(onFetch);
+                }
+                else { /* no camera at all */
+                    cameraSelect[0].selectedIndex = -1;
+
+                    if (onFetch) {
+                        onFetch(data);
+                    }
+                }
+
+                updateConfigUi();
+            }
+            else { /* normal user */
+                if (!cameras.length) {
+                    /* normal user with no cameras doesn't make too much sense - force login */
+                    doLogout();
+                }
+
+                if (onFetch) {
+                    onFetch(data);
+                }
+            }
+
+            var mainLoadingProgressImg = $('img.main-loading-progress');
+            if (mainLoadingProgressImg.length) {
+                mainLoadingProgressImg.animate({'opacity': 0}, 200, function () {
+                    recreateCameraFrames(cameras);
+                    mainLoadingProgressImg.remove();
+                });
+            }
+            else {
+                recreateCameraFrames(cameras);
+            }
+        });
+    }
+    /* add a progress indicator */
+    $('div.page-container').append('<img class="main-loading-progress" src="' + staticUrl + 'img/main-loading-progress.gif">');
+
+    if (isAdmin()) {
+        /* fetch the main configuration */
+        ajax('GET', baseUri + 'config/main/get/', null, function (data) {
+            if (data == null || data.error) {
+                showErrorMessage(data && data.error);
+                return;
+            }
+            
+            dict2MainUi(data);
+            fetchCameraList();
+        });
+    }
+    else {
+        fetchCameraList();
+    }
+}
+
+function fetchCurrentCameraConfig(onFetch) {
+    var cameraId = $('#cameraSelect').val();
+    if (cameraId != null) {
+        ajax('GET', baseUri + 'config/' + cameraId + '/get/?force=true', null, function (data) {
+            if (data == null || data.error) {
+                showErrorMessage(data && data.error);
+                dict2CameraUi(null);
+                if (onFetch) {
+                    onFetch(null);
+                }
+                
+                return;
+            }
+            
+            dict2CameraUi(data);
+            if (onFetch) {
+                onFetch(data);
+            }
+        });
+    }
+    else {
+        dict2CameraUi({});
+        if (onFetch) {
+            onFetch({});
+        }
+    }
+}
+
+function pushMainConfig(reboot) {
+    if (!initialConfigFetched) {
+        return;
+    }
+    
+    var mainConfig = mainUi2Dict();
+    
+    pushConfigReboot = pushConfigReboot || reboot;
+    pushConfigs['main'] = mainConfig;
+    if (!isApplyVisible()) {
+        showApply();
+    }
+}
+
+function pushCameraConfig(reboot) {
+    if (!initialConfigFetched) {
+        return;
+    }
+    
+    var cameraId = $('#cameraSelect').val();
+    if (!cameraId) {
+        return; /* event triggered without a selected camera */
+    }
+
+    var cameraConfig = cameraUi2Dict();
+
+    pushConfigReboot = pushConfigReboot || reboot;
+    pushConfigs[cameraId] = cameraConfig;
+    if (!isApplyVisible()) {
+        showApply();
+    }
+    
+    /* also update the config stored in the camera frame div */
+    var cameraFrame = $('div.camera-frame#camera' + cameraId);
+    if (cameraFrame.length) {
+        Object.update(cameraFrame[0].config, cameraConfig);
+    }
+}
+
+function pushPreview(control) {
+    var cameraId = $('#cameraSelect').val();
+    
+    var brightness = $('#brightnessSlider').val();
+    var contrast= $('#contrastSlider').val();
+    var saturation = $('#saturationSlider').val();
+    var hue = $('#hueSlider').val();
+    
+    var data = {};
+    
+    if (brightness !== '' && (!control || control == 'brightness')) {
+        data.brightness = brightness;
+    }
+    
+    if (contrast !== '' && (!control || control == 'contrast')) {
+        data.contrast = contrast;
+    }
+    
+    if (saturation !== '' && (!control || control == 'saturation')) {
+        data.saturation = saturation;
+    }
+    
+    if (hue !== '' && (!control || control == 'hue')) {
+        data.hue = hue;
+    }
+    
+    refreshDisabled[cameraId] |= 0;
+    refreshDisabled[cameraId]++;
+    
+    ajax('POST', baseUri + 'config/' + cameraId + '/set_preview/', data, function (data) {
+        refreshDisabled[cameraId]--;
+        
+        if (data == null || data.error) {
+            showErrorMessage(data && data.error);
+            return;
+        }
+    });
+}
+
+function getCameraIdsByInstance() {
+    /* a motion instance is identified by the (host, port) pair;
+     * the local instance has both the host and the port set to empty string */
+    
+    var cameraIdsByInstance = {};
+    $('div.camera-frame').each(function () {
+        var instance;
+        if (this.config.proto == 'netcam' || this.config.proto == 'v4l2') {
+            instance = '';
+        }
+        else if (this.config.proto == 'motioneye') {
+            instance = this.config.host || '';
+            if (this.config.port) {
+                instance += ':' + this.config.port;
+            }
+        }
+        else { /* assuming simple mjpeg camera */
+            return;
+        }
+        
+        (cameraIdsByInstance[instance] = cameraIdsByInstance[instance] || []).push(this.config.id);
+    });
+    
+    return cameraIdsByInstance;
+}
+
+
+    /* dialogs */
+
+function runAlertDialog(message, onOk, options) {
+    var params = {
+        title: message,
+        buttons: 'ok',
+        onOk: onOk
+    };
+    
+    if (options) {
+        Object.update(params, options);
+    }
+    
+    runModalDialog(params);
+}
+
+function runConfirmDialog(message, onYes, options) {
+    var params = {
+        title: message,
+        buttons: 'yesno',
+        onYes: onYes
+    };
+    
+    if (options) {
+        Object.update(params, options);
+    }
+    
+    runModalDialog(params);
+}
+
+function runLoginDialog(retry) {
+    /* a workaround so that browsers will remember the credentials */
+    var tempFrame = $('<iframe name="temp" id="temp" style="display: none;"></iframe>');
+    $('body').append(tempFrame);
+    
+    var form = 
+            $('<form action="' + baseUri + 'login/" target="temp" method="POST"><table class="login-dialog">' +
+                '<tr>' +
+                    '<td class="login-dialog-error" colspan="100"></td>' +
+                '</tr>' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Username</span></td>' +
+                    '<td class="dialog-item-value"><input type="text" name="username" class="styled" id="usernameEntry"></td>' +
+                '</tr>' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Password</span></td>' +
+                    '<td class="dialog-item-value"><input type="password" name="password" class="styled" id="passwordEntry"></td>' +
+                    '<input type="submit" style="display: none;" name="login" value="login">' +
+                '</tr>' +
+            '</table></form>');
+
+    var usernameEntry = form.find('#usernameEntry');
+    var passwordEntry = form.find('#passwordEntry');
+    var errorTd = form.find('td.login-dialog-error');
+    
+    if (window._loginRetry) {
+        errorTd.css('display', 'table-cell');
+        errorTd.html('Invalid credentials.');
+    }
+
+    var params = {
+        title: 'Login',
+        content: form,
+        buttons: [
+            {caption: 'Cancel', isCancel: true, click: function () {
+                tempFrame.remove();
+            }},
+            {caption: 'Login', isDefault: true, click: function () {
+                window.username = usernameEntry.val();
+                window.password = passwordEntry.val();
+                window._loginDialogSubmitted = true;
+                
+                setCookie('username', window.username);
+                
+                form.submit();
+                setTimeout(function () {
+                    tempFrame.remove();
+                }, 5000);
+                
+                if (retry) {
+                    retry();
+                }
+            }}
+        ],
+    };
+    
+    runModalDialog(params);
+}
+
+function runPictureDialog(entries, pos, mediaType) {
+    var content = $('<div class="picture-dialog-content"></div>');
+    
+    var img = $('<img class="picture-dialog-content">');
+    content.append(img);
+    
+    var prevArrow = $('<div class="picture-dialog-prev-arrow button mouse-effect" title="previous picture"></div>');
+    content.append(prevArrow);
+    
+    var nextArrow = $('<div class="picture-dialog-next-arrow button mouse-effect" title="next picture"></div>');
+    content.append(nextArrow);
+    
+    var progressImg = $('<img class="picture-dialog-progress" src="' + staticUrl + 'img/modal-progress.gif">');
+    
+    function updatePicture() {
+        var entry = entries[pos];
+
+        var windowWidth = $(window).width();
+        var windowHeight = $(window).height();
+        var widthCoef = windowWidth < 1000 ? 0.8 : 0.5;
+        var heightCoef = 0.75;
+        
+        var width = parseInt(windowWidth * widthCoef);
+        var height = parseInt(windowHeight * heightCoef);        
+        
+        prevArrow.css('display', 'none');
+        nextArrow.css('display', 'none');
+        img.parent().append(progressImg);
+        updateModalDialogPosition();
+        progressImg.css('left', (img.parent().width() - progressImg.width()) / 2);
+        progressImg.css('top', (img.parent().height() - progressImg.height()) / 2);
+        
+        img.attr('src', addAuthParams('GET', baseUri + mediaType + '/' + entry.cameraId + '/preview' + entry.path));
+        img.load(function () {
+            var aspectRatio = this.naturalWidth / this.naturalHeight;
+            var sizeWidth = width * width / aspectRatio;
+            var sizeHeight = height * aspectRatio * height;
+            
+            if (sizeWidth < sizeHeight) {
+                img.width(width);
+            }
+            else {
+                img.height(height);
+            }
+            updateModalDialogPosition();
+            prevArrow.css('display', pos > 0 ? '' : 'none');
+            nextArrow.css('display', pos < entries.length - 1 ? '' : 'none');
+            progressImg.remove();
+        });
+        
+        $('div.modal-container').find('span.modal-title:last').html(entry.name);
+        updateModalDialogPosition();
+    }
+    
+    prevArrow.click(function () {
+        if (pos > 0) {
+            pos--;
+        }
+        
+        updatePicture();
+    });
+    
+    nextArrow.click(function () {
+        if (pos < entries.length - 1) {
+            pos++;
+        }
+        
+        updatePicture();
+    });
+    
+    function bodyKeyDown(e) {
+        switch (e.which) {
+            case 37:
+                if (prevArrow.is(':visible')) {
+                    prevArrow.click();
+                }
+                break;
+                
+            case 39:
+                if (nextArrow.is(':visible')) {
+                    nextArrow.click();
+                }
+                break;
+        }
+    }
+    
+    $('body').on('keydown', bodyKeyDown);
+    
+    img.load(updateModalDialogPosition);
+    
+    runModalDialog({
+        title: ' ',
+        closeButton: true,
+        buttons: [
+            {caption: 'Close'},
+            {caption: 'Download', isDefault: true, click: function () {
+                var entry = entries[pos];
+                downloadFile(mediaType + '/' + entry.cameraId + '/download' + entry.path);
+                
+                return false;
+            }}
+        ],
+        content: content,
+        stack: true,
+        onShow: updatePicture,
+        onClose: function () {
+            $('body').off('keydown', bodyKeyDown);
+        }
+    });
+}
+
+function runAddCameraDialog() {
+    if (!$('#motionEyeSwitch')[0].checked) {
+        return runAlertDialog('Please enable motionEye first!');
+    }
+    
+    if (Object.keys(pushConfigs).length) {
+        return runAlertDialog('Please apply the modified settings first!');
+    }
+    
+    var content = 
+            $('<table class="add-camera-dialog">' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Camera Type</span></td>' +
+                    '<td class="dialog-item-value"><select class="styled" id="typeSelect">' +
+                        '<option value="v4l2">Local Camera</option>' +
+                        '<option value="netcam">Network Camera</option>' +
+                        '<option value="motioneye">Remote motionEye Camera</option>' +
+                        '<option value="mjpeg">Simple MJPEG Camera</option>' +
+                    '</select></td>' +
+                    '<td><span class="help-mark" title="the type of camera you wish to add">?</span></td>' +
+                '</tr>' +
+                '<tr class="motioneye netcam mjpeg">' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">URL</span></td>' +
+                    '<td class="dialog-item-value"><input type="text" class="styled" id="urlEntry" placeholder="http://example.com:8080/cams/..."></td>' +
+                    '<td><span class="help-mark" title="the camera URL (e.g. http://example.com:8080/cam/)">?</span></td>' +
+                '</tr>' +
+                '<tr class="motioneye netcam mjpeg">' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Username</span></td>' +
+                    '<td class="dialog-item-value"><input type="text" class="styled" id="usernameEntry" placeholder="username..."></td>' +
+                    '<td><span class="help-mark" title="the username for the URL, if required (e.g. admin)">?</span></td>' +
+                '</tr>' +
+                '<tr class="motioneye netcam mjpeg">' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Password</span></td>' +
+                    '<td class="dialog-item-value"><input type="password" class="styled" id="passwordEntry" placeholder="password..."></td>' +
+                    '<td><span class="help-mark" title="the password for the URL, if required">?</span></td>' +
+                '</tr>' +
+                '<tr class="v4l2 motioneye netcam mjpeg">' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Camera</span></td>' +
+                    '<td class="dialog-item-value"><select class="styled" id="addCameraSelect"></select><span id="cameraMsgLabel"></span></td>' +
+                    '<td><span class="help-mark" title="the camera you wish to add">?</span></td>' +
+                '</tr>' +
+                '<tr class="v4l2 motioneye netcam mjpeg">' +
+                    '<td colspan="100"><div class="dialog-item-separator"></div></td>' +
+                '</tr>' +
+                '<tr class="v4l2 motioneye netcam mjpeg">' +
+                    '<td class="dialog-item-value" colspan="100"><div id="addCameraInfo"></div></td>' +
+                '</tr>' +
+            '</table>');
+    
+    /* collect ui widgets */
+    var typeSelect = content.find('#typeSelect');
+    var urlEntry = content.find('#urlEntry');
+    var usernameEntry = content.find('#usernameEntry');
+    var passwordEntry = content.find('#passwordEntry');
+    var addCameraSelect = content.find('#addCameraSelect');
+    var addCameraInfo = content.find('#addCameraInfo');
+    var cameraMsgLabel = content.find('#cameraMsgLabel');
+    
+    /* make validators */
+    makeUrlValidator(urlEntry, true);
+    makeTextValidator(usernameEntry, false);
+    makeTextValidator(typeSelect, false);
+    makeComboValidator(addCameraSelect, true);
+    
+    /* ui interaction */
+    function updateUi() {
+        content.find('tr.v4l2, tr.motioneye, tr.netcam, tr.mjpeg').css('display', 'none');
+
+        if (typeSelect.val() == 'motioneye') {
+            content.find('tr.motioneye').css('display', 'table-row');
+            usernameEntry.val('admin');
+            usernameEntry.attr('readonly', 'readonly');
+            addCameraInfo.html(
+                    'Remote motionEye cameras are cameras installed behind another motionEye server. ' +
+                    'Adding them here will allow you to view and manage them remotely.');
+        }
+        else if (typeSelect.val() == 'netcam') {
+            usernameEntry.removeAttr('readonly');
+            
+            /* make sure there is one trailing slash so that
+             * an URI can be detected */
+            var url = urlEntry.val().trim();
+            var m = url.match(new RegExp('/', 'g'));
+            if (m && m.length < 3 && !url.endsWith('/')) {
+                urlEntry.val(url + '/');
+            }
+
+            content.find('tr.netcam').css('display', 'table-row');
+            addCameraInfo.html(
+                    'Network cameras (or IP cameras) are devices that natively stream RTSP or MJPEG videos or plain JPEG images. ' +
+                    "Consult your device's manual to find out the correct RTSP, MJPEG or JPEG URL.");
+        }
+        else if (typeSelect.val() == 'mjpeg') {
+            usernameEntry.removeAttr('readonly');
+            
+            /* make sure there is one trailing slash so that
+             * an URI can be detected */
+            var url = urlEntry.val().trim();
+            var m = url.match(new RegExp('/', 'g'));
+            if (m && m.length < 3 && !url.endsWith('/')) {
+                urlEntry.val(url + '/');
+            }
+
+            content.find('tr.mjpeg').css('display', 'table-row');
+            addCameraInfo.html(
+                    'Adding your device as a simple MJPEG camera instead of as a network camera will improve the framerate, ' +
+                    'but no motion detection, picture capturing or movie recording will be available for it. ' +
+                    'The camera must be accessible to both your server and your browser. ' +
+                    'This type of camera is not compatible with Internet Explorer.');
+        }
+        else { /* assuming v4l2 */
+            content.find('tr.v4l2').css('display', 'table-row');
+            addCameraInfo.html(
+                    'Local cameras are camera devices that are connected directly to your motionEye system. ' +
+                    'These are usually USB webcams or board-specific cameras.');
+        }
+        
+        updateModalDialogPosition();
+        
+        /* re-validate all the validators */
+        content.find('.validator').each(function () {
+            this.validate();
+        });
+        
+        if (uiValid()) {
+            listCameras();
+        }
+    }
+    
+    function uiValid(includeCameraSelect) {
+        var query = content.find('input, select');
+        if (!includeCameraSelect) {
+            query = query.not('#addCameraSelect');
+        }
+        else {
+            if (cameraMsgLabel.html() || !addCameraSelect.val()) {
+                return false;
+            }
+        }
+
+        /* re-validate all the validators */
+        content.find('.validator').each(function () {
+            this.validate();
+        });
+        
+        var valid = true;
+        query.each(function () {
+            if (this.invalid) {
+                valid = false;
+                return false;
+            }
+        });
+        
+        return valid;
+    }
+    
+    function splitCameraUrl(url) {
+        var parts = url.split('://');
+        var scheme = parts[0];
+        var index = parts[1].indexOf('/');
+        var host = null;
+        var uri = '';
+        if (index >= 0) {
+            host = parts[1].substring(0, index);
+            uri = parts[1].substring(index);
+        }
+        else {
+            host = parts[1];
+        }
+        
+        var port = '';
+        parts = host.split(':');
+        if (parts.length >= 2) {
+            host = parts[0];
+            port = parts[1];
+        }
+        
+        if (uri == '') {
+            uri = '/';
+        }
+        
+        return {
+            scheme: scheme,
+            host: host,
+            port: port,
+            uri: uri
+        };
+    }
+    
+    function listCameras() {
+        var progress = $('<div style="text-align: center; margin: 2px;"><img src="' + staticUrl + 'img/small-progress.gif"></div>');
+        
+        addCameraSelect.html('');
+        addCameraSelect.hide();
+        addCameraSelect.parent().find('div').remove(); /* remove any previous progress div */
+        addCameraSelect.before(progress);
+        
+        var data = {};
+        if (urlEntry.is(':visible') && urlEntry.val()) {
+            data = splitCameraUrl(urlEntry.val());
+        }
+        data.username = usernameEntry.val();
+        data.password = passwordEntry.val();
+        data.proto = typeSelect.val();
+        
+        cameraMsgLabel.html('');
+        
+        ajax('GET', baseUri + 'config/list/', data, function (data) {
+            progress.remove();
+            
+            if (data == null || data.error) {
+                cameraMsgLabel.html(data && data.error);
+                
+                return;
+            }
+            
+            if (data.error || !data.cameras) {
+                return;
+            }
+
+            data.cameras.forEach(function (info) {
+                var option = $('<option value="' + info.id + '">' + info.name + '</option>');
+                option[0]._extra_attrs = {};
+                Object.keys(info).forEach(function (key) {
+                    if (key == 'id' || key == 'name') {
+                        return;
+                    }
+                    
+                    var value = info[key];
+                    option[0]._extra_attrs[key] = value;
+                });
+
+                addCameraSelect.append(option);
+            });
+            
+            if (!data.cameras || !data.cameras.length) {
+                addCameraSelect.append('<option value="">(no cameras)</option>');
+            }
+            
+            addCameraSelect.show();
+            addCameraSelect[0].validate();
+        });
+    }
+    
+    typeSelect.change(function () {
+        addCameraSelect.html('');
+    });
+    
+    typeSelect.change(updateUi);
+    urlEntry.change(updateUi);
+    usernameEntry.change(updateUi);
+    passwordEntry.change(updateUi);
+    updateUi();
+
+    runModalDialog({
+        title: 'Add Camera...',
+        closeButton: true,
+        buttons: 'okcancel',
+        content: content,
+        onOk: function () {
+            if (!uiValid(true)) {
+                return false;
+            }
+
+            var data = {};
+            
+            if (typeSelect.val() == 'motioneye') {
+                data = splitCameraUrl(urlEntry.val());
+                data.proto = 'motioneye';
+                data.username = usernameEntry.val();
+                data.password = passwordEntry.val();
+                data.remote_camera_id = addCameraSelect.val();
+            }
+            else if (typeSelect.val() == 'netcam') {
+                data = splitCameraUrl(urlEntry.val());
+                data.username = usernameEntry.val();
+                data.password = passwordEntry.val();
+                data.proto = 'netcam';
+                data.camera_index = addCameraSelect.val();
+            }
+            else if (typeSelect.val() == 'mjpeg') {
+                data = splitCameraUrl(urlEntry.val());
+                data.username = usernameEntry.val();
+                data.password = passwordEntry.val();
+                data.proto = 'mjpeg';
+            }
+            else { /* assuming v4l2 */
+                data.proto = 'v4l2';
+                data.uri = addCameraSelect.val();
+            }
+            
+            /* add all extra attributes */
+            var option = addCameraSelect.find('option:eq(' + addCameraSelect[0].selectedIndex + ')')[0];
+            Object.keys(option._extra_attrs).forEach(function (key) {
+                var value = option._extra_attrs[key];
+                data[key] = value;
+            });
+
+            beginProgress();
+            ajax('POST', baseUri + 'config/add/', data, function (data) {
+                endProgress();
+
+                if (data == null || data.error) {
+                    showErrorMessage(data && data.error);
+                    return;
+                }
+                
+                var cameraOption = $('#cameraSelect').find('option[value=add]');
+                cameraOption.before('<option value="' + data.id + '">' + data.name + '</option>');
+                $('#cameraSelect').val(data.id).change();
+                recreateCameraFrames();
+            });
+        }
+    });
+}
+
+function runTimelapseDialog(cameraId, groupKey, group) {
+    var content = 
+            $('<table class="timelapse-dialog">' +
+                '<tr><td colspan="2" class="timelapse-warning"></td></tr>' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Group</span></td>' +
+                    '<td class="dialog-item-value">' + groupKey + '</td>' +
+                '</tr>' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Include a picture taken every</span></td>' +
+                    '<td class="dialog-item-value">' +
+                        '<select class="styled timelapse" id="intervalSelect">' + 
+                            '<option value="1">second</option>' +
+                            '<option value="5">5 seconds</option>' +
+                            '<option value="10">10 seconds</option>' +
+                            '<option value="30">30 seconds</option>' +
+                            '<option value="60">minute</option>' +
+                            '<option value="300">5 minutes</option>' +
+                            '<option value="600">10 minutes</option>' +
+                            '<option value="1800">30 minutes</option>' +
+                            '<option value="3600">hour</option>' +
+                        '</select>' +
+                    '</td>' +
+                    '<td><span class="help-mark" title="choose the interval of time between two selected pictures">?</span></td>' +
+                '</tr>' +
+                '<tr>' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Movie framerate</span></td>' +
+                    '<td class="dialog-item-value"><input type="text" class="styled range" id="framerateSlider"></td>' +
+                    '<td><span class="help-mark" title="choose how fast you want the timelapse playback to be">?</span></td>' +
+                '</tr>' +
+            '</table>');
+
+    var intervalSelect = content.find('#intervalSelect');
+    var framerateSlider = content.find('#framerateSlider');
+    var timelapseWarning = content.find('td.timelapse-warning');
+    
+    if (group.length > 1440) { /* one day worth of pictures, taken 1 minute apart */
+        timelapseWarning.html('Given the large number of pictures, creating your timelapse might take a while!');
+        timelapseWarning.css('display', 'table-cell');
+    }
+    
+    makeSlider(framerateSlider, 1, 100, 0, [
+        {value: 1, label: '1'},
+        {value: 20, label: '20'},
+        {value: 40, label: '40'},
+        {value: 60, label: '60'},
+        {value: 80, label: '80'},
+        {value: 100, label: '100'}
+    ], null, 0);
+    
+    intervalSelect.val(60);
+    framerateSlider.val(20).each(function () {this.update()});
+
+    runModalDialog({
+        title: 'Create Timelapse Movie',
+        closeButton: true,
+        buttons: 'okcancel',
+        content: content,
+        onOk: function () {
+            var progressBar = $('<div style=""></div>');
+            makeProgressBar(progressBar);
+            
+            runModalDialog({
+                title: 'Creating Timelapse Movie...',
+                content: progressBar,
+                stack: true,
+                noKeys: true
+            });
+            
+            var url = baseUri + 'picture/' + cameraId + '/timelapse/' + groupKey + '/';
+            var data = {interval: intervalSelect.val(), framerate: framerateSlider.val()};
+            var first = true;
+            
+            function checkTimelapse() {
+                var actualUrl = url;
+                if (!first) {
+                    actualUrl += '?check=true';
+                }
+
+                ajax('GET', actualUrl, data, function (data) {
+                    if (data == null || data.error) {
+                        hideModalDialog(); /* progress */
+                        hideModalDialog(); /* timelapse dialog */
+                        showErrorMessage(data && data.error);
+                        return;
+                    }
+                    
+                    if (data.progress != -1 && first) {
+                        showPopupMessage('A timelapse movie is already being created.');
+                    }
+                    
+                    if (data.progress == -1 && !first && !data.key) {
+                        hideModalDialog(); /* progress */
+                        hideModalDialog(); /* timelapse dialog */
+                        showErrorMessage('The timelapse movie could not be created.');
+                        return;
+                    }
+                    
+                    if (data.progress == -1) {
+                        data.progress = 0;
+                    }
+
+                    if (data.key) {
+                        progressBar[0].setProgress(100);
+                        progressBar[0].setText('100%');
+                        
+                        setTimeout(function () {
+                            hideModalDialog(); /* progress */
+                            hideModalDialog(); /* timelapse dialog */
+                            downloadFile('picture/' + cameraId + '/timelapse/' + groupKey + '/?key=' + data.key);
+                        }, 500);
+                    }
+                    else {
+                        progressBar[0].setProgress(data.progress * 100);
+                        progressBar[0].setText(parseInt(data.progress * 100) + '%');
+                        setTimeout(checkTimelapse, 1000);
+                    }
+
+                    first = false;
+                });
+            }
+            
+            checkTimelapse();
+
+            return false;
+        },
+        stack: true
+    });
+}
+
+function runMediaDialog(cameraId, mediaType) {
+    var dialogDiv = $('<div class="media-dialog"></div>');
+    var mediaListDiv = $('<div class="media-dialog-list"></div>');
+    var groupsDiv = $('<div class="media-dialog-groups"></div>');
+    var buttonsDiv = $('<div class="media-dialog-buttons"></div>');
+    
+    var groups = {};
+    var groupKey = null;
+    
+    dialogDiv.append(groupsDiv);
+    dialogDiv.append(mediaListDiv);
+    dialogDiv.append(buttonsDiv);
+    
+    /* add a temporary div to compute 3em in px */
+    var tempDiv = $('<div style="width: 3em; height: 3em;"></div>');
+    $('div.modal-container').append(tempDiv);
+    var height = tempDiv.height();
+    tempDiv.remove();
+
+    function showGroup(key) {
+        groupKey = key;
+        
+        if (mediaListDiv.find('img.media-list-progress').length) {
+            return; /* already in progress of loading */
+        }
+        
+        /* (re)set the current state of the group buttons */
+        groupsDiv.find('div.media-dialog-group-button').each(function () {
+            var $this = $(this);
+            if (this.key == key) {
+                $this.addClass('current');
+            }
+            else {
+                $this.removeClass('current');
+            }
+        });
+        
+        var mediaListByName = {};
+        var entries = groups[key];
+        
+        /* cleanup the media list */
+        mediaListDiv.children('div.media-list-entry').detach();
+        mediaListDiv.html('');
+        
+        function addEntries() {
+            /* add the entries to the media list */
+            entries.forEach(function (entry, i) {
+                var entryDiv = entry.div;
+                var detailsDiv = null;
+                
+                if (!entryDiv) {
+                    entryDiv = $('<div class="media-list-entry"></div>');
+                    
+                    var previewImg = $('<img class="media-list-preview" src="' + staticUrl + 'img/modal-progress.gif"/>');
+                    entryDiv.append(previewImg);
+                    previewImg[0]._src = addAuthParams('GET', baseUri + mediaType + '/' + cameraId + '/preview' + entry.path + '?height=' + height);
+                    
+                    var downloadButton = $('<div class="media-list-download-button button">Download</div>');
+                    entryDiv.append(downloadButton);
+                    
+                    var deleteButton = $('<div class="media-list-delete-button button">Delete</div>');
+                    if (isAdmin()) {
+                        entryDiv.append(deleteButton);
+                    }
+
+                    var nameDiv = $('<div class="media-list-entry-name">' + entry.name + '</div>');
+                    entryDiv.append(nameDiv);
+                    
+                    detailsDiv = $('<div class="media-list-entry-details"></div>');
+                    entryDiv.append(detailsDiv);
+                    
+                    downloadButton.click(function () {
+                        downloadFile(mediaType + '/' + cameraId + '/download' + entry.path);
+                        return false;
+                    });
+                    
+                    deleteButton.click(function () {
+                        doDeleteFile(baseUri + mediaType + '/' + cameraId + '/delete' + entry.path, function () {
+                            entryDiv.remove();
+                            entries.splice(i, 1); /* remove entry from group */
+
+                            /* update text on group button */
+                            groupsDiv.find('div.media-dialog-group-button').each(function () {
+                                var $this = $(this);
+                                if (this.key == groupKey) {
+                                    var text = this.innerHTML;
+                                    text = text.substring(0, text.lastIndexOf(' '));
+                                    text += ' (' + entries.length + ')';
+                                    this.innerHTML = text;
+                                }
+                            });
+                        });
+                        
+                        return false;
+                    });
+
+                    entryDiv.click(function () {
+                        var pos = entries.indexOf(entry);
+                        runPictureDialog(entries, pos, mediaType);
+                    });
+                    
+                    entry.div = entryDiv;
+                }
+                else {
+                    detailsDiv = entry.div.find('div.media-list-entry-details');
+                }                    
+                
+                var momentSpan = $('<span class="details-moment">' + entry.momentStr + ', </span>');
+                var momentShortSpan = $('<span class="details-moment-short">' + entry.momentStrShort + '</span>');
+                var sizeSpan = $('<span class="details-size">' + entry.sizeStr + '</span>');
+                detailsDiv.empty();
+                detailsDiv.append(momentSpan);
+                detailsDiv.append(momentShortSpan);
+                detailsDiv.append(sizeSpan);
+                mediaListDiv.append(entryDiv);
+            });
+
+            /* trigger a scroll event */
+            mediaListDiv.scroll();
+        }
+        
+        /* if details are already fetched, simply add the entries and return */
+        if (entries[0].timestamp) {
+            return addEntries();
+        }
+        
+        var previewImg = $('<img class="media-list-progress" src="' + staticUrl + 'img/modal-progress.gif"/>');
+        mediaListDiv.append(previewImg);
+        
+        var url = baseUri + mediaType + '/' + cameraId + '/list/?prefix=' + (key || 'ungrouped');
+        ajax('GET', url, null, function (data) {
+            previewImg.remove();
+            
+            if (data == null || data.error) {
+                hideModalDialog();
+                showErrorMessage(data && data.error);
+                return;
+            }
+            
+            /* index the media list by name */
+            data.mediaList.forEach(function (media) {
+                var path = media.path;
+                var parts = path.split('/');
+                var name = parts[parts.length - 1];
+                
+                mediaListByName[name] = media;
+            });
+            
+            /* assign details to entries */
+                entries.forEach(function (entry) {
+                    var media = mediaListByName[entry.name];
+                    if (media) {
+                        entry.momentStr = media.momentStr;
+                        entry.momentStrShort = media.momentStrShort;
+                        entry.sizeStr = media.sizeStr;
+                        entry.timestamp = media.timestamp;
+                    }
+                });
+                /* sort the entries by timestamp */
+            entries.sortKey(function (e) {return e.timestamp || e.name;}, true);
+            
+            addEntries();
+        });
+    }
+    
+    if (mediaType == 'picture') {
+        var zippedButton = $('<div class="media-dialog-button">Zipped</div>');
+        buttonsDiv.append(zippedButton);
+        
+        zippedButton.click(function () {
+            if (groupKey != null) {
+                doDownloadZipped(cameraId, groupKey);
+            }
+        });
+        
+        var timelapseButton = $('<div class="media-dialog-button">Timelapse</div>');
+        buttonsDiv.append(timelapseButton);
+        
+        timelapseButton.click(function () {
+            if (groupKey != null) {
+                runTimelapseDialog(cameraId, groupKey, groups[groupKey]);
+            }
+        });
+    }
+
+    if (isAdmin()) {
+        var deleteAllButton = $('<div class="media-dialog-button media-dialog-delete-all-button">Delete All</div>');
+        buttonsDiv.append(deleteAllButton);
+        
+        deleteAllButton.click(function () {
+            if (groupKey != null) {
+                doDeleteAllFiles(mediaType, cameraId, groupKey, function () {
+                    /* delete th group button */
+                    groupsDiv.find('div.media-dialog-group-button').each(function () {
+                        var $this = $(this);
+                        if (this.key == groupKey) {
+                            $this.remove();
+                        }
+                    });
+                    
+                    /* delete the group itself */
+                    delete groups[groupKey];
+                    
+                    /* show the first existing group, if any */
+                    var keys = Object.keys(groups);
+                    if (keys.length) {
+                        showGroup(keys[0]);
+                    }
+                    else {
+                        hideModalDialog();
+                    }
+                });
+            }
+        });
+    }
+    
+    function updateDialogSize() {
+        var windowWidth = $(window).width();
+        var windowHeight = $(window).height();
+        
+        if (Object.keys(groups).length == 0) {
+            groupsDiv.width('auto');
+            groupsDiv.height('auto');
+            groupsDiv.addClass('small-screen');
+            mediaListDiv.width('auto');
+            mediaListDiv.height('auto');
+            buttonsDiv.hide();
+
+            return;
+        }
+        
+        buttonsDiv.show();
+        
+        if (windowWidth < 1000) {
+            mediaListDiv.width(parseInt(windowWidth * 0.8));
+            mediaListDiv.height(parseInt(windowHeight * 0.7));
+            groupsDiv.width(parseInt(windowWidth * 0.8));
+            groupsDiv.height('');
+            groupsDiv.addClass('small-screen');
+        }
+        else {
+            mediaListDiv.width(parseInt(windowWidth * 0.7));
+            mediaListDiv.height(parseInt(windowHeight * 0.7));
+            groupsDiv.height(parseInt(windowHeight * 0.7));
+            groupsDiv.width('');
+            groupsDiv.removeClass('small-screen');
+        }
+    }
+    
+    function onResize() {
+        updateDialogSize();
+        updateModalDialogPosition();
+    }
+    
+    $(window).resize(onResize);
+    
+    updateDialogSize();
+    
+    showModalDialog('<div class="modal-progress"></div>');
+    
+    /* fetch the media list */
+    ajax('GET', baseUri + mediaType + '/' + cameraId + '/list/', null, function (data) {
+        if (data == null || data.error) {
+            hideModalDialog();
+            showErrorMessage(data && data.error);
+            return;
+        }
+        
+        /* group the media */
+        data.mediaList.forEach(function (media) {
+            var path = media.path;
+            var parts = path.split('/');
+            var keyParts = parts.splice(0, parts.length - 1);
+            var key = keyParts.join('/');
+            
+            if (key.indexOf('/') === 0) {
+                key = key.substring(1);
+            }
+            
+            var list = (groups[key] = groups[key] || []);
+            
+            list.push({
+                'path': path,
+                'group': key,
+                'name': parts[parts.length - 1],
+                'cameraId': cameraId
+            });
+        });
+        
+        updateDialogSize();
+        
+        var keys = Object.keys(groups);
+        keys.sort();
+        keys.reverse();
+        
+        if (keys.length) {
+            keys.forEach(function (key) {
+                var groupButton = $('<div class="media-dialog-group-button"></div>');
+                groupButton.text((key || '(ungrouped)') + ' (' + groups[key].length + ')');
+                groupButton[0].key = key;
+                
+                groupButton.click(function () {
+                    showGroup(key);
+                });
+                
+                groupsDiv.append(groupButton);
+            });
+            
+            /* add tooltips to larger group buttons */
+            setTimeout(function () {
+                groupsDiv.find('div.media-dialog-group-button').each(function () {
+                    if (this.scrollWidth > this.offsetWidth) {
+                        this.title = this.innerHTML;
+                    }
+                });
+            }, 10);
+        }
+        else {
+            groupsDiv.html('(no media files)');
+            mediaListDiv.remove();
+        }
+        
+        var title;
+        if ($(window).width() < 1000) {
+            title = data.cameraName;
+        }
+        else if (mediaType === 'picture') {
+            title = 'Pictures taken by ' + data.cameraName;
+        }
+        else {
+            title = 'Movies recorded by ' + data.cameraName;
+        }
+        
+        runModalDialog({
+            title: title,
+            closeButton: true,
+            buttons: '',
+            content: dialogDiv,
+            onShow: function () {
+                //dialogDiv.scrollTop(dialogDiv.prop('scrollHeight'));
+                if (keys.length) {
+                    showGroup(keys[0]);
+                }
+            },
+            onClose: function () {
+                $(window).unbind('resize', onResize);
+            }
+        });
+    });
+    
+    /* install the media list scroll event handler */
+    mediaListDiv.scroll(function () {
+        var height = mediaListDiv.height();
+        
+        mediaListDiv.find('img.media-list-preview').each(function () {
+            if (!this._src) {
+                return;
+            }
+            
+            var $this = $(this);
+            var entryDiv = $this.parent();
+            
+            var top1 = entryDiv.position().top;
+            var top2 = top1 + entryDiv.height();
+            
+            if ((top1 >= 0 && top1 <= height) ||
+                (top2 >= 0 && top2 <= height)) {
+                
+                this.src = this._src;
+                delete this._src;
+            }
+        });
+    });
+}
+
+
+    /* camera frames */
+
+function addCameraFrameUi(cameraConfig) {
+    var pageContainer = $('div.page-container');
+    
+    if (cameraConfig == null) {
+        var cameraFrameDivPlaceHolder = $('<div class="camera-frame-place-holder"></div>');
+        pageContainer.append(cameraFrameDivPlaceHolder);
+        
+        return;
+    }
+    
+    var cameraId = cameraConfig.id;
+    
+    var cameraFrameDiv = $(
+            '<div class="camera-frame">' +
+                '<div class="camera-top-bar">' +
+                    '<span class="camera-name"></span>' +
+                    '<div class="camera-buttons">' +
+                        '<div class="button camera-button mouse-effect full-screen" title="full-screen window"></div>' +
+                        '<div class="button camera-button mouse-effect media-pictures" title="pictures"></div>' +
+                        '<div class="button camera-button mouse-effect media-movies" title="movies"></div>' +
+                        '<div class="button camera-button mouse-effect configure" title="configure"></div>' +
+                    '</div>' +
+                '</div>' +
+                '<div class="camera-container">' +
+                    '<div class="camera-placeholder"><img class="no-camera" src="' + staticUrl + 'img/no-camera.svg"></div>' +
+                    '<img class="camera">' +
+                    '<div class="camera-progress"><img class="camera-progress"></div>' +
+                '</div>' +
+            '</div>');
+    
+    var nameSpan = cameraFrameDiv.find('span.camera-name');
+    var configureButton = cameraFrameDiv.find('div.camera-button.configure');
+    var picturesButton = cameraFrameDiv.find('div.camera-button.media-pictures');
+    var moviesButton = cameraFrameDiv.find('div.camera-button.media-movies');
+    var fullScreenButton = cameraFrameDiv.find('div.camera-button.full-screen');
+    var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
+    var cameraProgress = cameraFrameDiv.find('div.camera-progress');
+    var cameraImg = cameraFrameDiv.find('img.camera');
+    var progressImg = cameraFrameDiv.find('img.camera-progress');
+    
+    /* no camera buttons if not admin */
+    if (!isAdmin()) {
+        configureButton.hide();
+    }
+    
+    /* no media buttons for simple mjpeg cameras */
+    if (cameraConfig['proto'] == 'mjpeg') {
+        picturesButton.hide();
+        moviesButton.hide();
+    }
+    
+    cameraFrameDiv.attr('id', 'camera' + cameraId);
+    cameraFrameDiv[0].refreshDivider = 0;
+    cameraFrameDiv[0].config = cameraConfig;
+    nameSpan.html(cameraConfig.name);
+    progressImg.attr('src', staticUrl + 'img/camera-progress.gif');
+    
+    cameraProgress.click(function () {
+        doFullScreenCamera(cameraId);
+    });
+    
+    cameraProgress.addClass('visible');
+    cameraPlaceholder.css('opacity', '0');
+    
+    /* insert the new camera frame at the right position,
+     * with respect to the camera id */
+    var cameraFrames = pageContainer.find('div.camera-frame');
+    var cameraIds = cameraFrames.map(function () {return parseInt(this.id.substring(6));});
+    cameraIds.sort();
+    
+    var index = 0; /* find the first position that is greater than the current camera id */
+    while (index < cameraIds.length && cameraIds[index] < cameraId) {
+        index++;
+    }
+    
+    if (index < cameraIds.length) {
+        var beforeCameraFrame = pageContainer.find('div.camera-frame#camera' + cameraIds[index]);
+        cameraFrameDiv.insertAfter(beforeCameraFrame);
+    }
+    else  {
+        pageContainer.append(cameraFrameDiv);
+    }
+
+    /* fade in */
+    cameraFrameDiv.animate({'opacity': 1}, 100);
+    
+    /* add the button handlers */
+    configureButton.click(function () {
+        doConfigureCamera(cameraId);
+    });
+
+    picturesButton.click(function (cameraId) {
+        return function () {
+            runMediaDialog(cameraId, 'picture');
+        };
+    }(cameraId));
+    
+    moviesButton.click(function (cameraId) {
+        return function () {
+            runMediaDialog(cameraId, 'movie');
+        };
+    }(cameraId));
+    
+    fullScreenButton.click(function (cameraId) {
+        return function () {
+            var url = baseUri + 'picture/' + cameraId + '/frame/';
+            window.open(url, '_blank');
+        };
+    }(cameraId));
+    
+    /* error and load handlers */
+    cameraImg.error(function () {
+        this.error = true;
+        this.loading = 0;
+        
+        cameraImg.addClass('error').removeClass('loading');
+        cameraImg.height(Math.round(cameraImg.width() * 0.75));
+        cameraPlaceholder.css('opacity', 1);
+        cameraProgress.removeClass('visible');
+        cameraFrameDiv.removeClass('motion-detected');
+    });
+    cameraImg.load(function () {
+        if (refreshDisabled[cameraId]) {
+            return; /* refresh temporarily disabled for updating */
+        }
+        
+        this.error = false;
+        this.loading = 0;
+        
+        cameraImg.removeClass('error').removeClass('loading');
+        cameraImg.css('height', '');
+        cameraPlaceholder.css('opacity', 0);
+        cameraProgress.removeClass('visible');
+        
+        /* there's no point in looking for a cookie update more often than once every second */
+        var now = new Date().getTime();
+        if ((!this.lastCookieTime || now - this.lastCookieTime > 1000) && (cameraFrameDiv[0].config['proto'] != 'mjpeg')) {
+            if (getCookie('motion_detected_' + cameraId) == 'true') {
+                cameraFrameDiv.addClass('motion-detected');
+            }
+            else {
+                cameraFrameDiv.removeClass('motion-detected');
+            }
+            
+            this.lastCookieTime = now;
+        }
+
+        if (fullScreenCameraId) {
+            /* update the modal dialog position when image is loaded */
+            updateModalDialogPosition();
+        }
+    });
+    
+    cameraImg.addClass('loading');
+    cameraImg.height(Math.round(cameraImg.width() * 0.75));
+}
+
+function remCameraFrameUi(cameraId) {
+    var pageContainer = $('div.page-container');
+    var cameraFrameDiv = pageContainer.find('div.camera-frame#camera' + cameraId);
+    cameraFrameDiv.animate({'opacity': 0}, 100, function () {
+        cameraFrameDiv.remove();
+    });
+}
+
+function recreateCameraFrames(cameras) {
+    var pageContainer = $('div.page-container');
+    
+    function updateCameras(cameras) {
+        cameras = cameras.filter(function (camera) {return camera.enabled;});
+        var i, camera;
+
+        /* remove everything on the page */
+        pageContainer.children().remove();
+        
+        /* add camera frames */
+        for (i = 0; i < cameras.length; i++) {
+            camera = cameras[i];
+            addCameraFrameUi(camera);
+        }
+        
+        if ($('#cameraSelect').find('option').length < 2 && isAdmin() && $('#motionEyeSwitch')[0].checked) {
+            /* invite the user to add a camera */
+            var addCameraLink = $('<div class="add-camera-message">' + 
+                    '<a href="javascript:runAddCameraDialog()">You have not configured any camera yet. Click here to add one...</a></div>');
+            pageContainer.append(addCameraLink);
+        }
+    }
+    
+    if (cameras != null) {
+        updateCameras(cameras);
+    }
+    else {
+        ajax('GET', baseUri + 'config/list/', null, function (data) {
+            if (data == null || data.error) {
+                showErrorMessage(data && data.error);
+                return;
+            }
+            
+            updateCameras(data.cameras);
+        });
+    }
+    
+    /* update settings panel */
+    var cameraId = $('#cameraSelect').val();
+    if (cameraId && cameraId != 'add') {
+        openSettings(cameraId);
+    }
+}
+
+
+function doConfigureCamera(cameraId) {
+    if (inProgress) {
+        return;
+    }
+    
+    hideApply();
+    pushConfigs = {};
+    pushConfigReboot = false;
+    
+    openSettings(cameraId);
+}
+
+function doFullScreenCamera(cameraId) {
+    if (inProgress || refreshDisabled[cameraId]) {
+        return;
+    }
+    
+    if (fullScreenCameraId != null) {
+        return; /* a camera is already in full screen */
+    }
+    
+    fullScreenCameraId = -1; /* avoids successive fast toggles of fullscreen */
+    
+    var cameraFrameDiv = $('#camera' + cameraId);
+    var cameraName = cameraFrameDiv.find('span.camera-name').text();
+    var frameImg = cameraFrameDiv.find('img.camera');
+    var aspectRatio = frameImg.width() / frameImg.height();
+    var windowWidth = $(window).width();
+    var windowHeight = $(window).height();
+    var windowAspectRatio = windowWidth / windowHeight;
+    var frameIndex = cameraFrameDiv.index();
+    var pageContainer = $('div.page-container');
+    
+    if (frameImg.hasClass('error')) {
+        return; /* no full screen for erroneous cameras */
+    }
+
+    var width;
+    if (windowAspectRatio > aspectRatio) {
+        width = aspectRatio * Math.round(0.8 * windowHeight);
+    }
+    else {
+        width = Math.round(0.9 * windowWidth);
+    }
+
+    cameraFrameDiv.find('div.camera-progress').addClass('visible');
+    
+    var cameraImg = cameraFrameDiv.find('img.camera');
+    cameraImg.load(function showFullScreenCamera() {
+        cameraFrameDiv.css('width', width);
+        fullScreenCameraId = cameraId;
+        
+        runModalDialog({
+            title: cameraName,
+            closeButton: true,
+            content: cameraFrameDiv,
+            onShow: function () {
+                cameraImg.unbind('load', showFullScreenCamera);
+            },
+            onClose: function () {
+                fullScreenCameraId = null;
+                cameraFrameDiv.css('width', '');
+                var nextFrame = pageContainer.children('div:eq(' + frameIndex + ')');
+                if (nextFrame.length) {
+                    nextFrame.before(cameraFrameDiv);
+                }
+                else {
+                    pageContainer.append(cameraFrameDiv);
+                }
+            }
+        });
+    });
+    
+    if (cameraFrameDiv[0].config['proto'] == 'mjpeg') {
+        /* manually trigger the load event on simple mjpeg cameras */
+        cameraImg.load();
+    }
+}
+
+function refreshCameraFrames() {
+    function refreshCameraFrame(cameraId, img, serverSideResize) {
+        if (refreshDisabled[cameraId]) {
+            /* camera refreshing disabled, retry later */
+            
+            return;
+        }
+        
+        if (img.loading) {
+            img.loading++; /* increases each time the camera would refresh but is still loading */
+            
+            if (img.loading > 2 * 1000 / refreshInterval) { /* limits the retries to one every two seconds */
+                img.loading = 0;
+            }
+            else {
+                return; /* wait for the previous frame to finish loading */
+            }
+        }
+        
+        var timestamp = new Date().getTime();
+        var uri = baseUri + 'picture/' + cameraId + '/current/?_=' + timestamp;
+        if (serverSideResize) {
+            uri += '&width=' + img.width;
+        }
+        
+        uri = addAuthParams('GET', uri);
+        
+        img.src = uri;
+        img.loading = 1;
+    }
+
+    var cameraFrames;
+    if (fullScreenCameraId != null && fullScreenCameraId >= 0) {
+        cameraFrames = $('#camera' + fullScreenCameraId);
+    }
+    else {
+        cameraFrames = $('div.page-container').find('div.camera-frame');
+    }
+    
+    cameraFrames.each(function () {
+        if (!this.img) {
+            this.img = $(this).find('img.camera')[0];
+            if (this.config['proto'] == 'mjpeg') {
+                var url = this.config['url'].replace('127.0.0.1', window.location.host.split(':')[0]);
+                url += (url.indexOf('?') > 0 ? '&' : '?') + '_=' + new Date().getTime();
+                this.img.src = url;
+            }
+        }
+        
+        if (this.config['proto'] == 'mjpeg') {
+            return; /* no manual refresh for simple mjpeg cameras */
+        }
+        
+        /* at a refresh interval of 50ms, the refresh rate is limited to 20 fps */
+        var count = 1000 / (refreshInterval * this.config['streaming_framerate']);
+        var serverSideResize = this.config['streaming_server_resize'];
+        
+        if (count <= 2) {
+            /* skipping frames (showing the same frame twice) at this rate won't be visible,
+             * while the effective framerate will be as close as possible to the motion's one */
+            count -= 1;
+        }
+        
+        if (this.img.error) {
+            /* in case of error, decrease the refresh rate to 1 fps */
+            count = 1000 / refreshInterval;
+        }
+        
+        if (this.refreshDivider < count) {
+            this.refreshDivider++;
+        }
+        else {
+            var cameraId = this.id.substring(6);
+            refreshCameraFrame(cameraId, this.img, serverSideResize);
+            
+            this.refreshDivider = 0;
+        }
+    });
+    
+    setTimeout(refreshCameraFrames, refreshInterval);
+}
+
+function checkCameraErrors() {
+    /* properly triggers the onerror event on the cameras whose imgs were not successfully loaded,
+     * but the onerror event hasn't been triggered, for some reason (seems to happen in Chrome) */
+    var cameraFrames = $('div.page-container').find('img.camera');
+    
+    cameraFrames.each(function () {
+        if (this.complete === true && this.naturalWidth === 0 && !this.error && this.src) {
+            $(this).error();
+        }
+    });
+    
+    setTimeout(checkCameraErrors, 500);
+}
+
+
+    /* startup function */
+
+$(document).ready(function () {
+    /* detect base uri */
+    if (frame) {
+        baseUri = qualifyUri('../../../');
+
+    }
+    else {
+        baseUri = splitUrl(qualifyUri('')).baseUrl;
+
+        /* restore the username from cookie */
+        window.username = getCookie('username');
+    }
+    
+    /* open/close settings */
+    $('div.settings-button').click(function () {
+        if (isSettingsOpen()) {
+            closeSettings();
+        }
+        else {
+            openSettings();
+        }
+    });
+    
+    /* software update button */
+    $('div#updateButton').click(doUpdate);
+    
+    /* backup/restore */
+    $('div#backupButton').click(doBackup);
+    $('div#restoreButton').click(doRestore);
+    
+    /* prevent scroll events on settings div from propagating TODO this does not actually work */
+    $('div.settings').mousewheel(function (e, d) {
+        var t = $(this);
+        if (d > 0 && t.scrollTop() === 0) {
+            e.preventDefault();
+        }
+        else if (d < 0 && (t.scrollTop() === t.get(0).scrollHeight - t.innerHeight())) {
+            e.preventDefault();
+        }
+    });
+    
+    initUI();
+    beginProgress();
+    
+    ajax('GET', baseUri + 'login/', null, function () {
+        if (!frame) {
+            fetchCurrentConfig(endProgress);
+        }
+    });
+    
+    refreshCameraFrames();
+    checkCameraErrors();
+});
+
diff --git a/motioneye/static/js/ui.js b/motioneye/static/js/ui.js
new file mode 100644 (file)
index 0000000..7dfa9bb
--- /dev/null
@@ -0,0 +1,1100 @@
+
+var _modalDialogContexts = [];
+
+
+    /* UI widgets */
+
+function makeCheckBox($input) {
+    $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');
+        }
+        
+        function setOff() {
+            text.html('OFF');
+            mainDiv.removeClass('on');
+        }
+        
+        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;
+    });
+}
+
+function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decimals, unit) {
+    unit = unit || '';
+
+    $input.each(function () {
+        var $this = $(this);
+        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;
+            }
+            
+            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) {
+            $this.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);
+            }
+        }
+        
+        function bodyMouseUp(e) {
+            bar[0]._mouseDown = false;
+    
+            $('body').unbind('mousemove', bodyMouseMove);
+            $('body').unbind('mouseup', bodyMouseUp);
+            
+            cursorLabel.css('display', 'none');
+            
+            $this.change();
+        }
+        
+        bar.mousedown(function (e) {
+            if (e.which > 1) {
+                return;
+            }
+            
+            this._mouseDown = true;
+            bodyMouseMove(e);
+    
+            $('body').mousemove(bodyMouseMove);
+            $('body').mouseup(bodyMouseUp);
+            
+            slider.focus();
+            cursorLabel.css('display', 'inline-block');
+            
+            return false;
+        });
+        
+        /* ticks */
+        var autoTicks = (ticks == null);
+        
+        function makeTicks() {
+            if (ticksNumber == null) {
+                ticksNumber = 11; 
+            }
+    
+            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});
+                }
+            }
+            
+            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>');
+                
+                labels.append(span);
+                span.css('left', (pos - 10) + '%');
+            }
+            
+            return ticks;
+        }
+        
+        makeTicks();
+    
+        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].setMaxVal = function (mv) {
+            maxVal = mv;
+    
+            makeTicks();
+            
+            input2slider();
+        };
+    });
+}
+
+function makeProgressBar($div) {
+    $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>');
+    
+        $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 (required == null) {
+        required = true;
+    }
+
+    $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;
+            }
+    
+            return true;
+        }
+        
+        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;
+            }
+        }
+        
+        $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 (required == null) {
+        required = true;
+    }
+
+    $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;
+            }
+    
+            return true;
+        }
+        
+        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;
+            }
+        }
+        
+        $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 (minVal == null) {
+        minVal = -Infinity;
+    }
+    if (maxVal == null) {
+        maxVal = Infinity;
+    }
+    if (floating == null) {
+        floating = false;
+    }
+    if (sign == null) {
+        sign = false;
+    }
+    if (required == null) {
+        required = true;
+    }
+
+    $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 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;
+            }
+            else {
+                msg += ' greater than ' + minVal;
+            }
+        }
+        else {
+            if (isFinite(maxVal)) {
+                msg += ' smaller than ' + maxVal;
+            }
+        }
+        
+        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;
+            }
+        }
+        
+        $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) {
+    $input.each(function () {
+        var $this = $(this);
+
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+    
+            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 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;
+            }
+        }
+        
+        $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) {
+    $input.each(function () {
+        var $this = $(this);
+
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+    
+            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 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;
+            }
+        }
+        
+        $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 makeFileValidator($input, required) {
+    if (required == null) {
+        required = true;
+    }
+
+    $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;
+            }
+    
+            return true;
+        }
+        
+        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;
+            }
+        }
+        
+        $this.keyup(validate);
+        $this.blur(validate);
+        $this.change(validate).change();
+        
+        $this.addClass('validator');
+        $this.addClass('file-validator');
+        $this.each(function () {
+            var oldValidate = this.validate;
+            this.validate = function () {
+                if (oldValidate) {
+                    oldValidate.call(this);
+                }
+                validate();
+            }
+        });
+    });
+}
+function makeCustomValidator($input, isValidFunc) {
+    $input.each(function () {
+        var $this = $(this);
+        
+        function isValid(strVal) {
+            if (!$this.is(':visible')) {
+                return true; /* an invisible element is considered always valid */
+            }
+            
+            return isValidFunc(strVal);
+        }
+        
+        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;
+            }
+        }
+    
+        $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();
+            }
+        });
+    });
+}
+
+
+    /* other input value processors */
+
+function makeStrippedInput($input) {
+    $input.change(function () {
+        this.value = $.trim(this.value);
+    });
+}
+
+function makeCharReplacer($input, oldChars, newStr) {
+    $input.change(function () {
+        this.value = this.value.replace(new RegExp('[' + oldChars + ']', 'g'), newStr);
+    });
+}
+
+
+    /* modal dialog */
+
+function showModalDialog(content, onClose, onShow, stack) {
+    var glass = $('div.modal-glass');
+    var container = $('div.modal-container');
+    
+    if (container.is(':animated')) {
+        return setTimeout(function () {
+            showModalDialog(content, onClose, onShow, stack);
+        }, 100);
+    }
+    
+    if (container.is(':visible') && stack) {
+        /* the modal dialog is already visible,
+         * we just replace the content */
+        
+        var children = container.children(':visible');
+        _modalDialogContexts.push({
+            children: children,
+            onClose: container[0]._onClose,
+        });
+        
+        children.css('display', 'none');
+        updateModalDialogPosition();
+        
+        container[0]._onClose = onClose; /* set the new onClose handler */
+        container.append(content);
+        updateModalDialogPosition();
+        
+        if (onShow) {
+            onShow();
+        }
+        
+        return;
+    }
+    
+    glass.css('display', 'block');
+    glass.animate({'opacity': '0.7'}, 200);
+    
+    container[0]._onClose = onClose; /* remember the onClose handler */
+    container.html(content);
+    
+    container.css('display', 'block');
+    updateModalDialogPosition();
+    container.animate({'opacity': '1'}, 200);
+    
+    if (onShow) {
+        onShow();
+    }
+}
+
+function hideModalDialog() {
+    var glass = $('div.modal-glass');
+    var container = $('div.modal-container');
+    
+    if (container.is(':animated')) {
+        return setTimeout(function () {
+            hideModalDialog();
+        }, 100);
+    }
+    
+    if (_modalDialogContexts.length) {
+        if (container[0]._onClose) {
+            container[0]._onClose();
+        }
+        
+        container.children(':visible').remove();
+        
+        var context = _modalDialogContexts.pop();
+        context.children.css('display', '');
+        container[0]._onClose = context.onClose;
+        updateModalDialogPosition();
+        
+        return;
+    }
+    
+    glass.animate({'opacity': '0'}, 200, function () {
+        glass.css('display', 'none');
+    });
+    
+    container.animate({'opacity': '0'}, 200, function () {
+        container.css('display', 'none');
+        container.html('');
+    });
+    
+    /* run the onClose handler, if supplied */
+    if (container[0]._onClose) {
+        container[0]._onClose();
+    }
+}
+
+function updateModalDialogPosition() {
+    var container = $('div.modal-container');
+    if (!container.is(':visible')) {
+        return;
+    }
+    
+    var windowWidth = $(window).width();
+    var windowHeight = $(window).height();
+    var modalWidth, modalHeight, i;
+    
+    /* repeat the operation multiple times, the size might change */
+    for (i = 0; i < 3; i++) {
+        modalWidth = container.outerWidth();
+        modalHeight = container.outerHeight();
+        
+        container.css('left', Math.floor((windowWidth - modalWidth) / 2));
+        container.css('top', Math.floor((windowHeight - modalHeight) / 2));
+    }
+}
+
+function makeModalDialogButtons(buttonsInfo) {
+    /* buttonsInfo is an array of:
+     * * caption: String
+     * * isDefault: Boolean
+     * * click: Function
+     */
+    
+    var buttonsContainer = $('<table class="modal-buttons-container"><tr></tr></table>');
+    var tr = buttonsContainer.find('tr');
+    
+    buttonsInfo.forEach(function (info) {
+        var buttonDiv = $('<div class="button dialog mouse-effect"></div>');
+        
+        buttonDiv.attr('tabIndex', '0'); /* make button focusable */
+        buttonDiv.html(info.caption);
+        
+        if (info.isDefault) {
+            buttonDiv.addClass('default');
+        }
+        
+        if (info.click) {
+            var oldClick = info.click;
+            info.click = function () {
+                if (oldClick() == false) {
+                    return false;
+                }
+                
+                hideModalDialog();
+                
+                return false;
+            };
+        }
+        else {
+            info.click = hideModalDialog; /* every button closes the dialog */
+        }
+        
+        buttonDiv.click(info.click);
+
+        var td = $('<td></td>');
+        td.append(buttonDiv);
+        tr.append(td);
+    });
+    
+    /* limit the size of the buttons container */
+    buttonsContainer.css('max-width', (buttonsInfo.length * 10) + 'em');
+    
+    return buttonsContainer;
+}
+
+function makeModalDialogTitleBar(options) {
+    /* available options:
+     * * title: String
+     * * closeButton: Boolean
+     */
+    
+    var titleBar = $('<div class="modal-title-bar"></div>');
+    
+    var titleSpan = $('<span class="modal-title"></span>');
+    titleSpan.html(options.title || '');
+    if (options.closeButton) {
+        titleSpan.css('margin', '0px 1.5em');
+    }
+    
+    titleBar.append(titleSpan);
+    
+    if (options.closeButton) {
+        var closeButton = $('<div class="button modal-close-button mouse-effect" title="close"></div>');
+        closeButton.click(hideModalDialog);
+        titleBar.append(closeButton);
+    }
+    
+    return titleBar;
+}
+
+function runModalDialog(options) {
+    /* available options:
+     * * title: String
+     * * closeButton: Boolean
+     * * content: any
+     * * buttons: 'ok'|'yesno'|'okcancel'|Array
+     * * onYes: Function
+     * * onNo: Function
+     * * onOk: Function
+     * * onCancel: Function
+     * * onClose: Function
+     * * onShow: Function
+     * * stack: Boolean
+     * * noKeys: Boolean
+     */
+    
+    var content = $('<div></div>');
+    var titleBar = null;
+    var buttonsDiv = null;
+    var defaultClick = null;
+    var cancelClick = null;
+    
+    /* add title bar */
+    if (options.title) {
+        titleBar = makeModalDialogTitleBar({title: options.title, closeButton: options.closeButton});
+        content.append(titleBar);
+    }
+    
+    /* add supplied content */
+    if (options.content) {
+        var contentWrapper = $('<div style="padding: 10px;"></div>');
+        contentWrapper.append(options.content);
+        content.append(contentWrapper);
+    }
+    
+    /* add buttons */
+    if (options.buttons === 'yesno') {
+        options.buttons = [
+            {caption: 'No', click: options.onNo},
+            {caption: 'Yes', isDefault: true, click: options.onYes}
+        ];
+    }
+    if (options.buttons === 'yesnocancel') {
+        options.buttons = [
+            {caption: 'Cancel', isCancel: true, click: options.onCancel},
+            {caption: 'No', click: options.onNo},
+            {caption: 'Yes', isDefault: true, click: options.onYes}
+        ];
+    }
+    else if (options.buttons === 'okcancel') {
+        options.buttons = [
+            {caption: 'Cancel', isCancel:true, click: options.onCancel},
+            {caption: 'OK', isDefault: true, click: options.onOk}
+        ];
+    }
+    else if (options.buttons === 'ok') {
+        options.buttons = [
+            {caption: 'OK', isDefault: true, click: options.onOk}
+        ];
+    }
+    
+    if (options.buttons) {
+        buttonsDiv = makeModalDialogButtons(options.buttons);
+        content.append(buttonsDiv);
+        
+        options.buttons.forEach(function (info) {
+            if (info.isDefault) {
+                defaultClick = info.click;
+            }
+            else if (info.isCancel) {
+                cancelClick = info.click;
+            }
+        });
+    }
+    
+    /* add some margins */
+    if ((buttonsDiv || options.content) && titleBar) {
+        titleBar.css('margin-bottom', '5px');
+    }
+    
+    if (buttonsDiv && options.content) {
+        buttonsDiv.css('margin-top', '5px');
+    }
+    
+    var handleKeyUp = !options.noKeys && function (e) {
+        if (!content.is(':visible')) {
+            return;
+        }
+        
+        switch (e.which) {
+            case 13:
+                if (defaultClick && defaultClick() == false) {
+                    return;
+                }
+                
+                hideModalDialog();
+                
+                break;
+           
+            case 27:
+                if (cancelClick && cancelClick() == false) {
+                    return;
+                }
+
+                hideModalDialog();
+
+                break;
+        }
+    };
+    
+    var onClose = function () {
+        if (options.onClose) {
+            options.onClose();
+        }
+        
+        /* unbind html handlers */
+        
+        $('html').unbind('keyup', handleKeyUp);
+    };
+    
+    /* bind key handlers */
+    $('html').bind('keyup', handleKeyUp);
+    
+    /* and finally, show the dialog */
+
+    showModalDialog(content, onClose, options.onShow, options.stack);
+    
+    /* focus the default button if nothing else is focused */
+    if (content.find('*:focus').length === 0) {
+        content.find('div.button.default').focus();
+    }
+}
+
+
+    /* popup message */
+
+function showPopupMessage(message, type) {
+    var container = $('div.popup-message-container');
+    var content = $('<span class="popup-message"></span>');
+    
+    if (window._popupMessageTimeout) {
+        clearTimeout(window._popupMessageTimeout);
+    }
+    
+    content.html(message);
+    content.addClass(type);
+    container.html(content);
+    
+    var windowWidth = $(window).width();
+    var messageWidth = container.width();
+    
+    container.css('display', 'block');
+    container.css('left', (windowWidth - messageWidth) / 2);
+
+    container.animate({'opacity': '1'}, 200);
+    
+    window._popupMessageTimeout = setTimeout(function () {
+        window._popupMessageTimeout = null;
+        container.animate({'opacity': '0'}, 200, function () {
+            container.css('display', 'none');
+        });
+    }, 5000);
+}
diff --git a/motioneye/static/js/version.js b/motioneye/static/js/version.js
new file mode 100644 (file)
index 0000000..7d43298
--- /dev/null
@@ -0,0 +1,6 @@
+
+$(window).load(function () {
+    if (window.parent && window.parent.postMessage) {
+        window.parent.postMessage({'hostname': hostname, 'version': version, 'url': window.location.href.replace('version/', '')}, '*');
+    }
+});
diff --git a/motioneye/template.py b/motioneye/template.py
new file mode 100644 (file)
index 0000000..49ce299
--- /dev/null
@@ -0,0 +1,66 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+from jinja2 import Environment, FileSystemLoader
+
+import settings
+import utils
+
+
+_jinja_env = None
+
+
+def _init_jinja():
+    global _jinja_env
+    
+    _jinja_env = Environment(
+            loader=FileSystemLoader(settings.TEMPLATE_PATH),
+            trim_blocks=False)
+
+    # globals
+    _jinja_env.globals['settings'] = settings
+    
+    # filters
+    _jinja_env.filters['pretty_date_time'] = utils.pretty_date_time
+    _jinja_env.filters['pretty_date'] = utils.pretty_date
+    _jinja_env.filters['pretty_time'] = utils.pretty_time
+    _jinja_env.filters['pretty_duration'] = utils.pretty_duration
+
+
+def add_template_path(path):
+    global _jinja_env
+    if _jinja_env is None:
+        _init_jinja()
+    
+    _jinja_env.loader.searchpath.append(path)
+
+
+def add_context(name, value):
+    global _jinja_env
+    if _jinja_env is None:
+        _init_jinja()
+    
+    _jinja_env.globals[name] = value
+
+
+def render(template_name, **context):
+    global _jinja_env
+    if _jinja_env is None:
+        _init_jinja()
+    
+    template = _jinja_env.get_template(template_name)
+    return template.render(**context)
diff --git a/motioneye/templates/base.html b/motioneye/templates/base.html
new file mode 100644 (file)
index 0000000..6679b8d
--- /dev/null
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        {% block meta %}
+            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+            <meta http-equiv="X-UA-Compatible" content="IE=Edge">
+            <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+            <meta name="apple-mobile-web-app-capable" content="yes">
+            <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+        {% endblock %}
+        <title>{% block title %}{% endblock %}</title>
+        {% block style %}
+            <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/jquery.timepicker.css" />
+            <link rel="shortcut icon" href="/static/favicon.ico" />
+            <link rel="apple-touch-icon" href="/static/favicon.ico" />
+        {% endblock %}
+        {% block script %}
+            <script type="text/javascript" src="{{STATIC_URL}}js/css-browser-selector.js"></script>
+            <script type="text/javascript" src="{{STATIC_URL}}js/jquery.min.js"></script>
+            <script type="text/javascript" src="{{STATIC_URL}}js/jquery.timepicker.min.js"></script>
+            <script type="text/javascript" src="{{STATIC_URL}}js/jquery.mousewheel.js"></script>
+            
+            <script type="text/javascript">
+                var staticUrl = '{{STATIC_URL}}';
+            </script>
+        {% endblock %}
+    </head>
+    
+    <body>
+        {% block body %}{% endblock %}
+    </body>
+</html>
diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html
new file mode 100644 (file)
index 0000000..199e08f
--- /dev/null
@@ -0,0 +1,826 @@
+{% extends "base.html" %}
+
+{% macro config_item(config) -%}
+    <tr class="settings-item additional-config {% if config['advanced'] %}advanced-setting{% endif %}"
+            {% if config.get('reboot') and enable_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['max']}}"{% 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 %}
+            {% if config.get('validate') %}validate="{{config['validate']}}"{% endif %}>
+        {% if config['type'] == 'separator' %}
+        <td colspan="100"><div class="settings-item-separator"></div></td>
+        {% else %}
+        <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>
+            {% elif config['type'] == 'html' %}
+                <div class="html styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Html">
+            {% endif %}
+        </td>
+        <td>{% if config.get('description') %}<span class="help-mark" title="{{config['description']}}">?</span>{% endif %}</td>
+        {% endif %}
+    </tr>
+{%- endmacro %}
+
+{% block title %}{% if title %}{{title}}{% else %}{{hostname}}{% endif %}{% endblock %}
+
+{% block style %}
+    {{super()}}
+    <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/ui.css" />
+    <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/main.css" />
+    {% if frame %}
+        <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/frame.css" />
+    {% endif %}
+{% endblock %}
+
+{% block script %}
+    {{super()}}
+    <script type="text/javascript" src="{{STATIC_URL}}js/ui.js"></script>
+    <script type="text/javascript" src="{{STATIC_URL}}js/main.js"></script>
+    {% if frame %}
+        <script type="text/javascript" src="{{STATIC_URL}}js/frame.js"></script>
+    {% endif %}
+    <script type="text/javascript">
+        var adminUsername = '{{admin_username}}';
+        var frame = {% if frame %}true{% else %}false{% endif %};
+        var baseUri = null;
+    </script>
+{% endblock %}
+
+{% block body %}
+    {% if not frame %}
+    <div class="header">
+        <div class="header-container">
+            <div class="settings-top-bar closed">
+                <div class="button settings-button mouse-effect" title="settings"></div>
+                <div class="button logout-button mouse-effect" title="switch user"></div>
+                <select class="styled" id="cameraSelect" {% if not add_remove_cameras %}style="display: none;"{% endif %}></select>
+                <div class="button rem-camera-button mouse-effect" id="remCameraButton" title="remove camera" {% if not add_remove_cameras %}style="display: none;"{% endif %}></div>
+                <div class="button apply-button" id="applyButton">Apply</div>
+                {% if hostname %}<div class="hostname">{{hostname}}</div>{% endif %}
+            </div>
+            <div class="button logout-button mouse-effect" title="switch user"></div>
+            <div class="logo">
+                <a href="/">
+                    <span class="logo">motionEye</span>
+                    <img class="logo" src="{{STATIC_URL}}img/motioneye-logo.svg">
+                </a>
+            </div>
+        </div>
+    </div>
+    {% endif %}
+    {% if not frame %}
+    <div class="page">
+        <div class="settings closed">
+            <div class="settings-container">
+                <div class="settings-section-title">
+                    <input type="checkbox" class="styled section general main-config" id="motionEyeSwitch">
+                    <span class="help-mark" title="general settings, not related to any camera">?</span>
+                    <a class="settings-section-title">General Settings</a>
+                    <span class="minimize open"></span>
+                </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 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" 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 main-config" id="adminUsernameEntry" readonly="readonly"></td>
+                        <td><span class="help-mark" title="the username to be used for configuring the system">?</span></td>
+                    </tr>
+                    <tr class="settings-item" strip="true" {% if enable_reboot %}reboot="true"{% endif %}>
+                        <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 main-config" id="adminPasswordEntry"></td>
+                        <td><span class="help-mark" title="administrator's password">?</span></td>
+                    </tr>
+                    <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 main-config" id="normalUsernameEntry"></td>
+                        <td><span class="help-mark" title="the username to be used for video surveillance">?</span></td>
+                    </tr>
+                    <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 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>
+                    {% 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 class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label">Current Version</span></td>
+                        <td class="settings-item-value"><span class="settings-item-label">{{version}}</span></td>
+                    </tr>
+                    {% if enable_update %}
+                    <tr class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label">Software Update</span></td>
+                        <td class="settings-item-value"><div class="button normal-button update-button" id="updateButton">Check</div></td>
+                        <td><span class="help-mark" title="checks for new versions and performs updates">?</span></td>
+                    </tr>
+                    {% endif %}
+                    <tr class="settings-item advanced-setting{% if not enable_reboot %} hidden{% endif %}">
+                        <td class="settings-item-label"><span class="settings-item-label">Power</span></td>
+                        <td class="settings-item-value"><div class="button normal-button shut-down-button" id="shutDownButton">Shut Down</div></td>
+                        <td><span class="help-mark" title="shuts down the system">?</span></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting{% if not enable_reboot %} hidden{% endif %}">
+                        <td class="settings-item-label"><span class="settings-item-label"></span></td>
+                        <td class="settings-item-value"><div class="button normal-button reboot-button" id="rebootButton">Reboot</div></td>
+                        <td><span class="help-mark" title="reboots the system">?</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">
+                        <td class="settings-item-label"><span class="settings-item-label">Configuration</span></td>
+                        <td class="settings-item-value"><div class="button normal-button backup-button" id="backupButton">Backup</div></td>
+                        <td><span class="help-mark" title="creates a file with the current configuration for you to save it locally">?</span></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label"></span></td>
+                        <td class="settings-item-value"><div class="button normal-button restore-button" id="restoreButton">Restore</div></td>
+                        <td><span class="help-mark" title="restores the configuration from a previously saved backup file">?</span></td>
+                    </tr>
+                </table>
+
+                {% 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 %}">
+                    {% if section.get('onoff') %}<input type="checkbox" class="styled section additional-section {{section['name']}} main-config" id="{{section['name']}}Switch">{% endif %}
+                    {% if section.get('description') %}<span class="help-mark" title="{{section['description']}}">?</span>{% endif %}
+                    <a class="settings-section-title">{{section['label']}}</a>
+                    <span class="minimize {% if section.get('open') %}open{% endif %}"></span>
+                </div>
+                <table class="settings {% if section.get('advanced') %}advanced-setting{% endif %}">
+                    {% for config in section['configs'] %}
+                        {{config_item(config)}}
+                    {% endfor %}
+                </table>
+                {% endif %}                    
+                {% endfor %}
+
+                <tr class="settings-item advanced-setting">
+                    <td colspan="100"><div class="settings-item-separator" style="margin: 0px 0px 1.5em 0px;"></div></td>
+                </tr>
+                
+                <div class="settings-section-title">
+                    <input type="checkbox" class="styled section device camera-config" id="videoDeviceSwitch">
+                    <span class="help-mark" title="enable this if you want to use this camera device">?</span>
+                    <a class="settings-section-title">Video Device</a>
+                    <span class="minimize open"></span>
+                </div>
+                <table class="settings">
+                    <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 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 camera-config" id="deviceUriEntry" readonly="readonly"></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label">Camera Type</span></td>
+                        <td class="settings-item-value"><input type="text" class="styled device camera-config" id="deviceTypeEntry" readonly="readonly"></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 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 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" 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 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" 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 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" 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 camera-config" id="saturationSlider"></td>
+                        <td><span class="help-mark" title="sets a desired saturation level for this camera">?</span></td>
+                    </tr>
+                    <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 camera-config" id="hueSlider"></td>
+                        <td><span class="help-mark" title="sets a desired hue for this camera">?</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">
+                        <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 camera-config" id="resolutionSelect">
+                            </select>
+                        </td>
+                        <td><span class="help-mark" title="the video resolution (larger values produce better quality but require more CPU power, larger storage space and bandwidth)">?</span></td>
+                    </tr>
+                    <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 camera-config" id="rotationSelect">
+                                <option value="0">0&deg;</option>
+                                <option value="90">90&deg;</option>
+                                <option value="180">180&deg;</option>
+                                <option value="270">270&deg;</option>
+                            </select>
+                        </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" 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 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 more CPU power, larger storage space and bandwidth)">?</span></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label">Extra Motion Options</span></td>
+                        <td class="settings-item-value"><textarea class="styled device camera-config" id="extraOptionsEntry" rows="3"></textarea></td>
+                        <td><span class="help-mark" title="you can add here any extra options for the motion daemon (use the &quot;name value&quot; format, one option per line)">?</span></td>
+                    </tr>
+                    {% for config in camera_sections.get('device', {}).get('configs', []) %}
+                        {{config_item(config)}}
+                    {% endfor %}
+                </table>
+                
+                <div class="settings-section-title advanced-setting">
+                    <span class="help-mark" title="choose where and how your media files are saved">?</span>
+                    <a class="settings-section-title">File Storage</a>
+                    <span class="minimize"></span>
+                </div>
+                <table class="settings advanced-setting">
+                    <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 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" 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 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" 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 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" 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 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" 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 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" 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 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">
+                        <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">Disk Usage</span></td>
+                        <td class="settings-item-value">
+                            <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 camera-config" id="textOverlaySwitch">
+                    <span class="help-mark" title="choose what information is displayed on the captured frames">?</span>
+                    <a class="settings-section-title">Text Overlay</a>
+                    <span class="minimize"></span>
+                </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 camera-config" id="leftTextSelect">
+                                <option value="camera-name">Camera Name</option>
+                                <option value="timestamp">Timestamp</option>
+                                <option value="custom-text">Custom Text</option>
+                                <option value="disabled">Disabled</option>
+                            </select>
+                        </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" required="true" strip="true">
+                        <td class="settings-item-label"></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 camera-config" id="rightTextSelect">
+                                <option value="camera-name">Camera Name</option>
+                                <option value="timestamp">Timestamp</option>
+                                <option value="custom-text">Custom Text</option>
+                                <option value="disabled">Disabled</option>
+                            </select>
+                        </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" required="true" strip="true">
+                        <td class="settings-item-label"></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" minimize-switch-independent="true">
+                    <input type="checkbox" class="styled section streaming camera-config" id="videoStreamingSwitch">
+                    <span class="help-mark" title="enable this if you want video streaming for this camera">?</span>
+                    <a class="settings-section-title">Video Streaming</a>
+                    <span class="minimize"></span>
+                </div>
+                <table class="settings">
+                    <tr class="settings-item advanced-setting localhost-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 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 localhost-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 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 localhost-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 camera-config" id="streamingServerResizeSwitch"></td>
+                        <td><span class="help-mark" title="when this is enabled, the images are resized before they are sent to the browser (disable when running on a slow CPU)">?</span></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting localhost-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 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" 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 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>
+                    {% if not old_motion %}
+                    <tr class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label">Authentication Mode</span></td>
+                        <td class="settings-item-value">
+                            <select class="styled streaming camera-config" id="streamingAuthModeSelect">
+                                <option value="disabled">Disabled</option>
+                                <option value="basic">Basic</option>
+                                <option value="digest">Digest</option>
+                            </select>
+                        </td>
+                        <td><span class="help-mark" title="the authentication mode to use when accessing the stream (use Basic instead of Digest if you encounter issues with third party apps)">?</span></td>
+                    </tr>
+                    {% endif %}
+                    <tr class="settings-item advanced-setting localhost-streaming">
+                        <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 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">
+                        <td colspan="100"><div class="settings-item-separator"></div></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting localhost-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 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 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 localhost-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 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 camera-config" id="stillImagesSwitch">
+                    <span class="help-mark" title="enable this if you want to capture still images (pictures)">?</span>
+                    <a class="settings-section-title">Still Images</a>
+                    <span class="minimize"></span>
+                </div>
+                <table class="settings">
+                    <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 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" 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 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 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>
+                            </select>
+                        </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" 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 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 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>
+                                <option value="365">For One Year</option>
+                                <option value="0">Forever</option>
+                                <option value="-1">Custom</option>
+                            </select>
+                        </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" 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 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 camera-config" id="motionDetectionSwitch">
+                    <span class="help-mark" title="enable this to use and configure the motion detection mechanism">?</span>
+                    <a class="settings-section-title">Motion Detection</a>
+                    <span class="minimize"></span>
+                </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 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" 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 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 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" 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 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" 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 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" 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 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" 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 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" 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 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 camera-config" id="motionMoviesSwitch">
+                    <span class="help-mark" title="enable this if you want to record motion movies">?</span>
+                    <a class="settings-section-title">Motion Movies</a>
+                    <span class="minimize"></span>
+                </div>
+                <table class="settings">
+                    <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 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" 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 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 advanced-setting" min="0" max="86400" required="true">
+                        <td class="settings-item-label"><span class="settings-item-label">Maximum Movie Length</span></td>
+                        <td class="settings-item-value"><input type="text" class="number styled motion-movies camera-config" id="maxMovieLengthEntry"><span class="settings-item-unit">seconds</span></td>
+                        <td><span class="help-mark" title="sets the maximum length of motion movies, in seconds; if the motion event lasts longer, a new movie file is created; use 0 for infinite length">?</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 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>
+                                <option value="365">For One Year</option>
+                                <option value="0">Forever</option>
+                                <option value="-1">Custom</option>
+                            </select>
+                        </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" 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 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">
+                    <span class="help-mark" title="enable this if you want to be notified when motion is detected">?</span>
+                    <a class="settings-section-title">Motion Notifications</a>
+                    <span class="minimize"></span>
+                </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 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" 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 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" 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 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" 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 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" 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 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" 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 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 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" 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 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); set to 0 to disable picture attachments">?</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">
+                        <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 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" 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 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 camera-config" id="webHookHttpMethodSelect">
+                                <option value="GET">GET</option>
+                                <option value="POST">POST</option>
+                            </select>
+                        </td>
+                        <td><span class="help-mark" title="the HTTP method to use when requesting the web hook URL">?</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">
+                        <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 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" 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 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 camera-config" id="workingScheduleSwitch">
+                    <span class="help-mark" title="enable this if you want to define a weekly working schedule for motion detection">?</span>
+                    <a class="settings-section-title">Working Schedule</a>
+                    <span class="minimize"></span>
+                </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 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 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 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 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 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 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 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 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 %}">
+                    {% if section.get('onoff') %}<input type="checkbox" class="styled section additional-section {{section['name']}} camera-config" id="{{section['name']}}Switch">{% endif %}
+                    {% if section.get('description') %}<span class="help-mark" title="{{section['description']}}">?</span>{% endif %}
+                    <a class="settings-section-title">{{section['label']}}</a>
+                    <span class="minimize {% if section.get('open') %}open{% endif %}"></span>
+                </div>
+                <table class="settings">
+                    {% for config in section['configs'] %}
+                        {{config_item(config)}}
+                    {% endfor %}
+                </table>
+                {% endif %}                    
+                {% endfor %}
+
+                <div class="settings-progress"></div>
+            </div>
+        </div>
+        <img class="background-logo" src="{{STATIC_URL}}img/motioneye-logo.svg" onmousedown="return false;">
+        <div class="page-container"></div>
+        <div class="footer">
+            <div class="copyright-note">copyright &copy; Calin Crisan</div>
+        </div> 
+    </div>
+    {% else %}
+        <div class="camera-frame" id="camera{{camera_id}}"
+                streaming_framerate="{{camera_config['stream_maxrate']}}" streaming_server_resize="{{camera_config['@webcam_server_resize']}}"
+                proto="{{camera_config['@proto']}}" url="{{camera_config['@url']}}">
+            
+            <div class="camera-container">
+                <div class="camera-placeholder"><img class="no-camera" src="{{STATIC_URL}}img/no-camera.svg"></div><img
+                    class="camera"><div class="camera-progress"><img class="camera-progress"></div>
+            </div>
+        </div>
+    {% endif %}
+    <div class="modal-glass"></div>
+    <div class="modal-container"></div>
+    <div class="popup-message-container"></div>
+{% endblock %}
diff --git a/motioneye/templates/version.html b/motioneye/templates/version.html
new file mode 100644 (file)
index 0000000..3c7a8f8
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block script %}
+    {{super()}}
+    <script type="text/javascript">
+        var hostname = '{{hostname}}';
+        var version = '{{version}}';
+    </script>
+    <script type="text/javascript" src="{{STATIC_URL}}js/version.js"></script>
+{% endblock %}
+
+{% block body %}
+hostname = "{{hostname}}"<br>
+version = "{{version}}"
+{% endblock %}
diff --git a/motioneye/thumbnailer.py b/motioneye/thumbnailer.py
new file mode 100644 (file)
index 0000000..c702863
--- /dev/null
@@ -0,0 +1,86 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import logging
+import multiprocessing
+import os
+import signal
+import tornado
+
+import cleanup
+import mediafiles
+import settings
+
+
+_process = None
+
+
+def start():
+    # schedule the first call a bit later to improve performance at startup
+    ioloop = tornado.ioloop.IOLoop.instance()
+    ioloop.add_timeout(datetime.timedelta(seconds=30), _run_process)
+
+
+def stop():
+    global _process
+    
+    if not running():
+        _process = None
+        return
+    
+    if _process.is_alive():
+        _process.join(timeout=10)
+    
+    if _process.is_alive():
+        logging.error('thumbnailer process did not finish in time, killing it...')
+        os.kill(_process.pid, signal.SIGKILL)
+
+    _process = None
+
+
+def running():
+    return _process is not None and _process.is_alive()
+
+
+def _run_process():
+    global _process
+    
+    # schedule the next call
+    ioloop = tornado.ioloop.IOLoop.instance()
+    ioloop.add_timeout(datetime.timedelta(seconds=settings.THUMBNAILER_INTERVAL), _run_process)
+
+    if not running() and not cleanup.running(): # check that the previous process has finished and that cleanup is not running
+        logging.debug('running thumbnailer process...')
+
+        _process = multiprocessing.Process(target=_do_next_movie_thumbail)
+        _process.start()
+
+
+def _do_next_movie_thumbail():
+    # this will be executed in a separate subprocess
+    
+    # ignore the terminate and interrupt signals in this subprocess
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+    signal.signal(signal.SIGTERM, signal.SIG_IGN)
+    
+    try:
+        mediafiles.make_next_movie_preview()
+         
+    except Exception as e:
+        logging.error('failed to make movie thumbnail: %(msg)s' % {
+                'msg': unicode(e)}, exc_info=True)
diff --git a/motioneye/tzctl.py b/motioneye/tzctl.py
new file mode 100644 (file)
index 0000000..ff36ab7
--- /dev/null
@@ -0,0 +1,139 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import hashlib
+import logging
+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
+    if not file:
+        return None
+    
+    for i in xrange(8): # recursively follow the symlinks @UnusedVariable
+        try:
+            file = os.readlink(file)
+
+        except OSError:
+            break
+    
+    if file and file.startswith('/usr/share/zoneinfo/'):
+        file = file[20:]
+    
+    else:
+        file = None
+
+    time_zone = file or None
+    if time_zone:
+        logging.debug('found time zone by symlink method: %s' % time_zone)
+    
+    return time_zone
+
+
+def _get_time_zone_md5():
+    if settings.LOCAL_TIME_FILE:
+        return None
+
+    try:
+        output = subprocess.check_output('cd /usr/share/zoneinfo; find * -type f | xargs md5sum', shell=True)
+
+    except Exception as e:
+        logging.error('getting md5 of zoneinfo files failed: %s' % e)
+
+        return None
+    
+    lines = [l for l in output.split('\n') if l]
+    lines = [l.split(None, 1) for l in lines]
+    time_zone_by_md5 = dict(lines)
+
+    try:
+        with open(settings.LOCAL_TIME_FILE, 'r') as f:
+            data = f.read()
+    
+    except Exception as e:
+        logging.error('failed to read local time file: %s' % e)
+        
+        return None
+
+    md5 = hashlib.md5(data).hexdigest()
+    time_zone = time_zone_by_md5.get(md5)
+    
+    if time_zone:
+        logging.debug('found time zone by md5 method: %s' % time_zone)
+    
+    return time_zone
+
+
+def _get_time_zone():
+    return _get_time_zone_symlink() or _get_time_zone_md5() or 'UTC'
+
+
+def _set_time_zone(time_zone):
+    time_zone = time_zone or 'UTC'
+
+    zoneinfo_file = '/usr/share/zoneinfo/' + time_zone
+    if not os.path.exists(zoneinfo_file):
+        logging.error('%s file does not exist' % zoneinfo_file)
+        
+        return False
+
+    logging.debug('linking "%s" to "%s"' % (settings.LOCAL_TIME_FILE, zoneinfo_file))
+
+    try:
+        os.remove(settings.LOCAL_TIME_FILE)
+    
+    except:
+        pass # nevermind
+    
+    try:
+        os.symlink(zoneinfo_file, settings.LOCAL_TIME_FILE)
+        
+        return True
+    
+    except Exception as e:
+        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,
+        'reboot': True,
+        'get': _get_time_zone,
+        'set': _set_time_zone
+    }
diff --git a/motioneye/update.py b/motioneye/update.py
new file mode 100644 (file)
index 0000000..0c53b25
--- /dev/null
@@ -0,0 +1,67 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import logging
+import re
+
+
+# versions
+
+def get_version():
+    import motioneye
+    
+    return motioneye.VERSION
+
+
+def get_all_versions():
+    return []
+
+
+def compare_versions(version1, version2):
+    version1 = re.sub('[^0-9.]', '', version1)
+    version2 = re.sub('[^0-9.]', '', version2)
+    
+    version1 = [int(n) for n in version1.split('.')]
+    version2 = [int(n) for n in version2.split('.')]
+    
+    len1 = len(version1)
+    len2 = len(version2)
+    length = min(len1, len2)
+    for i in xrange(length):
+        p1 = version1[i]
+        p2 = version2[i]
+        
+        if p1 < p2:
+            return -1
+        
+        elif p1 > p2:
+            return 1
+    
+    if len1 < len2:
+        return -1
+    
+    elif len1 > len2:
+        return 1
+    
+    else:
+        return 0
+
+
+def perform_update(version):
+    logging.error('updating is not implemented')
+    
+    return False
diff --git a/motioneye/utils.py b/motioneye/utils.py
new file mode 100644 (file)
index 0000000..a3fe0b8
--- /dev/null
@@ -0,0 +1,681 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import base64
+import datetime
+import functools
+import hashlib
+import logging
+import os
+import re
+import socket
+import time
+import urllib
+import urlparse
+
+from tornado.httpclient import AsyncHTTPClient, HTTPRequest
+from tornado.iostream import IOStream
+from tornado.ioloop import IOLoop
+
+import settings
+
+
+try:
+    from collections import OrderedDict  # @UnusedImport
+
+except:
+    from ordereddict import OrderedDict  # @UnusedImport @Reimport
+
+
+_SIGNATURE_REGEX = re.compile('[^a-zA-Z0-9/?_.=&{}\[\]":, _-]')
+
+
+COMMON_RESOLUTIONS = [
+    (320, 240),
+    (640, 480),
+    (800, 480),
+    (1024, 576),
+    (1024, 768),
+    (1280, 720),
+    (1280, 800),
+    (1280, 960),
+    (1280, 1024),
+    (1440, 960),
+    (1440, 1024),
+    (1600, 1200)
+]
+
+
+def pretty_date_time(date_time, tzinfo=None, short=False):
+    if date_time is None:
+        return '('+  _('never') + ')'
+
+    if isinstance(date_time, int):
+        return pretty_date_time(datetime.datetime.fromtimestamp(date_time))
+
+    if short:
+        text = u'{day} {month}, {hm}'.format(
+            day=date_time.day,
+            month=date_time.strftime('%b'),
+            hm=date_time.strftime('%H:%M')
+        )
+    
+    else:
+        text = u'{day} {month} {year}, {hm}'.format(
+            day=date_time.day,
+            month=date_time.strftime('%B'),
+            year=date_time.year,
+            hm=date_time.strftime('%H:%M')
+        )
+    
+    if tzinfo:
+        offset = tzinfo.utcoffset(datetime.datetime.utcnow()).seconds
+        tz = 'GMT'
+        if offset >= 0:
+            tz += '+'
+
+        else:
+            tz += '-'
+            offset = -offset
+
+        tz += '%.2d' % (offset / 3600) + ':%.2d' % ((offset % 3600) / 60)
+
+        text += ' (' + tz + ')'
+
+    return text
+
+
+def pretty_date(date):
+    if date is None:
+        return '('+  _('never') + ')'
+
+    if isinstance(date, int):
+        return pretty_date(datetime.datetime.fromtimestamp(date))
+
+    return u'{day} {month} {year}'.format(
+        day=date.day,
+        month=_(date.strftime('%B')),
+        year=date.year
+    )
+
+
+def pretty_time(time):
+    if time is None:
+        return ''
+
+    if isinstance(time, datetime.timedelta):
+        hour = time.seconds / 3600
+        minute = (time.seconds % 3600) / 60
+        time = datetime.time(hour, minute)
+
+    return '{hm}'.format(
+        hm=time.strftime('%H:%M')
+    )
+
+
+def pretty_duration(duration):
+    if duration is None:
+        duration = 0
+
+    if isinstance(duration, datetime.timedelta):
+        duration = duration.seconds + duration.days * 86400
+
+    if duration < 0:
+        negative = True
+        duration = -duration
+
+    else:
+        negative = False
+
+    days = int(duration / 86400)
+    duration %= 86400
+    hours = int(duration / 3600)
+    duration %= 3600
+    minutes = int(duration / 60)
+    duration %= 60
+    seconds = duration
+
+    # treat special cases
+    special_result = None
+    if days != 0 and hours == 0 and minutes == 0 and seconds == 0:
+        if days == 1:
+            special_result = str(days) + ' ' + _('day')
+
+        elif days == 7:
+            special_result = '1 ' + _('week')
+
+        elif days in [30, 31, 32]:
+            special_result = '1 ' + _('month')
+
+        elif days in [365, 366]:
+            special_result = '1 ' + _('year')
+
+        else:
+            special_result = str(days) + ' ' + _('days')
+
+    elif days == 0 and hours != 0 and minutes == 0 and seconds == 0:
+        if hours == 1:
+            special_result = str(hours) + ' ' + _('hour')
+
+        else:
+            special_result = str(hours) + ' ' + _('hours')
+
+    elif days == 0 and hours == 0 and minutes != 0 and seconds == 0:
+        if minutes == 1:
+            special_result = str(minutes) + ' ' + _('minute')
+
+        else:
+            special_result = str(minutes) + ' ' + _('minutes')
+
+    elif days == 0 and hours == 0 and minutes == 0 and seconds != 0:
+        if seconds == 1:
+            special_result = str(seconds) + ' ' + _('second')
+
+        else:
+            special_result = str(seconds) + ' ' + _('seconds')
+
+    elif days == 0 and hours == 0 and minutes == 0 and seconds == 0:
+        special_result = str(0) + ' ' + _('seconds')
+
+    if special_result:
+        if negative:
+            special_result = _('minus') + ' ' + special_result
+
+        return special_result
+
+    if days:
+        format = "{d}d{h}h{m}m"
+
+    elif hours:
+        format = "{h}h{m}m"
+
+    elif minutes:
+        format = "{m}m"
+        if seconds:
+            format += "{s}s"
+
+    else:
+        format = "{s}s"
+
+    if negative:
+        format = '-' + format
+
+    return format.format(d=days, h=hours, m=minutes, s=seconds)
+
+
+def pretty_size(size):
+    if size < 1024: # less than 1kB
+        size, unit = size, 'B'
+    
+    elif size < 1024 * 1024: # less than 1MB
+        size, unit = size / 1024.0, 'kB'
+        
+    elif size < 1024 * 1024 * 1024: # less than 1GB
+        size, unit = size / 1024.0 / 1024, 'MB'
+    
+    else: # greater than or equal to 1GB
+        size, unit = size / 1024.0 / 1024 / 1024, 'GB'
+    
+    return '%.1f %s' % (size, unit)
+
+
+def pretty_http_error(response):
+    if response.code == 401 or response.error == 'Authentication Error':
+        return 'authentication failed'
+
+    if not response.error:
+        return 'ok'
+    
+    msg = unicode(response.error)
+    if msg.startswith('HTTP '):
+        msg = msg.split(':', 1)[-1].strip()
+
+    if msg.startswith('[Errno '):
+        msg = msg.split(']', 1)[-1].strip()
+    
+    if 'timeout' in msg.lower() or 'timed out' in msg.lower():
+        msg = 'request timed out' 
+
+    return msg
+
+
+def make_str(s):
+    if isinstance(s, str):
+        return s
+
+    try:
+        return str(s)
+
+    except:
+        try:
+            return unicode(s, encoding='utf8').encode('utf8')
+    
+        except:
+            return unicode(s).encode('utf8')
+
+
+def make_unicode(s):
+    if isinstance(s, unicode):
+        return s
+
+    try:
+        return unicode(s, encoding='utf8')
+    
+    except:
+        try:
+            return unicode(s)
+        
+        except:
+            return str(s).decode('utf8')
+
+
+def get_disk_usage(path):
+    logging.debug('getting disk usage for path %(path)s...' % {
+            'path': path})
+
+    try:    
+        result = os.statvfs(path)
+    
+    except OSError as e:
+        logging.error('failed to execute statvfs: %(msg)s' % {'msg': unicode(e)})
+        
+        return None
+
+    block_size = result.f_frsize
+    free_blocks = result.f_bfree
+    total_blocks = result.f_blocks
+    
+    free_size = free_blocks * block_size
+    total_size = total_blocks * block_size
+    used_size = total_size - free_size
+    
+    return (used_size, total_size)
+
+
+def local_motion_camera(config):
+    '''Tells if a camera is managed by the local motion instance.'''
+    return bool(config.get('videodevice') or config.get('netcam_url'))
+
+
+def remote_camera(config):
+    '''Tells if a camera is managed by a remote motionEye server.'''
+    return config.get('@proto') == 'motioneye'
+
+
+def v4l2_camera(config):
+    '''Tells if a camera is a v4l2 device managed by the local motion instance.'''
+    return bool(config.get('videodevice'))
+
+
+def net_camera(config):
+    '''Tells if a camera is a network camera managed by the local motion instance.'''
+    return bool(config.get('netcam_url'))
+
+
+def simple_mjpeg_camera(config):
+    '''Tells if a camera is a simple MJPEG camera not managed by any motion instance.'''
+    return bool(config.get('@proto') == 'mjpeg')
+
+
+def test_mjpeg_url(data, auth_modes, allow_jpeg, callback):
+    data = dict(data)
+    data.setdefault('scheme', 'http')
+    data.setdefault('host', '127.0.0.1')
+    data.setdefault('port', '80')
+    data.setdefault('uri', '')
+    data.setdefault('username', None)
+    data.setdefault('password', None)
+
+    url = '%(scheme)s://%(host)s%(port)s%(uri)s' % {
+            'scheme': data['scheme'],
+            'host': data['host'],
+            'port': ':' + str(data['port']) if data['port'] else '',
+            'uri': data['uri'] or ''}
+    
+    called = [False]
+    status_2xx = [False]
+    http_11 = [False]
+
+    def do_request(on_response):
+        if data['username']:
+            auth = auth_modes[0]
+            
+        else:
+            auth = 'no'
+
+        logging.debug('testing (m)jpg netcam at %s using %s authentication' % (url, auth))
+
+        request = HTTPRequest(url, auth_username=username, auth_password=password, auth_mode=auth_modes.pop(0),
+                connect_timeout=settings.REMOTE_REQUEST_TIMEOUT, request_timeout=settings.REMOTE_REQUEST_TIMEOUT,
+                header_callback=on_header)
+
+        http_client = AsyncHTTPClient(force_instance=True)    
+        http_client.fetch(request, on_response)
+
+    def on_header(header):
+        header = header.lower()
+        if header.startswith('content-type') and status_2xx[0]:
+            content_type = header.split(':')[1].strip()
+            called[0] = True
+
+            if content_type in ['image/jpg', 'image/jpeg', 'image/pjpg'] and allow_jpeg:
+                callback([{'id': 1, 'name': 'JPEG Network Camera', 'keep_alive': http_11[0]}])
+            
+            elif content_type.startswith('multipart/x-mixed-replace'):
+                callback([{'id': 1, 'name': 'MJPEG Network Camera', 'keep_alive': http_11[0]}])
+            
+            else:
+                callback(error='not a supported network camera')
+
+        else:
+            # check for the status header
+            m = re.match('^http/1.(\d) (\d+) ', header)
+            if m:
+                if int(m.group(2)) / 100 == 2:
+                    status_2xx[0] = True
+                
+                if m.group(1) == '1':
+                    http_11[0] = True
+
+    def on_response(response):
+        if not called[0]:
+            if response.code == 401 and auth_modes and data['username']:
+                status_2xx[0] = False
+                do_request(on_response)
+                
+            else:
+                called[0] = True
+                callback(error=pretty_http_error(response) if response.error else 'not a supported network camera')
+    
+    username = data['username'] or None
+    password = data['password'] or None
+    
+    do_request(on_response)
+
+
+def test_rtsp_url(data, callback):
+    import config
+    
+    data = dict(data)
+    data.setdefault('scheme', 'rtsp')
+    data.setdefault('host', '127.0.0.1')
+    data['port'] = data.get('port') or '554'
+    data.setdefault('uri', '')
+    data.setdefault('username', None)
+    data.setdefault('password', None)
+
+    url = '%(scheme)s://%(host)s%(port)s%(uri)s' % {
+            'scheme': data['scheme'],
+            'host': data['host'],
+            'port': ':' + str(data['port']) if data['port'] else '',
+            'uri': data['uri'] or ''}
+    
+    called = [False]
+    timeout = [None]
+    stream = None
+
+    def connect():
+        logging.debug('testing rtsp netcam at %s' % url)
+
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+        s.settimeout(settings.MJPG_CLIENT_TIMEOUT)
+        stream = IOStream(s)
+        stream.set_close_callback(on_close)
+        stream.connect((data['host'], int(data['port'])), on_connect)
+        
+        timeout[0] = IOLoop.instance().add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT),
+                functools.partial(on_connect, _timeout=True))
+        
+        return stream
+    
+    def on_connect(_timeout=False):
+        IOLoop.instance().remove_timeout(timeout[0])
+        
+        if _timeout:
+            return handle_error('timeout connecting to rtsp netcam')
+
+        if not stream:
+            return handle_error('failed to connect to rtsp netcam') 
+
+        logging.debug('connected to rtsp netcam')
+        
+        stream.write('\r\n'.join([
+            'OPTIONS %s RTSP/1.0' % url.encode('utf8'),
+            'CSeq: 1',
+            'User-Agent: motionEye',
+            '',
+            ''
+        ]))
+
+        seek_rtsp()
+        
+    def seek_rtsp():
+        if check_error():
+            return
+
+        stream.read_until_regex('RTSP/1.0 \d+ ', on_rtsp)
+        timeout[0] = IOLoop.instance().add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT), on_rtsp)
+
+    def on_rtsp(data):
+        IOLoop.instance().remove_timeout(timeout[0])
+
+        if data:
+            if data.endswith('200 '):
+                seek_server()
+    
+            else:
+                handle_error('rtsp netcam returned erroneous response: %s' % data)
+        
+        else:
+            handle_error('timeout waiting for rtsp netcam response')
+
+    def seek_server():
+        if check_error():
+            return
+
+        stream.read_until_regex('Server: .*', on_server)
+        timeout[0] = IOLoop.instance().add_timeout(datetime.timedelta(seconds=1), on_server)
+
+    def on_server(data=None):
+        IOLoop.instance().remove_timeout(timeout[0])
+
+        if data:
+            identifier = re.findall('Server: (.*)', data)[0].strip()
+            logging.debug('rtsp netcam identifier is "%s"' % identifier)
+        
+        else:
+            identifier = None
+            logging.debug('no rtsp netcam identifier')
+
+        handle_success(identifier)
+
+    def on_close():
+        if called[0]:
+            return
+        if not check_error():
+            handle_error('connection closed')
+
+    def handle_success(identifier):
+        if called[0]:
+            return
+        
+        called[0] = True
+        cameras = []
+        rtsp_support = config.motion_rtsp_support()
+        if identifier:
+            identifier = ' ' + identifier
+            
+        else:
+            identifier = ''
+
+        if 'udp' in rtsp_support:
+            cameras.append({'id': 'udp', 'name': '%sRTSP/UDP Camera' % identifier})
+        
+        if 'tcp' in rtsp_support:
+            cameras.append({'id': 'tcp', 'name': '%sRTSP/TCP Camera' % identifier})
+
+        callback(cameras)
+
+    def handle_error(e):
+        if called[0]:
+            return
+        
+        called[0] = True
+        logging.error('rtsp client error: %s' % unicode(e))
+
+        try:
+            stream.close()
+        
+        except:
+            pass
+        
+        callback(error=unicode(e))
+
+    def check_error():
+        error = getattr(stream, 'error', None)
+        if error and getattr(error, 'errno', None) != 0:
+            handle_error(error.strerror)
+            return True
+
+        if stream and stream.socket is None:
+            handle_error('connection closed')
+            stream.close()
+
+            return True
+        
+        return False
+
+    stream = connect()
+
+
+def compute_signature(method, uri, body, key):
+    parts = list(urlparse.urlsplit(uri))
+    query = [q for q in urlparse.parse_qsl(parts[3], keep_blank_values=True) if (q[0] != '_signature')]
+    query.sort(key=lambda q: q[0])
+    # "safe" characters here are set to match the encodeURIComponent JavaScript counterpart
+    query = [(n, urllib.quote(v, safe="!'()*~")) for (n, v) in query]
+    query = '&'.join([(q[0] + '=' + q[1]) for q in query])
+    parts[0] = parts[1] = ''
+    parts[3] = query
+    uri = urlparse.urlunsplit(parts)
+    uri = _SIGNATURE_REGEX.sub('-', uri)
+    key = _SIGNATURE_REGEX.sub('-', key)
+
+    if body and body.startswith('---'):
+        body = None # file attachment
+
+    body = body and _SIGNATURE_REGEX.sub('-', body.decode('utf8'))
+
+    return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower()
+
+
+def build_basic_header(username, password):
+    return 'Basic ' + base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
+
+
+def build_digest_header(method, url, username, password, state):
+    realm = state['realm']
+    nonce = state['nonce']
+    last_nonce = state.get('last_nonce', '')
+    nonce_count = state.get('nonce_count', 0)
+    qop = state.get('qop')
+    algorithm = state.get('algorithm')
+    opaque = state.get('opaque')
+
+    if algorithm is None:
+        _algorithm = 'MD5'
+
+    else:
+        _algorithm = algorithm.upper()
+
+    if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':
+        def md5_utf8(x):
+            if isinstance(x, str):
+                x = x.encode('utf-8')
+            return hashlib.md5(x).hexdigest()
+        hash_utf8 = md5_utf8
+
+    elif _algorithm == 'SHA':
+        def sha_utf8(x):
+            if isinstance(x, str):
+                x = x.encode('utf-8')
+            return hashlib.sha1(x).hexdigest()
+        hash_utf8 = sha_utf8
+
+    KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
+
+    if hash_utf8 is None:
+        return None
+
+    entdig = None
+    p_parsed = urlparse.urlparse(url)
+    path = p_parsed.path
+    if p_parsed.query:
+        path += '?' + p_parsed.query
+
+    A1 = '%s:%s:%s' % (username, realm, password)
+    A2 = '%s:%s' % (method, path)
+
+    HA1 = hash_utf8(A1)
+    HA2 = hash_utf8(A2)
+
+    if nonce == last_nonce:
+        nonce_count += 1
+
+    else:
+        nonce_count = 1
+    
+    ncvalue = '%08x' % nonce_count
+    s = str(nonce_count).encode('utf-8')
+    s += nonce.encode('utf-8')
+    s += time.ctime().encode('utf-8')
+    s += os.urandom(8)
+
+    cnonce = (hashlib.sha1(s).hexdigest()[:16])
+    if _algorithm == 'MD5-SESS':
+        HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
+
+    if qop is None:
+        respdig = KD(HA1, "%s:%s" % (nonce, HA2))
+    
+    elif qop == 'auth' or 'auth' in qop.split(','):
+        noncebit = "%s:%s:%s:%s:%s" % (
+            nonce, ncvalue, cnonce, 'auth', HA2
+            )
+        respdig = KD(HA1, noncebit)
+    
+    else:
+        return None
+
+    last_nonce = nonce
+
+    base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
+           'response="%s"' % (username, realm, nonce, path, respdig)
+    if opaque:
+        base += ', opaque="%s"' % opaque
+    if algorithm:
+        base += ', algorithm="%s"' % algorithm
+    if entdig:
+        base += ', digest="%s"' % entdig
+    if qop:
+        base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)
+    
+    state['last_nonce'] = last_nonce
+    state['nonce_count'] = nonce_count
+
+    return 'Digest %s' % (base)
diff --git a/motioneye/v4l2ctl.py b/motioneye/v4l2ctl.py
new file mode 100644 (file)
index 0000000..10637f4
--- /dev/null
@@ -0,0 +1,418 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import fcntl
+import logging
+import os.path
+import re
+import stat
+import subprocess
+import time
+import utils
+
+
+_resolutions_cache = {}
+_ctrls_cache = {}
+_ctrl_values_cache = {}
+
+_DEV_V4L_BY_ID = '/dev/v4l/by-id/'
+
+
+def find_v4l2_ctl():
+    try:
+        return subprocess.check_output('which v4l2-ctl', shell=True).strip()
+    
+    except subprocess.CalledProcessError: # not found
+        return None
+
+
+def list_devices():
+    global _resolutions_cache, _ctrls_cache, _ctrl_values_cache
+    
+    logging.debug('listing v4l2 devices...')
+    
+    try:
+        output = ''
+        started = time.time()
+        p = subprocess.Popen('v4l2-ctl --list-devices 2>/dev/null', shell=True, stdout=subprocess.PIPE, bufsize=1)
+
+        fd = p.stdout.fileno()
+        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+        while True:
+            try:
+                data = p.stdout.read(1024)
+                if not data:
+                    break
+            
+            except IOError:
+                data = ''
+                time.sleep(0.01)
+
+            output += data
+
+            if len(output) > 10240:
+                logging.warn('v4l2-ctl command returned more than 10k of output')
+                break
+            
+            if time.time() - started > 3:
+                logging.warn('v4l2-ctl command ran for more than 3 seconds')
+                break
+
+    except subprocess.CalledProcessError:
+        logging.debug('failed to list devices (probably no devices installed)')
+        return []
+
+    try:
+        # try to kill the v4l2-ctl subprocess
+        p.kill()
+
+    except:
+        pass # nevermind
+
+    name = None
+    devices = []
+    for line in output.split('\n'):
+        if line.startswith('\t'):
+            device = line.strip()
+            persistent_device = find_persistent_device(device)
+            devices.append((device, persistent_device, name))
+        
+            logging.debug('found device %(name)s: %(device)s, %(persistent_device)s' % {
+                    'name': name, 'device': device, 'persistent_device': persistent_device})
+
+        else:
+            name = line.split('(')[0].strip()
+    
+    # clear the cache
+    _resolutions_cache = {}
+    _ctrls_cache = {}
+    _ctrl_values_cache = {}
+
+    return devices
+
+
+def list_resolutions(device):
+    global _resolutions_cache
+    
+    device = utils.make_str(device)
+    
+    if device in _resolutions_cache:
+        return _resolutions_cache[device]
+    
+    logging.debug('listing resolutions of device %(device)s...' % {'device': device})
+    
+    resolutions = set()
+    output = ''
+    started = time.time()
+    p = subprocess.Popen('v4l2-ctl -d "%(device)s" --list-formats-ext | grep -vi stepwise | grep -oE "[0-9]+x[0-9]+" || true' % {
+            'device': device}, shell=True, stdout=subprocess.PIPE, bufsize=1)
+
+    fd = p.stdout.fileno()
+    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+    while True:
+        try:
+            data = p.stdout.read(1024)
+            if not data:
+                break
+
+        except IOError:
+            data = ''
+            time.sleep(0.01)
+
+        output += data
+
+        if len(output) > 10240:
+            logging.warn('v4l2-ctl command returned more than 10k of output')
+            break
+        
+        if time.time() - started > 3:
+            logging.warn('v4l2-ctl command ran for more than 3 seconds')
+            break
+    
+    try:
+        # try to kill the v4l2-ctl subprocess
+        p.kill()
+    
+    except:
+        pass # nevermind
+
+    for pair in output.split('\n'):
+        pair = pair.strip()
+        if not pair:
+            continue
+        
+        width, height = pair.split('x')
+        width = int(width)
+        height = int(height)
+
+        if (width, height) in resolutions:
+            continue # duplicate resolution
+
+        if width < 96 or height < 96: # some reasonable minimal values
+            continue
+        
+        if width % 16 or height % 16: # ignore non-modulo 16 resolutions
+            continue
+
+        resolutions.add((width, height))
+        
+        logging.debug('found resolution %(width)sx%(height)s for device %(device)s' % {
+                'device': device, 'width': width, 'height': height})
+    
+    if not resolutions:
+        logging.debug('no resolutions found for device %(device)s, using common values' % {'device': device})
+
+        # no resolution returned by v4l2-ctl call, add common default resolutions
+        resolutions = utils.COMMON_RESOLUTIONS
+
+    resolutions = list(sorted(resolutions, key=lambda r: (r[0], r[1])))
+    _resolutions_cache[device] = resolutions
+    
+    return resolutions
+
+
+def device_present(device):
+    device = utils.make_str(device)
+    
+    try:
+        st = os.stat(device)
+        return stat.S_ISCHR(st.st_mode)
+
+    except:
+        return False
+    
+
+def find_persistent_device(device):
+    device = utils.make_str(device)
+    
+    try:
+        devs_by_id = os.listdir(_DEV_V4L_BY_ID)
+
+    except OSError:
+        return device
+    
+    for p in devs_by_id:
+        p = os.path.join(_DEV_V4L_BY_ID, p)
+        if os.path.realpath(p) == device:
+            return p
+    
+    return device
+
+
+def get_brightness(device):
+    return _get_ctrl(device, 'brightness')
+
+
+def set_brightness(device, value):
+    _set_ctrl(device, 'brightness', value)
+
+
+def get_contrast(device):
+    return _get_ctrl(device, 'contrast')
+
+
+def set_contrast(device, value):
+    _set_ctrl(device, 'contrast', value)
+
+
+def get_saturation(device):
+    return _get_ctrl(device, 'saturation')
+
+
+def set_saturation(device, value):
+    _set_ctrl(device, 'saturation', value)
+
+
+def get_hue(device):
+    return _get_ctrl(device, 'hue')
+
+
+def set_hue(device, value):
+    _set_ctrl(device, 'hue', value)
+
+
+def _get_ctrl(device, control):
+    global _ctrl_values_cache
+    
+    device = utils.make_str(device)
+    
+    if not device_present(device):
+        return None
+    
+    if device in _ctrl_values_cache and control in _ctrl_values_cache[device]:
+        return _ctrl_values_cache[device][control]
+    
+    controls = _list_ctrls(device)
+    properties = controls.get(control)
+    if properties is None:
+        logging.warn('control %(control)s not found for device %(device)s' % {
+                'control': control, 'device': device})
+        
+        return None
+    
+    value = int(properties['value'])
+    
+    # adjust the value range
+    if 'min' in properties and 'max' in properties:
+        min_value = int(properties['min'])
+        max_value = int(properties['max'])
+        
+        value = int(round((value - min_value) * 100.0 / (max_value - min_value)))
+    
+    else:
+        logging.warn('min and max values not found for control %(control)s of device %(device)s' % {
+                'control': control, 'device': device})
+    
+    logging.debug('control %(control)s of device %(device)s is %(value)s%%' % {
+            'control': control, 'device': device, 'value': value})
+    
+    return value
+
+
+def _set_ctrl(device, control, value):
+    global _ctrl_values_cache
+    
+    device = utils.make_str(device)
+    
+    if not device_present(device):
+        return
+
+    controls = _list_ctrls(device)
+    properties = controls.get(control)
+    if properties is None:
+        logging.warn('control %(control)s not found for device %(device)s' % {
+                'control': control, 'device': device})
+        
+        return
+    
+    _ctrl_values_cache.setdefault(device, {})[control] = value
+
+    # adjust the value range
+    if 'min' in properties and 'max' in properties:
+        min_value = int(properties['min'])
+        max_value = int(properties['max'])
+        
+        value = int(round(min_value + value * (max_value - min_value) / 100.0))
+    
+    else:
+        logging.warn('min and max values not found for control %(control)s of device %(device)s' % {
+                'control': control, 'device': device})
+    
+    logging.debug('setting control %(control)s of device %(device)s to %(value)s' % {
+            'control': control, 'device': device, 'value': value})
+
+    output = ''
+    started = time.time()
+    p = subprocess.Popen('v4l2-ctl -d "%(device)s" --set-ctrl %(control)s=%(value)s' % {
+            'device': device, 'control': control, 'value': value}, shell=True, stdout=subprocess.PIPE, bufsize=1)
+
+    fd = p.stdout.fileno()
+    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+    while True:
+        try:
+            data = p.stdout.read(1024)
+            if not data:
+                break
+        
+        except IOError:
+            data = ''
+            time.sleep(0.01)
+
+        output += data
+
+        if len(output) > 10240:
+            logging.warn('v4l2-ctl command returned more than 10k of output')
+            break
+
+        if time.time() - started > 3:
+            logging.warn('v4l2-ctl command ran for more than 3 seconds')
+            break
+
+    try:
+        # try to kill the v4l2-ctl subprocess
+        p.kill()
+
+    except:
+        pass # nevermind
+
+
+def _list_ctrls(device):
+    global _ctrls_cache
+    
+    device = utils.make_str(device)
+
+    if device in _ctrls_cache:
+        return _ctrls_cache[device]
+    
+    output = ''
+    started = time.time()
+    p = subprocess.Popen('v4l2-ctl -d "%(device)s" --list-ctrls' % {
+            'device': device}, shell=True, stdout=subprocess.PIPE, bufsize=1)
+
+    fd = p.stdout.fileno()
+    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+    while True:
+        try:
+            data = p.stdout.read(1024)
+            if not data:
+                break
+        
+        except IOError:
+            data = ''
+            time.sleep(0.01)
+
+        output += data
+
+        if len(output) > 10240:
+            logging.warn('v4l2-ctl command returned more than 10k of output')
+            break
+
+        if time.time() - started > 3:
+            logging.warn('v4l2-ctl command ran for more than 3 seconds')
+            break
+
+    try:
+        # try to kill the v4l2-ctl subprocess
+        p.kill()
+
+    except:
+        pass # nevermind
+
+    controls = {}
+    for line in output.split('\n'):
+        if not line:
+            continue
+        
+        match = re.match('^\s*(\w+)\s+\(\w+\)\s+\:\s*(.+)', line)
+        if not match:
+            continue
+        
+        (control, properties) = match.groups()
+        properties = dict([v.split('=', 1) for v in properties.split(' ') if v.count('=')])
+        controls[control] = properties
+    
+    _ctrls_cache[device] = controls
+    
+    return controls
diff --git a/motioneye/webhook.py b/motioneye/webhook.py
new file mode 100755 (executable)
index 0000000..016a0db
--- /dev/null
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import logging
+import sys
+import urllib2
+import urlparse
+
+import settings
+
+from motioneye import _configure_settings, _configure_logging
+
+
+_configure_settings()
+_configure_logging()
+
+
+def print_usage():
+    print 'Usage: webhook.py <method> <url>'
+
+
+if __name__ == '__main__':
+    if len(sys.argv) < 3:
+        print_usage()
+        sys.exit(-1)
+    
+    method = sys.argv[1] 
+    url = sys.argv[2]
+
+    logging.debug('method = %s' % method)
+    logging.debug('url = %s' % url)
+    
+    if method == 'POST':
+        parts = urlparse.urlparse(url)
+        data = parts.query
+
+    else:
+        data = None
+
+    request = urllib2.Request(url, data)
+    try:
+        urllib2.urlopen(request, timeout=settings.REMOTE_REQUEST_TIMEOUT)
+        logging.debug('webhook successfully called')
+    
+    except Exception as e:
+        logging.error('failed to call webhook: %s' % e)
diff --git a/motioneye/wifictl.py b/motioneye/wifictl.py
new file mode 100644 (file)
index 0000000..32ec562
--- /dev/null
@@ -0,0 +1,248 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import logging
+import re
+import settings
+
+from config import additional_config, additional_section
+
+
+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' % WPA_SUPPLICANT_CONF)
+    
+    try:
+        conf_file = open(WPA_SUPPLICANT_CONF, 'r')
+    
+    except Exception as e:
+        logging.error('could open wifi settings file %(path)s: %(msg)s' % {
+                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+        
+        return {
+            'wifiEnabled': False,
+            'wifiNetworkName': '',
+            'wifiNetworkKey': ''
+        }
+    
+    lines = conf_file.readlines()
+    conf_file.close()
+    
+    ssid = psk = ''
+    in_section = False
+    for line in lines:
+        line = line.strip()
+        if line.startswith('#'):
+            continue
+        
+        if '{' in line:
+            in_section = True
+            
+        elif '}' in line:
+            in_section = False
+            break
+            
+        elif in_section:
+            m = re.search('ssid\s*=\s*"(.*?)"', line)
+            if m:
+                ssid = m.group(1)
+    
+            m = re.search('psk\s*=\s*"(.*?)"', line)
+            if m:
+                psk = m.group(1)
+
+    if ssid:
+        logging.debug('wifi is enabled (ssid = "%s")' % ssid)
+    
+        return {
+            'wifiEnabled': True,
+            'wifiNetworkName': ssid,
+            'wifiNetworkKey': psk
+        }
+
+    else:
+        logging.debug('wifi is disabled')
+
+        return {
+            'wifiEnabled': False,
+            'wifiNetworkName': ssid,
+            'wifiNetworkKey': psk
+        }
+
+
+def _set_wifi_settings(s):
+    s.setdefault('wifiEnabled', False)
+    s.setdefault('wifiNetworkName', '')
+    s.setdefault('wifiNetworkKey', '')
+    
+    logging.debug('writing wifi settings to %s: enabled=%s, ssid="%s"' % (
+            WPA_SUPPLICANT_CONF, s['wifiEnabled'], s['wifiNetworkName']))
+
+    enabled = s['wifiEnabled']
+    ssid = s['wifiNetworkName']
+    psk = s['wifiNetworkKey']
+    
+    # will update the first configured network
+    try:
+        conf_file = open(WPA_SUPPLICANT_CONF, 'r')
+    
+    except Exception as e:
+        logging.error('could open wifi settings file %(path)s: %(msg)s' % {
+                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+
+        return
+    
+    lines = conf_file.readlines()
+    conf_file.close()
+    
+    in_section = False
+    found_ssid = False
+    found_psk = False
+    i = 0
+    while i < len(lines):
+        line = lines[i].strip()
+        if line.startswith('#'):
+            i += 1
+            continue
+        
+        if '{' in line:
+            in_section = True
+            
+        elif '}' in line:
+            in_section = False
+            if enabled and ssid and not found_ssid:
+                lines.insert(i, '    ssid="' + ssid + '"\n')
+            if enabled and psk and not found_psk:
+                lines.insert(i, '    psk="' + psk + '"\n')
+            
+            found_psk = found_ssid = True
+            
+            break
+            
+        elif in_section:
+            if enabled:
+                if re.match('ssid\s*=\s*".*?"', line):
+                    lines[i] = '    ssid="' + ssid + '"\n'
+                    found_ssid = True
+                
+                elif re.match('psk\s*=\s*".*?"', line):
+                    if psk:
+                        lines[i] = '    psk="' + psk + '"\n'
+                        found_psk = True
+                
+                    else:
+                        lines.pop(i)
+                        i -= 1
+        
+            else: # wifi disabled
+                if re.match('ssid\s*=\s*".*?"', line) or re.match('psk\s*=\s*".*?"', line):
+                    lines.pop(i)
+                    i -= 1
+        
+        i += 1
+
+    if enabled and not found_ssid:
+        lines.append('network={\n')
+        lines.append('    scan_ssid=1\n')
+        lines.append('    ssid="' + ssid + '"\n')
+        lines.append('    psk="' + psk + '"\n')
+        lines.append('}\n\n')
+
+    try:
+        conf_file = open(WPA_SUPPLICANT_CONF, 'w')
+    
+    except Exception as e:
+        logging.error('could open wifi settings file %(path)s: %(msg)s' % {
+                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+
+        return
+    
+    for line in lines:
+        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': False,
+        'reboot': True,
+        'depends': ['wifiEnabled'],
+        'get': _get_wifi_settings,
+        'set': _set_wifi_settings,
+        'get_set_dict': True
+    }
diff --git a/motioneye/wsswitch.py b/motioneye/wsswitch.py
new file mode 100644 (file)
index 0000000..be65c27
--- /dev/null
@@ -0,0 +1,116 @@
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
+
+import datetime
+import logging
+import tornado
+
+import config
+import motionctl
+import utils
+
+
+def start():
+    ioloop = tornado.ioloop.IOLoop.instance()
+    ioloop.add_timeout(datetime.timedelta(seconds=10), _check_ws)
+
+
+def _during_working_schedule(now, working_schedule):
+    parts = working_schedule.split('|')
+    if len(parts) < 7:
+        return False # invalid ws
+
+    ws_day = parts[now.weekday()]
+    parts = ws_day.split('-')
+    if len(parts) != 2:
+        return False # invalid ws
+    
+    _from, to = parts
+    if not _from or not to:
+        return False # ws disabled for this day
+    
+    _from = _from.split(':')
+    to = to.split(':')
+    if len(_from) != 2 or len(to) != 2:
+        return False # invalid ws
+    
+    try:
+        from_h = int(_from[0])
+        from_m = int(_from[1])
+        to_h = int(to[0])
+        to_m = int(to[1])
+    
+    except ValueError:
+        return False # invalid ws
+    
+    if now.hour < from_h or now.hour > to_h:
+        return False
+
+    if now.hour == from_h and now.minute < from_m:
+        return False
+
+    if now.hour == to_h and now.minute > to_m:
+        return False
+    
+    return True
+
+
+def _check_ws():
+    # schedule the next call
+    ioloop = tornado.ioloop.IOLoop.instance()
+    ioloop.add_timeout(datetime.timedelta(seconds=10), _check_ws)
+
+    if not motionctl.running():
+        return
+
+    now = datetime.datetime.now()
+    for camera_id in config.get_camera_ids():
+        camera_config = config.get_camera(camera_id)
+        if not utils.local_motion_camera(camera_config):
+            continue
+        
+        working_schedule = camera_config.get('@working_schedule')
+        motion_detection = camera_config.get('@motion_detection')
+        working_schedule_type = camera_config.get('@working_schedule_type') or 'outside'
+        
+        if not working_schedule: # working schedule disabled, motion detection left untouched
+            continue
+        
+        if not motion_detection: # motion detection explicitly disabled
+            continue
+        
+        now_during = _during_working_schedule(now, working_schedule)
+        must_be_enabled = (now_during and working_schedule_type == 'during') or (not now_during and working_schedule_type == 'outside')
+        
+        currently_enabled = motionctl.get_motion_detection(camera_id)
+        if currently_enabled is None: # could not detect current status
+            logging.warn('skipping motion detection status update for camera with id %(id)s' % {'id': camera_id})
+            continue
+            
+        if currently_enabled and not must_be_enabled:
+            logging.debug('must disable motion detection for camera with id %(id)s (%(what)s working schedule)' % {
+                    'id': camera_id,
+                    'what': working_schedule_type})
+            
+            motionctl.set_motion_detection(camera_id, False)
+
+        elif not currently_enabled and must_be_enabled:
+            logging.debug('must enable motion detection for camera with id %(id)s (%(what)s working schedule)' % {
+                    'id': camera_id,
+                    'what': working_schedule_type})
+            
+            motionctl.set_motion_detection(camera_id, True)
diff --git a/sendmail.py b/sendmail.py
deleted file mode 100755 (executable)
index 4844c08..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import logging
-import os
-import re
-import smtplib
-import socket
-import sys
-import time
-
-from email import Encoders
-from email.mime.text import MIMEText
-from email.MIMEMultipart import MIMEMultipart
-from email.MIMEBase import MIMEBase
-from tornado.ioloop import IOLoop
-
-import settings
-
-from motioneye import _configure_settings, _configure_logging, _configure_signals
-
-_configure_settings()
-_configure_signals()
-_configure_logging(module='sendmail')
-
-import config
-import mediafiles
-import tzctl
-
-
-messages = {
-    'motion_start': 'Motion has been detected by camera "%(camera)s/%(hostname)s" at %(moment)s (%(timezone)s).'
-}
-
-subjects = {
-    'motion_start': 'motionEye: motion detected by "%(camera)s"'
-}
-
-
-def send_mail(server, port, account, password, tls, to, subject, message, files):
-    conn = smtplib.SMTP(server, port, timeout=getattr(settings, 'SMTP_TIMEOUT', 60))
-    if tls:
-        conn.starttls()
-    
-    if account and password:
-        conn.login(account, password)
-    
-    _from = account or 'motioneye@' + socket.gethostname()
-    
-    email = MIMEMultipart()
-    email['Subject'] = subject
-    email['From'] = _from
-    email['To'] = ', '.join(to)
-    email.attach(MIMEText(message))
-    
-    for file in reversed(files):
-        part = MIMEBase('image', 'jpeg')
-        with open(file, 'rb') as f:
-            part.set_payload(f.read())
-        
-        Encoders.encode_base64(part)
-        part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(file))
-        email.attach(part)
-    
-    if files:
-        logging.debug('attached %d pictures' % len(files))
-
-    logging.debug('sending email message')
-    conn.sendmail(_from, to, email.as_string())
-    conn.quit()
-
-
-def make_message(subject, message, camera_id, moment, timespan, callback):
-    camera_config = config.get_camera(camera_id)
-    
-    def on_media_files(media_files):
-        logging.debug('got media files')
-        
-        timestamp = time.mktime(moment.timetuple())
-
-        media_files = [m for m in media_files if abs(m['timestamp'] - timestamp) < timespan] # filter out non-recent media files
-        media_files.sort(key=lambda m: m['timestamp'], reverse=True)
-        media_files = [os.path.join(camera_config['target_dir'], re.sub('^/', '', m['path'])) for m in media_files]
-        
-        logging.debug('selected %d pictures' % len(media_files))
-
-        format_dict = {
-            'camera': camera_config['@name'],
-            'hostname': socket.gethostname(),
-            'moment': moment.strftime('%Y-%m-%d %H:%M:%S'),
-        }
-        
-        if settings.LOCAL_TIME_FILE:
-            format_dict['timezone'] = tzctl._get_time_zone()
-        
-        else:
-            format_dict['timezone'] = 'local time'
-    
-        m = message % format_dict
-        s = subject % format_dict
-        s = s.replace('\n', ' ')
-    
-        m += '\n\n'
-        m += 'motionEye.'
-        
-        callback(s, m, media_files)
-
-    if not timespan:
-        return on_media_files([])
-
-    logging.debug('creating email message')
-
-    time.sleep(timespan) # give motion some time to create motion pictures
-    mediafiles.list_media(camera_config, media_type='picture', callback=on_media_files)
-
-
-def print_usage():
-    print 'Usage: sendmail.py <server> <port> <account> <password> <tls> <to> <msg_id> <camera_id> <moment> <frames> [timespan]'
-
-
-if __name__ == '__main__':
-    if len(sys.argv) < 10:
-        print_usage()
-        sys.exit(-1)
-    
-    server = sys.argv[1]
-    port = int(sys.argv[2]) 
-    account = sys.argv[3]
-    password = sys.argv[4]
-    tls = sys.argv[5].lower() == 'true'
-    to = sys.argv[6]
-    msg_id = sys.argv[7]
-    camera_id = sys.argv[8]
-    moment = sys.argv[9]
-    try:
-        timespan = int(sys.argv[10])
-
-    except:
-        timespan = 0
-
-    logging.debug('hello!')
-
-    message = messages.get(msg_id)
-    subject = subjects.get(msg_id)
-    if not message or not subject:
-        logging.error('unknown message id')
-        sys.exit(-1)
-    
-    moment = datetime.datetime.strptime(moment, '%Y-%m-%dT%H:%M:%S')
-    
-    logging.debug('server = %s' % server)
-    logging.debug('port = %s' % port)
-    logging.debug('account = %s' % account)
-    logging.debug('password = ******')
-    logging.debug('server = %s' % server)
-    logging.debug('tls = %s' % tls)
-    logging.debug('to = %s' % to)
-    logging.debug('msg_id = %s' % msg_id)
-    logging.debug('camera_id = %s' % camera_id)
-    logging.debug('moment = %s' % moment.strftime('%Y-%m-%d %H:%M:%S'))
-    logging.debug('smtp timeout = %d' % settings.SMTP_TIMEOUT)
-    logging.debug('timespan = %d' % timespan)
-    
-    if not to:
-        logging.info('no email address specified')
-        sys.exit(0)
-
-    to = [t.strip() for t in re.split('[,;| ]', to)]
-    to = [t for t in to if t]
-
-    io_loop = IOLoop.instance()
-    
-    def on_message(subject, message, files):
-        try:
-            send_mail(server, port, account, password, tls, to, subject, message, files)
-            logging.info('email sent')
-        
-        except Exception as e:
-            logging.error('failed to send mail: %s' % e, exc_info=True)
-
-        io_loop.stop()
-    
-    def ioloop_timeout():
-        io_loop.stop()
-    
-    make_message(subject, message, camera_id, moment, timespan, on_message)
-
-    io_loop.add_timeout(datetime.timedelta(seconds=settings.SMTP_TIMEOUT), ioloop_timeout)
-    io_loop.start()
-
-    logging.debug('bye!')
\ No newline at end of file
diff --git a/settings_default.py b/settings_default.py
deleted file mode 100644 (file)
index d200cc3..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-
-import logging
-import os.path
-import sys
-
-# you normally don't have to change these 
-PROJECT_PATH = os.path.dirname(sys.argv[0])
-TEMPLATE_PATH = os.path.join(PROJECT_PATH, 'templates')
-STATIC_PATH = os.path.join(PROJECT_PATH, 'static')
-
-# static files (.css, .js etc) are served at this root url
-STATIC_URL = '/static/'
-
-# path to the config directory; must be writable
-CONF_PATH = os.path.abspath(os.path.join(PROJECT_PATH, 'conf'))
-
-# pid files go here
-RUN_PATH = os.path.abspath(os.path.join(PROJECT_PATH, 'run'))
-
-# log files go here
-LOG_PATH = os.path.abspath(os.path.join(PROJECT_PATH, 'log'))
-
-# default output path for media files
-MEDIA_PATH = os.path.abspath(os.path.join(PROJECT_PATH, 'media'))
-
-# path to motion binary (automatically detected if not set)
-MOTION_BINARY = None
-
-# set to logging.DEBUG for verbose output
-LOG_LEVEL = logging.INFO
-
-# set to 127.0.0.1 to restrict access to localhost
-LISTEN = '0.0.0.0'
-
-# change the port according to your requirements/restrictions
-PORT = 8765
-
-# interval in seconds at which motionEye checks the SMB mounts
-MOUNT_CHECK_INTERVAL = 300
-
-# interval in seconds at which motionEye checks if motion is running
-MOTION_CHECK_INTERVAL = 10
-
-# interval in seconds at which the janitor is called to remove old pictures and movies
-CLEANUP_INTERVAL = 43200
-
-# interval in seconds at which the thumbnail mechanism runs (set to 0 to disable) 
-THUMBNAILER_INTERVAL = 60
-
-# timeout in seconds when waiting for response from a remote motionEye server
-REMOTE_REQUEST_TIMEOUT = 10
-
-# timeout in seconds when waiting for mjpg data from the motion daemon
-MJPG_CLIENT_TIMEOUT = 10
-
-# timeout in seconds after which an idle mjpg client is removed (set to 0 to disable)
-MJPG_CLIENT_IDLE_TIMEOUT = 10
-
-# enable SMB shares (requires root) 
-SMB_SHARES = False
-
-# the directory where the SMB mounts will be created
-SMB_MOUNT_ROOT = '/media'
-
-# path to a wpa_supplicant.conf file if wifi settings UI is desired
-WPA_SUPPLICANT_CONF = None
-
-# path to a localtime file if time zone settings UI is desired
-LOCAL_TIME_FILE = None
-
-# enables shutdown and rebooting after changing system settings (such as wifi settings or system updates)
-ENABLE_REBOOT = False
-
-# the timeout in seconds to use when talking to a SMTP server
-SMTP_TIMEOUT = 60
-
-# the time to wait for zip file creation
-ZIP_TIMEOUT = 500
-
-# enable adding and removing cameras from UI
-ADD_REMOVE_CAMERAS = True
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..b963858
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,64 @@
+
+# Always prefer setuptools over distutils
+from setuptools import setup
+# To use a consistent encoding
+from codecs import open
+from os import path
+
+here = path.abspath(path.dirname(__file__))
+
+with open(path.join(here, 'README.md'), encoding='utf-8') as f:
+    long_description = f.read()
+
+setup(
+    name='motioneye',
+
+    version='0.25.2',
+
+    description='motionEye server',
+    long_description=long_description,
+
+    url='https://bitbucket.org/ccrisan/motioneye/',
+
+    author='Calin Crisan',
+    author_email='ccrisan@gmail.com',
+
+    license='GPLv3',
+
+    # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+    classifiers=[
+        'Development Status :: 3 - Beta',
+
+        'Intended Audience :: End Users/Desktop',
+        'Topic :: Multimedia :: Video',
+
+        'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
+
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7'
+    ],
+
+    keywords='motion video surveillance frontend',
+
+    packages=['motioneye'],
+
+    install_requires=['tornado>=3.1', 'jinja2', 'pillow', 'pycurl'],
+
+    package_data={
+        'motioneye': [
+            'static/*',
+            'static/*/*',
+            'templates/*'
+        ]
+    },
+
+    data_files=[],
+
+    entry_points={
+        'console_scripts': [
+            'motioneye=motioneye.motioneye:main',
+        ],
+    },
+)
+
diff --git a/src/cleanup.py b/src/cleanup.py
deleted file mode 100644 (file)
index a032ef1..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import logging
-import multiprocessing
-import os
-import signal
-import tornado
-
-import mediafiles
-import settings
-import thumbnailer
-
-
-_process = None
-
-
-def start():
-    # schedule the first call a bit later to improve performance at startup
-    ioloop = tornado.ioloop.IOLoop.instance()
-    ioloop.add_timeout(datetime.timedelta(seconds=60), _run_process)
-
-
-def stop():
-    global _process
-    
-    if not running():
-        _process = None
-        return
-    
-    if _process.is_alive():
-        _process.join(timeout=10)
-    
-    if _process.is_alive():
-        logging.error('cleanup process did not finish in time, killing it...')
-        os.kill(_process.pid, signal.SIGKILL)
-    
-    _process = None
-
-
-def running():
-    return _process is not None and _process.is_alive()
-
-
-def _run_process():
-    global _process
-    
-    ioloop = tornado.ioloop.IOLoop.instance()
-    
-    if thumbnailer.running():
-        # postpone if thumbnailer is currently running
-        ioloop.add_timeout(datetime.timedelta(seconds=60), _run_process)
-        
-        return
-        
-    else:
-        # schedule the next call
-        ioloop.add_timeout(datetime.timedelta(seconds=settings.CLEANUP_INTERVAL), _run_process)
-
-    if not running(): # check that the previous process has finished
-        logging.debug('running cleanup process...')
-
-        _process = multiprocessing.Process(target=_do_cleanup)
-        _process.start()
-
-
-def _do_cleanup():
-    # this will be executed in a separate subprocess
-    
-    # ignore the terminate and interrupt signals in this subprocess
-    signal.signal(signal.SIGINT, signal.SIG_IGN)
-    signal.signal(signal.SIGTERM, signal.SIG_IGN)
-    
-    try:
-        mediafiles.cleanup_media('picture')
-        mediafiles.cleanup_media('movie')
-        logging.debug('cleanup done')
-         
-    except Exception as e:
-        logging.error('failed to cleanup media files: %(msg)s' % {
-                'msg': unicode(e)}, exc_info=True)
diff --git a/src/config.py b/src/config.py
deleted file mode 100644 (file)
index 235d763..0000000
+++ /dev/null
@@ -1,1789 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import collections
-import datetime
-import errno
-import logging
-import os.path
-import re
-import shlex
-import subprocess
-import urlparse
-
-from tornado.ioloop import IOLoop
-
-import diskctl
-import powerctl
-import settings
-import update
-import utils
-import v4l2ctl
-
-from utils import OrderedDict
-
-
-_CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf'
-_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')
-
-_KNOWN_MOTION_OPTIONS = set([
-    'auto_brightness', 'brightness', 'contrast', 'emulate_motion', 'event_gap', 'ffmpeg_bps', 'ffmpeg_output_movies', 'ffmpeg_variable_bitrate', 'ffmpeg_video_codec',
-    'framerate', 'height', 'hue', 'lightswitch', 'locate_motion_mode', 'locate_motion_style', 'minimum_motion_frames', 'movie_filename', 'max_movie_time', 'max_mpeg_time',
-    'noise_level', 'noise_tune', 'on_event_end', 'on_event_start', 'output_pictures', 'picture_filename', 'post_capture', 'pre_capture', 'quality', 'rotate', 'saturation',
-    'snapshot_filename', 'snapshot_interval', 'stream_auth_method', 'stream_authentication', 'stream_localhost', 'stream_maxrate', 'stream_motion', 'stream_port', 'stream_quality',
-    'target_dir', 'text_changes', 'text_double', 'text_left', 'text_right', 'threshold', 'videodevice', 'width',
-    'webcam_localhost', 'webcam_port', 'webcam_maxrate', 'webcam_quality', 'webcam_motion', 'ffmpeg_cap_new', 'output_normal', 'output_motion', 'jpeg_filename', 'output_all', 'gap', 'locate',
-    'netcam_url', 'netcam_userpass', 'netcam_http', 'netcam_tolerant_check', 'netcam_keepalive', 'rtsp_uses_tcp'
-])
-
-
-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
-    
-    if not as_lines and _main_config_cache is not None:
-        return _main_config_cache
-    
-    config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
-    
-    logging.debug('reading main config from file %(path)s...' % {'path': config_file_path})
-    
-    lines = None
-    try:
-        file = open(config_file_path, 'r')
-    
-    except IOError as e:
-        if e.errno == errno.ENOENT:  # file does not exist
-            logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
-            
-            lines = []
-        
-        else:
-            logging.error('could not open main config file %(path)s: %(msg)s' % {
-                    'path': config_file_path, 'msg': unicode(e)})
-            
-            raise
-    
-    if lines is None:
-        try:
-            lines = [l[:-1] for l in file.readlines()]
-        
-        except Exception as e:
-            logging.error('could not read main config file %(path)s: %(msg)s' % {
-                    'path': config_file_path, 'msg': unicode(e)})
-            
-            raise
-        
-        finally:
-            file.close()
-    
-    if as_lines:
-        return lines
-    
-    main_config = _conf_to_dict(lines,
-            list_names=['thread'],
-            no_convert=['@admin_username', '@admin_password', '@normal_username', '@normal_password'])
-    
-    _get_additional_config(main_config)
-    _set_default_motion(main_config, old_motion=is_old_motion())
-    
-    _main_config_cache = main_config
-    
-    return main_config
-
-
-def set_main(main_config):
-    global _main_config_cache
-    
-    main_config = dict(main_config)
-    _set_default_motion(main_config, old_motion=is_old_motion())
-    for n, v in _main_config_cache.iteritems():
-        main_config.setdefault(n, v)
-    _main_config_cache = main_config
-    
-    main_config = dict(main_config)
-    _set_additional_config(main_config)
-
-    config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
-    
-    # read the actual configuration from file
-    lines = get_main(as_lines=True)
-    
-    # write the configuration to file
-    logging.debug('writing main config to %(path)s...' % {'path': config_file_path})
-    
-    try:
-        file = open(config_file_path, 'w')
-    
-    except Exception as e:
-        logging.error('could not open main config file %(path)s for writing: %(msg)s' % {
-                'path': config_file_path, 'msg': unicode(e)})
-        
-        raise
-    
-    lines = _dict_to_conf(lines, main_config, list_names=['thread'])
-    
-    try:
-        file.writelines([utils.make_str(l) + '\n' for l in lines])
-    
-    except Exception as e:
-        logging.error('could not write main config file %(path)s: %(msg)s' % {
-                'path': config_file_path, 'msg': unicode(e)})
-        
-        raise
-    
-    finally:
-        file.close()
-
-
-def get_camera_ids(filter_valid=True):
-    global _camera_ids_cache
-    
-    if _camera_ids_cache is not None:
-        return _camera_ids_cache
-
-    config_path = settings.CONF_PATH
-    
-    logging.debug('listing config dir %(path)s...' % {'path': config_path})
-    
-    try:
-        ls = os.listdir(config_path)
-    
-    except Exception as e:
-        logging.error('failed to list config dir %(path)s: %(msg)s', {
-                'path': config_path, 'msg': unicode(e)})
-        
-        raise
-    
-    camera_ids = []
-    
-    pattern = '^' + _CAMERA_CONFIG_FILE_NAME.replace('%(id)s', '(\d+)') + '$'
-    for name in ls:
-        match = re.match(pattern, name)
-        if match:
-            camera_id = int(match.groups()[0])
-            logging.debug('found camera with id %(id)s' % {
-                    'id': camera_id})
-            
-            camera_ids.append(camera_id)
-        
-    camera_ids.sort()
-    
-    if not filter_valid:
-        return camera_ids
-
-    filtered_camera_ids = []
-    for camera_id in camera_ids:
-        if get_camera(camera_id):
-            filtered_camera_ids.append(camera_id)
-    
-    _camera_ids_cache = filtered_camera_ids
-    
-    return filtered_camera_ids
-
-
-def get_enabled_local_motion_cameras():
-    if not get_main().get('@enabled'):
-        return []
-    
-    camera_ids = get_camera_ids()
-    cameras = [get_camera(camera_id) for camera_id in camera_ids]
-    return [c for c in cameras if c.get('@enabled') and utils.local_motion_camera(c)]
-
-
-def get_network_shares():
-    if not get_main().get('@enabled'):
-        return []
-
-    camera_ids = get_camera_ids()
-    cameras = [get_camera(camera_id) for camera_id in camera_ids]
-    
-    mounts = []
-    for camera in cameras:
-        if camera.get('@storage_device') != 'network-share':
-            continue
-        
-        mounts.append({
-            'server': camera['@network_server'],
-            'share': camera['@network_share_name'],
-            'username': camera['@network_username'],
-            'password': camera['@network_password'],
-        })
-        
-    return mounts
-
-
-def get_camera(camera_id, as_lines=False):
-    global _camera_config_cache
-    
-    if not as_lines and camera_id in _camera_config_cache:
-        return _camera_config_cache[camera_id]
-    
-    camera_config_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
-    
-    logging.debug('reading camera config from %(path)s...' % {'path': camera_config_path})
-    
-    try:
-        file = open(camera_config_path, 'r')
-    
-    except Exception as e:
-        logging.error('could not open camera config file: %(msg)s' % {'msg': unicode(e)})
-        
-        raise
-    
-    try:
-        lines = [l.strip() for l in file.readlines()]
-    
-    except Exception as e:
-        logging.error('could not read camera config file %(path)s: %(msg)s' % {
-                'path': camera_config_path, 'msg': unicode(e)})
-        
-        raise
-    
-    finally:
-        file.close()
-    
-    if as_lines:
-        return lines
-        
-    camera_config = _conf_to_dict(lines,
-            no_convert=['@name', '@network_share_name', '@network_server',
-                        '@network_username', '@network_password', '@storage_device'])
-    
-    if utils.local_motion_camera(camera_config):
-        # determine the enabled status
-        main_config = get_main()
-        threads = main_config.get('thread', [])
-        camera_config['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads
-        camera_config['@id'] = camera_id
-        
-        old_motion = is_old_motion()
-        
-        # adapt directives from old configuration, if needed
-        if old_motion:
-            logging.debug('using old motion config directives')
-            
-            if 'output_normal' in camera_config:
-                camera_config['output_pictures'] = camera_config.pop('output_normal')
-            if 'output_all' in camera_config:
-                camera_config['emulate_motion'] = camera_config.pop('output_all')
-            if 'ffmpeg_cap_new' in camera_config:
-                camera_config['ffmpeg_output_movies'] = camera_config.pop('ffmpeg_cap_new')
-            if 'locate' in camera_config:
-                camera_config['locate_motion_mode'] = camera_config.pop('locate')
-            if 'jpeg_filename' in camera_config:
-                camera_config['picture_filename'] = camera_config.pop('jpeg_filename')
-            if 'max_mpeg_time' in camera_config:
-                camera_config['max_movie_time'] = camera_config.pop('max_mpeg_time')
-            if 'webcam_port' in camera_config:
-                camera_config['stream_port'] = camera_config.pop('webcam_port')
-            if 'webcam_quality' in camera_config:
-                camera_config['stream_quality'] = camera_config.pop('webcam_quality')
-            if 'webcam_motion' in camera_config:
-                camera_config['stream_motion'] = camera_config.pop('webcam_motion')
-            if 'webcam_maxrate' in camera_config:
-                camera_config['stream_maxrate'] = camera_config.pop('webcam_maxrate')
-            if 'webcam_localhost' in camera_config:
-                camera_config['stream_localhost'] = camera_config.pop('webcam_localhost')
-            if 'gap' in camera_config:
-                camera_config['event_gap'] = camera_config.pop('gap')
-            if 'netcam_http' in camera_config:
-                camera_config['netcam_keepalive'] = camera_config.pop('netcam_http') in ['1.1', 'keepalive']
-
-        _get_additional_config(camera_config, camera_id=camera_id)
-        _set_default_motion_camera(camera_id, camera_config)
-    
-    elif utils.remote_camera(camera_config):
-        pass
-    
-    elif utils.simple_mjpeg_camera(camera_config):
-        _get_additional_config(camera_config, camera_id=camera_id)
-    
-    else: # incomplete configuration
-        logging.warn('camera config file at %s is incomplete, ignoring' % camera_config_path)
-        
-        return None
-    
-    _camera_config_cache[camera_id] = dict(camera_config)
-    
-    return camera_config
-
-
-def set_camera(camera_id, camera_config):
-    global _camera_config_cache
-
-    camera_config['@id'] = camera_id
-    _camera_config_cache[camera_id] = camera_config
-
-    camera_config = dict(camera_config)
-    
-    if utils.local_motion_camera(camera_config):
-        old_motion = is_old_motion()
-        
-        # adapt directives to old configuration, if needed
-        if old_motion:
-            logging.debug('using old motion config directives')
-            
-            if 'output_pictures' in camera_config:
-                camera_config['output_normal'] = camera_config.pop('output_pictures')
-            if 'emulate_motion' in camera_config:
-                camera_config['output_all'] = camera_config.pop('emulate_motion')
-            if 'ffmpeg_output_movies' in camera_config:
-                camera_config['ffmpeg_cap_new'] = camera_config.pop('ffmpeg_output_movies')
-            if 'locate_motion_mode' in camera_config:
-                camera_config['locate'] = camera_config.pop('locate_motion_mode')
-            if 'picture_filename' in camera_config:
-                camera_config['jpeg_filename'] = camera_config.pop('picture_filename')
-            if 'max_movie_time' in camera_config:
-                camera_config['max_mpeg_time'] = camera_config.pop('max_movie_time')
-            if 'stream_port' in camera_config:
-                camera_config['webcam_port'] = camera_config.pop('stream_port')
-            if 'stream_quality' in camera_config:
-                camera_config['webcam_quality'] = camera_config.pop('stream_quality')
-            if 'stream_motion' in camera_config:
-                camera_config['webcam_motion'] = camera_config.pop('stream_motion')
-            if 'stream_maxrate' in camera_config:
-                camera_config['webcam_maxrate'] = camera_config.pop('stream_maxrate')
-            if 'stream_localhost' in camera_config:
-                camera_config['webcam_localhost'] = camera_config.pop('stream_localhost')
-            if 'stream_auth_method' in camera_config:
-                camera_config.pop('stream_auth_method')
-            if 'stream_authentication' in camera_config:
-                camera_config.pop('stream_authentication')
-            if 'event_gap' in camera_config:
-                camera_config['gap'] = camera_config.pop('event_gap')
-            if 'netcam_keepalive' in camera_config:
-                camera_config['netcam_http'] = '1.1' if camera_config.pop('netcam_keepalive') else '1.0'
-                
-        _set_default_motion_camera(camera_id, camera_config, old_motion)
-        
-        # set the enabled status in main config
-        main_config = get_main()
-        threads = main_config.setdefault('thread', [])
-        config_file_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
-        if camera_config['@enabled'] and config_file_name not in threads:
-            threads.append(config_file_name)
-                
-        elif not camera_config['@enabled']:
-            threads = [t for t in threads if t != config_file_name]
-
-        main_config['thread'] = threads
-        
-        set_main(main_config)
-        _set_additional_config(camera_config, camera_id=camera_id)
-
-    elif utils.remote_camera(camera_config):
-        pass
-    
-    elif utils.simple_mjpeg_camera(camera_config):
-        _set_additional_config(camera_config, camera_id=camera_id)
-
-    # read the actual configuration from file
-    config_file_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
-    if os.path.isfile(config_file_path):
-        lines = get_camera(camera_id, as_lines=True)
-    
-    else:
-        lines = []
-    
-    # write the configuration to file
-    camera_config_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
-    logging.debug('writing camera config to %(path)s...' % {'path': camera_config_path})
-    
-    try:
-        file = open(camera_config_path, 'w')
-    
-    except Exception as e:
-        logging.error('could not open camera config file %(path)s for writing: %(msg)s' % {
-                'path': camera_config_path, 'msg': unicode(e)})
-        
-        raise
-    
-    lines = _dict_to_conf(lines, camera_config)
-    
-    try:
-        file.writelines([utils.make_str(l) + '\n' for l in lines])
-    
-    except Exception as e:
-        logging.error('could not write camera config file %(path)s: %(msg)s' % {
-                'path': camera_config_path, 'msg': unicode(e)})
-        
-        raise
-    
-    finally:
-        file.close()
-        
-
-def add_camera(device_details):
-    global _camera_ids_cache
-    global _camera_config_cache
-    
-    proto = device_details['proto']
-    if proto in ['netcam', 'mjpeg']:
-        host = device_details['host']
-        if device_details['port']:
-            host += ':' + str(device_details['port'])
-
-        if device_details['username'] and proto == 'mjpeg':
-            if device_details['password']:
-                host = device_details['username'] + ':' + device_details['password'] + '@' + host
-                
-            else:
-                host = device_details['username'] + '@' + host
-
-        device_details['url'] = urlparse.urlunparse((device_details['scheme'], host, device_details['uri'], '', '', ''))
-
-    # determine the last camera id
-    camera_ids = get_camera_ids()
-
-    camera_id = 1
-    while camera_id in camera_ids:
-        camera_id += 1
-    
-    logging.info('adding new camera with id %(id)s...' % {'id': camera_id})
-    
-    # prepare a default camera config
-    camera_config = {'@enabled': True}
-    if proto == 'v4l2':
-        # find a suitable resolution
-        for (w, h) in v4l2ctl.list_resolutions(device_details['uri']):
-            if w > 300:
-                camera_config['width'] = w
-                camera_config['height'] = h
-                break
-
-        camera_config['videodevice'] = device_details['uri']
-        _set_default_motion_camera(camera_id, camera_config)
-    
-    elif proto == 'motioneye':
-        camera_config['@proto'] = 'motioneye'
-        camera_config['@scheme'] = device_details['scheme']
-        camera_config['@host'] = device_details['host']
-        camera_config['@port'] = device_details['port']
-        camera_config['@uri'] = device_details['uri']
-        camera_config['@username'] = device_details['username']
-        camera_config['@password'] = device_details['password']
-        camera_config['@remote_camera_id'] = device_details['remote_camera_id']
-
-    elif proto == 'netcam':
-        camera_config['netcam_url'] = device_details['url']
-        camera_config['text_double'] = True
-        
-        if device_details['username']:
-            camera_config['netcam_userpass'] = device_details['username'] + ':' + device_details['password']
-        
-        camera_config['netcam_keepalive'] = device_details.get('keep_alive')
-        camera_config['netcam_tolerant_check'] = True
-
-        if device_details.get('camera_index') == 'udp':
-            camera_config['rtsp_uses_tcp'] = False
-        
-        if camera_config['netcam_url'].startswith('rtsp'):
-            camera_config['width'] = 640
-            camera_config['height'] = 480
-
-        _set_default_motion_camera(camera_id, camera_config)
-
-    else: # assuming mjpeg
-        camera_config['@proto'] = 'mjpeg'
-        camera_config['@url'] = device_details['url']
-        _set_default_simple_mjpeg_camera(camera_id, camera_config)
-
-    # write the configuration to file
-    set_camera(camera_id, camera_config)
-    
-    _camera_ids_cache = None
-    _camera_config_cache = {}
-    
-    camera_config = get_camera(camera_id)
-    
-    return camera_config
-
-
-def rem_camera(camera_id):
-    global _camera_ids_cache
-    global _camera_config_cache
-    
-    camera_config_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
-    camera_config_path = os.path.join(settings.CONF_PATH, _CAMERA_CONFIG_FILE_NAME) % {'id': camera_id}
-    
-    # remove the camera from the main config
-    main_config = get_main()
-    threads = main_config.setdefault('thread', [])
-    threads = [t for t in threads if t != camera_config_name]
-    
-    main_config['thread'] = threads
-
-    set_main(main_config)
-    
-    logging.info('removing camera config file %(path)s...' % {'path': camera_config_path})
-    
-    _camera_ids_cache = None
-    _camera_config_cache = {}
-    
-    try:
-        os.remove(camera_config_path)
-    
-    except Exception as e:
-        logging.error('could not remove camera config file %(path)s: %(msg)s' % {
-                'path': camera_config_path, 'msg': unicode(e)})
-        
-        raise
-
-
-def main_ui_to_dict(ui):
-    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']
-    }
-
-    # additional configs
-    for name, value in ui.iteritems():
-        if not name.startswith('_'):
-            continue
-
-        data['@' + name] = value
-
-    return data
-
-
-def main_dict_to_ui(data):
-    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']
-    }
-
-    # additional configs
-    for name, value in data.iteritems():
-        if not name.startswith('@_'):
-            continue
-        
-        ui[name[1:]] = value
-
-    return ui
-
-
-def motion_camera_ui_to_dict(ui, old_config=None):
-    import smbctl
-    
-    old_config = dict(old_config or {})
-    main_config = get_main() # needed for surveillance password
-
-    data = {
-        # device
-        '@name': ui['name'],
-        '@enabled': ui['enabled'],
-        'lightswitch': int(ui['light_switch_detect']) * 50,
-        'auto_brightness': ui['auto_brightness'],
-        'framerate': int(ui['framerate']),
-        'rotate': int(ui['rotation']),
-        
-        # file storage
-        '@storage_device': ui['storage_device'],
-        '@network_server': ui['network_server'],
-        '@network_share_name': ui['network_share_name'],
-        '@network_username': ui['network_username'],
-        '@network_password': ui['network_password'],
-        
-        # text overlay
-        'text_left': '',
-        'text_right': '',
-        'text_double': False,
-        
-        # streaming
-        'stream_localhost': not ui['video_streaming'],
-        'stream_port': int(ui['streaming_port']),
-        'stream_maxrate': int(ui['streaming_framerate']),
-        'stream_quality': max(1, int(ui['streaming_quality'])),
-        '@webcam_resolution': max(1, int(ui['streaming_resolution'])),
-        '@webcam_server_resize': ui['streaming_server_resize'],
-        'stream_motion': ui['streaming_motion'],
-        'stream_auth_method': {'disabled': 0, 'basic': 1, 'digest': 2}.get(ui['streaming_auth_mode'], 0),
-        'stream_authentication': main_config['@normal_username'] + ':' + main_config['@normal_password'],
-
-        # still images
-        'output_pictures': False,
-        'emulate_motion': False,
-        'snapshot_interval': 0,
-        'picture_filename': '',
-        'snapshot_filename': '',
-        '@preserve_pictures': int(ui['preserve_pictures']),
-        
-        # motion detection
-        '@motion_detection': ui['motion_detection'],
-        'text_changes': ui['show_frame_changes'],
-        'locate_motion_mode': ui['show_frame_changes'],
-        'noise_tune': ui['auto_noise_detect'],
-        'noise_level': max(1, int(round(int(ui['noise_level']) * 2.55))),
-        'event_gap': int(ui['event_gap']),
-        'pre_capture': int(ui['pre_capture']),
-        'post_capture': int(ui['post_capture']),
-        'minimum_motion_frames': int(ui['minimum_motion_frames']),
-        
-        # movies
-        'ffmpeg_output_movies': ui['motion_movies'],
-        'movie_filename': ui['movie_file_name'],
-        'ffmpeg_bps': 44000, # a quality of about 85% for 320x240x2fps
-        'max_movie_time': ui['max_movie_length'],
-        '@preserve_movies': int(ui['preserve_movies']),
-    
-        # working schedule
-        '@working_schedule': '',
-    
-        # events
-        'on_event_start': '',
-        'on_event_end': ''
-    }
-    
-    if utils.v4l2_camera(old_config):
-        proto = 'v4l2'
-        
-    else:
-        proto = 'netcam'
-    
-    if proto == 'v4l2':
-        # leave videodevice unchanged
-        
-        # resolution
-        if not ui['resolution']:
-            ui['resolution'] = '320x240'
-
-        width = int(ui['resolution'].split('x')[0])
-        height = int(ui['resolution'].split('x')[1])
-        data['width'] = width
-        data['height'] = height
-        
-        threshold = int(float(ui['frame_change_threshold']) * width * height / 100)
-
-        if 'brightness' in ui:
-            if int(ui['brightness']) == 50:
-                data['brightness'] = 0
-                
-            else:
-                data['brightness'] = max(1, int(round(int(ui['brightness']) * 2.55)))
-        
-        if 'contrast' in ui:
-            if int(ui['contrast']) == 50:
-                data['contrast'] = 0
-                
-            else:
-                data['contrast'] = max(1, int(round(int(ui['contrast']) * 2.55)))
-        
-        if 'saturation' in ui:
-            if int(ui['saturation']) == 50:
-                data['saturation'] = 0
-                
-            else:
-                data['saturation'] = max(1, int(round(int(ui['saturation']) * 2.55)))
-            
-        if 'hue' in ui:
-            if int(ui['hue']) == 50:
-                data['hue'] = 0
-                
-            else:
-                data['hue'] = max(1, int(round(int(ui['hue']) * 2.55)))
-    
-    else: # assuming netcam
-        if data.get('netcam_url', old_config.get('netcam_url', '')).startswith('rtsp'):
-            # motion uses the configured width and height for RTSP cameras
-            width = int(ui['resolution'].split('x')[0])
-            height = int(ui['resolution'].split('x')[1])
-            data['width'] = width
-            data['height'] = height
-            
-            threshold = int(float(ui['frame_change_threshold']) * width * height / 100)
-        
-        else: # width & height are not available for other netcams
-            threshold = int(float(ui['frame_change_threshold']) * 640 * 480 / 100)
-
-    data['threshold'] = threshold
-
-    if (ui['storage_device'] == 'network-share') and settings.SMB_SHARES:
-        mount_point = smbctl.make_mount_point(ui['network_server'], ui['network_share_name'], ui['network_username'])
-        if ui['root_directory'].startswith('/'):
-            ui['root_directory'] = ui['root_directory'][1:]
-        data['target_dir'] = os.path.normpath(os.path.join(mount_point, ui['root_directory']))
-    
-    elif ui['storage_device'].startswith('local-disk'):
-        target_dev = ui['storage_device'][10:].replace('-', '/')
-        mounted_partitions = diskctl.list_mounted_partitions()
-        partition = mounted_partitions[target_dev]
-        mount_point = partition['mount_point']
-        
-        if ui['root_directory'].startswith('/'):
-            ui['root_directory'] = ui['root_directory'][1:]
-        data['target_dir'] = os.path.normpath(os.path.join(mount_point, ui['root_directory']))
-
-    else:
-        data['target_dir'] = ui['root_directory']
-
-    if ui['text_overlay']:
-        left_text = ui['left_text']
-        if left_text == 'camera-name':
-            data['text_left'] = ui['name']
-            
-        elif left_text == 'timestamp':
-            data['text_left'] = '%Y-%m-%d\\n%T'
-            
-        elif left_text == 'disabled':
-            data['text_left'] = ''
-            
-        else:
-            data['text_left'] = ui['custom_left_text']
-        
-        right_text = ui['right_text']
-        if right_text == 'camera-name':
-            data['text_right'] = ui['name']
-            
-        elif right_text == 'timestamp':
-            data['text_right'] = '%Y-%m-%d\\n%T'
-            
-        elif right_text == 'disabled':
-            data['text_right'] = ''
-            
-        else:
-            data['text_right'] = ui['custom_right_text']
-        
-        if proto == 'netcam' or data['width'] > 320:
-            data['text_double'] = True
-    
-    if ui['still_images']:
-        capture_mode = ui['capture_mode']
-        if capture_mode == 'motion-triggered':
-            data['output_pictures'] = True
-            data['picture_filename'] = ui['image_file_name']  
-            
-        elif capture_mode == 'interval-snapshots':
-            data['snapshot_interval'] = int(ui['snapshot_interval'])
-            data['snapshot_filename'] = ui['image_file_name']
-            
-        elif capture_mode == 'all-frames':
-            data['output_pictures'] = True
-            data['emulate_motion'] = True
-            data['picture_filename'] = ui['image_file_name']
-            
-        data['quality'] = max(1, int(ui['image_quality']))
-    
-    if proto == 'v4l2':
-        max_val = data['width'] * data['height'] * data['framerate'] / 3
-    
-    else: # always assume a netcam image size of 640x480, since we have no means to know it at this point
-        max_val = 640 * 480 * data['framerate'] / 3
-        
-    max_val = min(max_val, 9999999)
-    
-    data['ffmpeg_bps'] = int(ui['movie_quality']) * max_val / 100
-    
-    # working schedule
-    if ui['working_schedule']:
-        data['@working_schedule'] = (
-                ui['monday_from'] + '-' + ui['monday_to'] + '|' + 
-                ui['tuesday_from'] + '-' + ui['tuesday_to'] + '|' + 
-                ui['wednesday_from'] + '-' + ui['wednesday_to'] + '|' + 
-                ui['thursday_from'] + '-' + ui['thursday_to'] + '|' + 
-                ui['friday_from'] + '-' + ui['friday_to'] + '|' + 
-                ui['saturday_from'] + '-' + ui['saturday_to'] + '|' + 
-                ui['sunday_from'] + '-' + ui['sunday_to'])
-        
-        data['@working_schedule_type'] = ui['working_schedule_type']
-    
-    # event start
-    event_relay_path = os.path.join(settings.PROJECT_PATH, 'eventrelay.py')
-    event_relay_path = os.path.abspath(event_relay_path)
-        
-    on_event_start = ['%(script)s start %%t' % {'script': event_relay_path}]
-    if ui['email_notifications_enabled']:
-        send_mail_path = os.path.join(settings.PROJECT_PATH, 'sendmail.py')
-        send_mail_path = os.path.abspath(send_mail_path)
-        emails = re.sub('\\s', '', ui['email_notifications_addresses'])
-        
-        on_event_start.append("%(script)s '%(server)s' '%(port)s' '%(account)s' '%(password)s' '%(tls)s' '%(to)s' 'motion_start' '%%t' '%%Y-%%m-%%dT%%H:%%M:%%S' '%(timespan)s'" % {
-                'script': send_mail_path,
-                'server': ui['email_notifications_smtp_server'],
-                'port': ui['email_notifications_smtp_port'],
-                'account': ui['email_notifications_smtp_account'],
-                'password': ui['email_notifications_smtp_password'],
-                'tls': ui['email_notifications_smtp_tls'],
-                'to': emails,
-                'timespan': ui['email_notifications_picture_time_span']})
-
-    if ui['web_hook_notifications_enabled']:
-        web_hook_path = os.path.join(settings.PROJECT_PATH, 'webhook.py')
-        web_hook_path = os.path.abspath(web_hook_path)
-        url = re.sub('\\s', '+', ui['web_hook_notifications_url'])
-
-        on_event_start.append("%(script)s '%(method)s' '%(url)s'" % {
-                'script': web_hook_path,
-                'method': ui['web_hook_notifications_http_method'],
-                'url': url})
-
-    if ui['command_notifications_enabled']:
-        commands = ui['command_notifications_exec'].split(';')
-        on_event_start += [c.strip() for c in commands]
-
-    data['on_event_start'] = '; '.join(on_event_start)
-
-    # event end
-    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
-        
-    # extra motion options
-    for name in old_config.keys():
-        if name not in _KNOWN_MOTION_OPTIONS and not name.startswith('@'):
-            old_config.pop(name)
-
-    extra_options = ui.get('extra_options', [])
-    for name, value in extra_options:
-        data[name] = value or ''
-
-    old_config.update(data)
-
-    return old_config
-
-
-def motion_camera_dict_to_ui(data):
-    import smbctl
-    
-    ui = {
-        # device
-        'name': data['@name'],
-        'enabled': data['@enabled'],
-        'id': data['@id'],
-        'light_switch_detect': data['lightswitch'] > 0,
-        'auto_brightness': data['auto_brightness'],
-        'framerate': int(data['framerate']),
-        'rotation': int(data['rotate']),
-        
-        # file storage
-        'smb_shares': settings.SMB_SHARES,
-        'storage_device': data['@storage_device'],
-        'network_server': data['@network_server'],
-        'network_share_name': data['@network_share_name'],
-        'network_username': data['@network_username'],
-        'network_password': data['@network_password'],
-        'disk_used': 0,
-        'disk_total': 0,
-        'available_disks': diskctl.list_mounted_disks(),
-
-        # text overlay
-        'text_overlay': False,
-        'left_text': 'camera-name',
-        'right_text': 'timestamp',
-        'custom_left_text': '',
-        'custom_right_text': '',
-        
-        # streaming
-        'video_streaming': not data['stream_localhost'],
-        'streaming_framerate': int(data['stream_maxrate']),
-        'streaming_quality': int(data['stream_quality']),
-        'streaming_resolution': int(data['@webcam_resolution']),
-        'streaming_server_resize': data['@webcam_server_resize'],
-        'streaming_port': int(data['stream_port']),
-        'streaming_auth_mode': {0: 'disabled', 1: 'basic', 2: 'digest'}.get(data.get('stream_auth_method'), 'disabled'),
-        'streaming_motion': int(data['stream_motion']),
-        
-        # still images
-        'still_images': False,
-        'capture_mode': 'motion-triggered',
-        'image_file_name': '%Y-%m-%d/%H-%M-%S',
-        'image_quality': 85,
-        'snapshot_interval': 0,
-        'preserve_pictures': data['@preserve_pictures'],
-        
-        # motion detection
-        'motion_detection': data['@motion_detection'],
-        'show_frame_changes': data['text_changes'] or data['locate_motion_mode'],
-        'auto_noise_detect': data['noise_tune'],
-        'noise_level': int(int(data['noise_level']) / 2.55),
-        'event_gap': int(data['event_gap']),
-        'pre_capture': int(data['pre_capture']),
-        'post_capture': int(data['post_capture']),
-        'minimum_motion_frames': int(data['minimum_motion_frames']),
-        
-        # motion movies
-        'motion_movies': data['ffmpeg_output_movies'],
-        'movie_file_name': data['movie_filename'],
-        'max_movie_length': data['max_movie_time'],
-        'preserve_movies': data['@preserve_movies'],
-
-        # motion notifications
-        'email_notifications_enabled': False,
-        'web_hook_notifications_enabled': False,
-        'command_notifications_enabled': False,
-        
-        # working schedule
-        'working_schedule': False,
-        'working_schedule_type': 'during',
-        'monday_from': '', 'monday_to': '',
-        'tuesday_from': '', 'tuesday_to': '',
-        'wednesday_from': '', 'wednesday_to': '',
-        'thursday_from': '', 'thursday_to': '',
-        'friday_from': '', 'friday_to': '',
-        'saturday_from': '', 'saturday_to': '',
-        'sunday_from': '', 'sunday_to': ''
-    }
-    
-    if utils.net_camera(data):
-        ui['device_url'] = data['netcam_url']
-        ui['proto'] = 'netcam'
-
-        # resolutions
-        if data['netcam_url'].startswith('rtsp'):
-            # motion uses the configured width and height for RTSP cameras
-            resolutions = utils.COMMON_RESOLUTIONS
-            ui['available_resolutions'] = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
-            ui['resolution'] = str(data['width']) + 'x' + str(data['height'])
-
-            threshold = data['threshold'] * 100.0 / (data['width'] * data['height'])
-
-        else: # width & height are not available for other netcams
-            # we have no other choice but use something like 640x480 as reference
-            threshold = data['threshold'] * 100.0 / (640 * 480)
-
-    else: # assuming v4l2
-        ui['device_url'] = data['videodevice']
-        ui['proto'] = 'v4l2'
-
-        # resolutions
-        resolutions = v4l2ctl.list_resolutions(data['videodevice'])
-        ui['available_resolutions'] = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
-        ui['resolution'] = str(data['width']) + 'x' + str(data['height'])
-    
-        # the brightness & co. keys in the ui dictionary
-        # indicate the presence of these controls
-        # we must call v4l2ctl functions to determine the available controls    
-        brightness = v4l2ctl.get_brightness(data['videodevice'])
-        if brightness is not None: # has brightness control
-            if data.get('brightness', 0) != 0:
-                ui['brightness'] = brightness
-                    
-            else:
-                ui['brightness'] = 50
-
-        contrast = v4l2ctl.get_contrast(data['videodevice'])
-        if contrast is not None: # has contrast control
-            if data.get('contrast', 0) != 0:
-                ui['contrast'] = contrast
-            
-            else:
-                ui['contrast'] = 50
-            
-        saturation = v4l2ctl.get_saturation(data['videodevice'])
-        if saturation is not None: # has saturation control
-            if data.get('saturation', 0) != 0:
-                ui['saturation'] = saturation
-            
-            else:
-                ui['saturation'] = 50
-            
-        hue = v4l2ctl.get_hue(data['videodevice'])
-        if hue is not None: # has hue control
-            if data.get('hue', 0) != 0:
-                ui['hue'] = hue
-            
-            else:
-                ui['hue'] = 50
-        
-        threshold = data['threshold'] * 100.0 / (data['width'] * data['height'])
-        
-    ui['frame_change_threshold'] = threshold
-    
-    if (data['@storage_device'] == 'network-share') and settings.SMB_SHARES:
-        mount_point = smbctl.make_mount_point(data['@network_server'], data['@network_share_name'], data['@network_username'])
-        ui['root_directory'] = data['target_dir'][len(mount_point):] or '/'
-    
-    elif data['@storage_device'].startswith('local-disk'):
-        target_dev = data['@storage_device'][10:].replace('-', '/')
-        mounted_partitions = diskctl.list_mounted_partitions()
-        for partition in mounted_partitions.values():
-            if partition['target'] == target_dev and data['target_dir'].startswith(partition['mount_point']):
-                ui['root_directory'] = data['target_dir'][len(partition['mount_point']):] or '/'
-                break
-
-        else: # not found for some reason
-            logging.error('could not find mounted partition for device "%s" and target dir "%s"' % (target_dev, data['target_dir']))
-            ui['root_directory'] = data['target_dir']
-
-    else:
-        ui['root_directory'] = data['target_dir']
-
-    # disk usage
-    usage = utils.get_disk_usage(data['target_dir'])
-    if usage:
-        ui['disk_used'], ui['disk_total'] = usage
-    
-    text_left = data['text_left']
-    text_right = data['text_right'] 
-    if text_left or text_right:
-        ui['text_overlay'] = True
-        
-        if text_left == data['@name']:
-            ui['left_text'] = 'camera-name'
-            
-        elif text_left == '%Y-%m-%d\\n%T':
-            ui['left_text'] = 'timestamp'
-
-        elif text_left == '':
-            ui['left_text'] = 'disabled'
-            
-        else:
-            ui['left_text'] = 'custom-text'
-            ui['custom_left_text'] = text_left
-
-        if text_right == data['@name']:
-            ui['right_text'] = 'camera-name'
-            
-        elif text_right == '%Y-%m-%d\\n%T':
-            ui['right_text'] = 'timestamp'
-            
-        elif text_right == '':
-            ui['right_text'] = 'disabled'
-
-        else:
-            ui['right_text'] = 'custom-text'
-            ui['custom_right_text'] = text_right
-
-    emulate_motion = data['emulate_motion']
-    output_pictures = data['output_pictures']
-    picture_filename = data['picture_filename']
-    snapshot_interval = data['snapshot_interval']
-    snapshot_filename = data['snapshot_filename']
-    
-    if (((emulate_motion or output_pictures) and picture_filename) or
-        (snapshot_interval and snapshot_filename)):
-        
-        ui['still_images'] = True
-        
-        if emulate_motion:
-            ui['capture_mode'] = 'all-frames'
-            ui['image_file_name'] = picture_filename
-            
-        elif snapshot_interval:
-            ui['capture_mode'] = 'interval-snapshots'
-            ui['image_file_name'] = snapshot_filename
-            ui['snapshot_interval'] = snapshot_interval
-            
-        elif output_pictures:
-            ui['capture_mode'] = 'motion-triggered'
-            ui['image_file_name'] = picture_filename  
-            
-        ui['image_quality'] = data['quality']
-
-    ffmpeg_bps = data['ffmpeg_bps']
-    if ffmpeg_bps is not None:
-        if utils.v4l2_camera(data):
-            max_val = data['width'] * data['height'] * data['framerate'] / 3
-        
-        else: # net camera
-            max_val = 640 * 480 * data['framerate'] / 3
-            
-        max_val = min(max_val, 9999999)
-        
-        ui['movie_quality'] = min(100, int(round(ffmpeg_bps * 100.0 / max_val)))
-
-    # working schedule
-    working_schedule = data['@working_schedule']
-    if working_schedule:
-        days = working_schedule.split('|')
-        ui['working_schedule'] = True
-        ui['monday_from'], ui['monday_to'] = days[0].split('-')
-        ui['tuesday_from'], ui['tuesday_to'] = days[1].split('-')
-        ui['wednesday_from'], ui['wednesday_to'] = days[2].split('-')
-        ui['thursday_from'], ui['thursday_to'] = days[3].split('-')
-        ui['friday_from'], ui['friday_to'] = days[4].split('-')
-        ui['saturday_from'], ui['saturday_to'] = days[5].split('-')
-        ui['sunday_from'], ui['sunday_to'] = days[6].split('-')
-        ui['working_schedule_type'] = data['@working_schedule_type']
-    
-    # event start    
-    on_event_start = data.get('on_event_start') or []
-    if on_event_start:
-        on_event_start = [e.strip() for e in on_event_start.split(';')]
-
-    ui['email_notifications_picture_time_span'] = 0
-    command_notifications = []
-    for e in on_event_start:
-        if e.count('sendmail.py') and e.count('motion_start'):
-            e = shlex.split(e)
-            if len(e) < 10:
-                continue
-
-            ui['email_notifications_enabled'] = True 
-            ui['email_notifications_smtp_server'] = e[1]
-            ui['email_notifications_smtp_port'] = e[2]
-            ui['email_notifications_smtp_account'] = e[3]
-            ui['email_notifications_smtp_password'] = e[4]
-            ui['email_notifications_smtp_tls'] = e[5].lower() == 'true'
-            ui['email_notifications_addresses'] = e[6]
-            try:
-                ui['email_notifications_picture_time_span'] = int(e[10])
-                
-            except:
-                ui['email_notifications_picture_time_span'] = 0
-
-        elif e.count('webhook.py'):
-            e = shlex.split(e)
-            if len(e) != 3:
-                continue
-
-            ui['web_hook_notifications_enabled'] = True 
-            ui['web_hook_notifications_http_method'] = e[1]
-            ui['web_hook_notifications_url'] = e[2]
-        
-        elif e.count('eventrelay.py'):
-            continue # ignore internal relay script
-
-        else: # custom command
-            command_notifications.append(e)
-    
-    if command_notifications: 
-        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
-    
-    # extra motion options
-    extra_options = []
-    for name, value in data.iteritems():
-        if name not in _KNOWN_MOTION_OPTIONS and not name.startswith('@'):
-            extra_options.append((name, value))
-
-    ui['extra_options'] = extra_options
-
-    return ui
-
-
-def simple_mjpeg_camera_ui_to_dict(ui, old_config=None):
-    old_config = dict(old_config or {})
-
-    data = {
-        # device
-        '@name': ui['name'],
-        '@enabled': ui['enabled'],
-    }
-    
-    # additional configs
-    for name, value in ui.iteritems():
-        if not name.startswith('_'):
-            continue
-
-        data['@' + name] = value
-        
-    old_config.update(data)
-
-    return old_config
-
-
-def simple_mjpeg_camera_dict_to_ui(data):
-    ui = {
-        'name': data['@name'],
-        'enabled': data['@enabled'],
-        'id': data['@id'],
-        'proto': 'mjpeg',
-        'url': data['@url']
-    }
-    
-    # additional configs
-    for name, value in data.iteritems():
-        if not name.startswith('@_'):
-            continue
-        
-        ui[name[1:]] = value
-    
-    return ui
-
-
-def backup():
-    logging.debug('generating config backup file')
-
-    if len(os.listdir(settings.CONF_PATH)) > 100:
-        logging.debug('config path "%s" appears to be a system-wide config directory, performing a selective backup' % settings.CONF_PATH)
-        cmd = 'cd "%s" && tar zc motion.conf thread-*.conf' % settings.CONF_PATH
-        try:
-            content = subprocess.check_output(cmd, shell=True)
-            logging.debug('backup file created (%s bytes)' % len(content))
-            
-            return content
-            
-        except Exception as e:
-            logging.error('backup failed: %s' % e, exc_info=True)
-            
-            return None
-
-    else:
-        logging.debug('config path "%s" appears to be a motion-specific config directory, performing a full backup' % settings.CONF_PATH)
-
-        cmd = 'cd "%s" && tar zc .' % settings.CONF_PATH
-        try:
-            content = subprocess.check_output(cmd, shell=True)
-            logging.debug('backup file created (%s bytes)' % len(content))
-            
-            return content
-            
-        except Exception as e:
-            logging.error('backup failed: %s' % e, exc_info=True)
-            
-            return None
-
-
-def restore(content):
-    global _main_config_cache
-    global _camera_config_cache
-    global _camera_ids_cache
-    global _additional_structure_cache
-    
-    logging.info('restoring config from backup file')
-
-    cmd = 'tar zxC "%s" || true' % settings.CONF_PATH
-
-    try:
-        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        msg = p.communicate(content)[0]
-        if msg:
-            logging.error('failed to restore configuration: %s' % msg)
-            return False
-
-        logging.debug('configuration restored successfully')
-
-        if settings.ENABLE_REBOOT:
-            def later():
-                powerctl.reboot()
-
-            IOLoop.instance().add_timeout(datetime.timedelta(seconds=2), later)
-
-        else:
-            logging.info('invalidating config cache')
-            invalidate()
-
-        return {'reboot': settings.ENABLE_REBOOT}
-
-    except Exception as e:
-        logging.error('failed to restore configuration: %s' % e, exc_info=True)
-
-        return None
-
-
-def is_old_motion():
-    import motionctl
-    
-    try:
-        binary, version = motionctl.find_motion()  # @UnusedVariable
-        
-        if version.startswith('trunkREV'): # e.g. trunkREV599
-            version = int(version[8:])
-            return version <= _LAST_OLD_CONFIG_VERSIONS[0]
-        
-        elif version.count('Git'): # e.g. Unofficial-Git-a5b5f13
-            return False # all git versions are assumed to be new
-        
-        else: # stable release, should be in the format x.y.z
-            return update.compare_versions(version, _LAST_OLD_CONFIG_VERSIONS[1]) <= 0
-
-    except:
-        return False
-
-
-def motion_rtsp_support():
-    import motionctl
-
-    try:
-        binary, version = motionctl.find_motion()  # @UnusedVariable
-        
-        if version.startswith('trunkREV'): # e.g. trunkREV599
-            version = int(version[8:])
-            if version > _LAST_OLD_CONFIG_VERSIONS[0]:
-                return ['tcp']
-        
-        elif version.count('Git'): # e.g. Unofficial-Git-a5b5f13
-            return ['tcp', 'udp'] # all git versions are assumed to support both transport protocols
-        
-        else: # stable release, should be in the format x.y.z
-            return []
-
-    except:
-        return []
-
-
-def invalidate():
-    global _main_config_cache
-    global _camera_config_cache
-    global _camera_ids_cache
-    global _additional_structure_cache
-
-    logging.debug('invalidating config cache')    
-    _main_config_cache = None
-    _camera_config_cache = {}
-    _camera_ids_cache = None
-    _additional_structure_cache = {}
-
-
-def _value_to_python(value):
-    value_lower = value.lower()
-    if value_lower == 'off':
-        return False
-    
-    elif value_lower == 'on':
-        return True
-    
-    try:
-        return int(value)
-    
-    except ValueError:
-        try:
-            return float(value)
-        
-        except ValueError:
-            return value
-
-
-def _python_to_value(value):
-    if value is True:
-        return 'on'
-    
-    elif value is False:
-        return 'off'
-    
-    elif isinstance(value, (int, float)):
-        return str(value)
-    
-    else:
-        return value
-
-
-def _conf_to_dict(lines, list_names=[], no_convert=[]):
-    data = OrderedDict()
-    
-    for line in lines:
-        line = line.strip()
-        if len(line) == 0:  # empty line
-            continue
-        
-        if line.startswith(';'):  # comment line
-            continue
-        
-        match = re.match('^\#\s*(\@\w+)\s*(.*)', line)
-        if match:
-            name, value = match.groups()[:2]
-        
-        elif line.startswith('#') or line.startswith(';'): # comment line
-            continue
-
-        else:
-            parts = line.split(None, 1)
-            if len(parts) == 1: # empty value
-                parts.append('')
-
-            (name, value) = parts
-            value = value.strip()
-        
-        if name not in no_convert:
-            value = _value_to_python(value)
-        
-        if name in list_names:
-            data.setdefault(name, []).append(value)
-        
-        else:
-            data[name] = value
-
-    return data
-
-
-def _dict_to_conf(lines, data, list_names=[]):
-    conf_lines = []
-    remaining = OrderedDict(data)
-    processed = set()
-    
-    # parse existing lines and replace the values
-    
-    for line in lines:
-        line = line.strip()
-        if len(line) == 0:  # empty line
-            conf_lines.append(line)
-            continue
-
-        if line.startswith(';'):  # simple comment line
-            conf_lines.append(line)
-            continue
-        
-        match = re.match('^\#\s*(\@\w+)\s*(.*)', line)
-        if match: # @line
-            (name, value) = match.groups()[:2]
-        
-        elif line.startswith('#'):  # simple comment line
-            conf_lines.append(line)
-            continue
-        
-        else:
-            parts = line.split(None, 1)
-            if len(parts) == 2:
-                (name, value) = parts
-            
-            else:
-                (name, value) = parts[0], ''
-        
-        if name in processed:
-            continue # name already processed
-        
-        processed.add(name)
-        
-        if name in list_names:
-            new_value = data.get(name)
-            if new_value is not None:
-                for v in new_value:
-                    if v is None:
-                        continue
-
-                    line = name + ' ' + _python_to_value(v)
-                    conf_lines.append(line)
-            
-            else:
-                line = name + ' ' + value
-                conf_lines.append(line)
-
-        else:
-            new_value = data.get(name)
-            if new_value is not None:
-                value = _python_to_value(new_value)
-                line = name + ' ' + value
-                conf_lines.append(line)
-
-        remaining.pop(name, None)
-    
-    # add the remaining config values not covered by existing lines
-    
-    if len(remaining) and len(lines):
-        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:
-                if v is None:
-                    continue
-
-                line = name + ' ' + _python_to_value(v)
-                conf_lines.append(line)
-
-        else:
-            line = name + ' ' + _python_to_value(value)
-            conf_lines.append(line)
-            
-    # build the final config lines
-    conf_lines.sort(key=lambda l: not l.startswith('@'))
-    
-    lines = []
-    for i, line in enumerate(conf_lines):
-        # squeeze successive blank lines
-        if i > 0 and len(line.strip()) == 0 and len(conf_lines[i - 1].strip()) == 0:
-            continue
-        
-        if line.startswith('@'):
-            line = '# ' + line
-        
-        elif i > 0 and conf_lines[i - 1].startswith('@'):
-            lines.append('') # add a blank line between @lines and the rest
-        
-        lines.append(line)
-        
-    return lines
-
-
-def _set_default_motion(data, old_motion):
-    data.setdefault('@enabled', True)
-
-    data.setdefault('@show_advanced', False)
-    data.setdefault('@admin_username', 'admin')
-    data.setdefault('@admin_password', '')
-    data.setdefault('@normal_username', 'user')
-    data.setdefault('@normal_password', '')
-
-    if old_motion:
-        data.setdefault('control_port', 7999)
-    
-    else:
-        data.setdefault('webcontrol_port', 7999)
-
-
-def _set_default_motion_camera(camera_id, data, old_motion=False):
-    data.setdefault('@name', 'Camera' + str(camera_id))
-    data.setdefault('@enabled', False)
-    data.setdefault('@id', camera_id)
-    
-    if not utils.net_camera(data):
-        data.setdefault('videodevice', '/dev/video0')
-        data.setdefault('brightness', 0)
-        data.setdefault('contrast', 0)
-        data.setdefault('saturation', 0)
-        data.setdefault('hue', 0)
-        data.setdefault('width', 352)
-        data.setdefault('height', 288)
-
-    data.setdefault('lightswitch', 50)
-    data.setdefault('auto_brightness', False)
-    data.setdefault('framerate', 2)
-    data.setdefault('rotate', 0)
-    
-    data.setdefault('@storage_device', 'custom-path')
-    data.setdefault('@network_server', '')
-    data.setdefault('@network_share_name', '')
-    data.setdefault('@network_username', '')
-    data.setdefault('@network_password', '')
-    data.setdefault('target_dir', settings.MEDIA_PATH)
-    
-    if old_motion:
-        data.setdefault('webcam_localhost', False)
-        data.setdefault('webcam_port', int('808' + str(camera_id)))
-        data.setdefault('webcam_maxrate', 5)
-        data.setdefault('webcam_quality', 85)
-        data.setdefault('webcam_motion', False)
-    
-    else:
-        data.setdefault('stream_localhost', False)
-        data.setdefault('stream_port', int('808' + str(camera_id)))
-        data.setdefault('stream_maxrate', 5)
-        data.setdefault('stream_quality', 85)
-        data.setdefault('stream_motion', False)
-        data.setdefault('stream_auth_method', 0)
-
-    data.setdefault('@webcam_resolution', 100)
-    data.setdefault('@webcam_server_resize', False)
-    
-    data.setdefault('text_left', data['@name'])
-    data.setdefault('text_right', '%Y-%m-%d\\n%T')
-    data.setdefault('text_double', False)
-
-    data.setdefault('@motion_detection', True)
-    data.setdefault('text_changes', False)
-    if old_motion:
-        data.setdefault('locate', False)
-    
-    else:
-        data.setdefault('locate_motion_mode', False)
-        data.setdefault('locate_motion_style', 'redbox')
-    
-    data.setdefault('threshold', 2000)
-    data.setdefault('noise_tune', True)
-    data.setdefault('noise_level', 32)
-    data.setdefault('minimum_motion_frames', 1)
-    
-    data.setdefault('pre_capture', 2)
-    data.setdefault('post_capture', 4)
-    data.setdefault('minimum_motion_frames', 1)
-    
-    if old_motion:
-        data.setdefault('output_normal', False)
-        data.setdefault('jpeg_filename', '')
-        data.setdefault('output_all', False)
-        data.setdefault('gap', 10)
-
-    else:
-        data.setdefault('output_pictures', False)
-        data.setdefault('picture_filename', '')
-        data.setdefault('emulate_motion', False)
-        data.setdefault('event_gap', 10)
-    
-    data.setdefault('snapshot_interval', 0)
-    data.setdefault('snapshot_filename', '')
-    data.setdefault('quality', 85)
-    data.setdefault('@preserve_pictures', 0)
-    
-    data.setdefault('ffmpeg_variable_bitrate', 0)
-    data.setdefault('ffmpeg_bps', 44000) # a quality of about 85% 
-    data.setdefault('movie_filename', '%Y-%m-%d/%H-%M-%S')
-    if old_motion:
-        data.setdefault('max_mpeg_time', 0)
-        data.setdefault('ffmpeg_cap_new', False)
-    
-    else:
-        data.setdefault('max_movie_time', 0)
-        data.setdefault('ffmpeg_output_movies', False)
-    data.setdefault('ffmpeg_video_codec', 'msmpeg4')
-    data.setdefault('@preserve_movies', 0)
-    
-    data.setdefault('@working_schedule', '')
-    data.setdefault('@working_schedule_type', 'outside')
-
-    data.setdefault('on_event_start', '')
-    data.setdefault('on_event_end', '')
-
-
-def _set_default_simple_mjpeg_camera(camera_id, data):
-    data.setdefault('@name', 'Camera' + str(camera_id))
-    data.setdefault('@enabled', False)
-    data.setdefault('@id', camera_id)
-
-    
-def get_additional_structure(camera, separators=False):
-    if _additional_structure_cache.get((camera, separators)) is None:
-        logging.debug('loading additional config structure for %s, %s separators' % (
-                'camera' if camera else 'main',
-                'with' if separators else 'without'))
-
-        # 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
-            
-            result['name'] = func.func_name
-            sections[func.func_name] = result
-            
-            logging.debug('additional config section: %s' % result['name'])
-    
-        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
-
-            if result['type'] == 'separator' and not separators:
-                continue
-
-            result['name'] = func.func_name
-            configs[func.func_name] = result
-    
-            section = sections.setdefault(result.get('section'), {})
-            section.setdefault('configs', []).append(result)
-            
-            logging.debug('additional config item: %s' % result['name'])
-
-        _additional_structure_cache[(camera, separators)] = sections, configs
-
-    return _additional_structure_cache[(camera, separators)]
-
-
-def _get_additional_config(data, camera_id=None):
-    args = [camera_id] if camera_id else []
-    
-    (sections, configs) = get_additional_structure(camera=bool(camera_id))
-    get_funcs = set([c.get('get') for c in configs.itervalues() if c.get('get')])
-    get_func_values = collections.OrderedDict((f, f(*args)) 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_id=None):
-    args = [camera_id] if camera_id else []
-
-    (sections, configs) = get_additional_structure(camera=bool(camera_id))
-    
-    set_func_values = collections.OrderedDict()
-    for name, section in sections.iteritems():
-        if not section.get('set'):
-            continue
-        
-        if ('@_' + name) not in data:
-            continue
-
-        if section.get('get_set_dict'):
-            set_func_values.setdefault(section['set'], {})[name] = data['@_' + name]
-
-        else:
-            set_func_values[section['set']] = data['@_' + name]
-
-    for name, config in configs.iteritems():
-        if not config.get('set'):
-            continue
-
-        if ('@_' + name) not in data:
-            continue
-
-        if config.get('get_set_dict'):
-            set_func_values.setdefault(config['set'], {})[name] = data['@_' + name]
-            
-        else:
-            set_func_values[config['set']] = data['@_' + name]
-
-    for func, value in set_func_values.iteritems():
-        func(*(args + [value]))
diff --git a/src/diskctl.py b/src/diskctl.py
deleted file mode 100644 (file)
index b4cd738..0000000
+++ /dev/null
@@ -1,259 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import logging
-import os
-import re
-import subprocess
-
-
-def _list_mounts():
-    logging.debug('listing mounts...')
-    
-    seen_targets = set()
-    
-    mounts = []
-    with open('/proc/mounts', 'r') as f:
-        for line in f:
-            line = line.strip()
-            if not line:
-                continue
-            parts = line.split()
-            if len(parts) < 4:
-                continue
-            
-            target = parts[0]
-            mount_point = parts[1]
-            fstype = parts[2]
-            opts = parts[3]
-            
-            if not os.access(mount_point, os.W_OK):
-                continue
-            
-            if target in seen_targets:
-                continue # probably a bind mount
-            
-            seen_targets.add(target)
-
-            if fstype == 'fuseblk':
-                fstype = 'ntfs' # most likely
-            
-            logging.debug('found mount "%s" at "%s"' % (target, mount_point))
-            
-            mounts.append({
-                'target': target,
-                'mount_point': mount_point,
-                'fstype': fstype,
-                'opts': opts,
-            })
-
-    return mounts
-
-
-def _list_disks():
-    if os.path.exists('/dev/disk/by-id/'):
-        return _list_disks_dev_by_id()
-    
-    else:  # fall back to fdisk -l
-        return _list_disks_fdisk()
-
-
-def _list_disks_dev_by_id():
-    logging.debug('listing disks using /dev/disk/by-id/')
-    
-    disks_by_dev = {}
-    partitions_by_dev = {}
-
-    for entry in os.listdir('/dev/disk/by-id/'):
-        parts = entry.split('-', 1)
-        if len(parts) < 2:
-            continue
-        
-        target = os.path.realpath(os.path.join('/dev/disk/by-id/', entry))
-        
-        bus, entry = parts
-        m = re.search('-part(\d+)$', entry)
-        if m:
-            part_no = int(m.group(1))
-            entry = re.sub('-part\d+$', '', entry)
-        
-        else:
-            part_no = None
-
-        parts = entry.split('_')
-        if len(parts) < 2:
-            vendor = parts[0]
-            model = ''
-        
-        else:
-            vendor, model = parts[:2]
-
-        if part_no is not None:
-            logging.debug('found partition "%s" at "%s" on bus "%s": "%s %s"' % (part_no, target, bus, vendor, model))
-        
-            partitions_by_dev[target] = {
-                'target': target,
-                'bus': bus,
-                'vendor': vendor,
-                'model': model,
-                'part_no': part_no,
-                'unmatched': True
-            }
-            
-        else:
-            logging.debug('found disk at "%s" on bus "%s": "%s %s"' % (target, bus, vendor, model))
-
-            disks_by_dev[target] = {
-                'target': target,
-                'bus': bus,
-                'vendor': vendor,
-                'model': model,
-                'partitions': []
-            }
-        
-    # group partitions by disk
-    for dev, partition in partitions_by_dev.items():
-        for disk_dev, disk in disks_by_dev.items():
-            if dev.startswith(disk_dev):
-                disk['partitions'].append(partition)
-                partition.pop('unmatched')
-            
-    # add separate partitions that did not match any disk
-    for partition in partitions_by_dev.values():
-        if partition.pop('unmatched', False):
-            disks_by_dev[partition['target']] = partition
-            partition['partitions'] = [dict(partition)]
-
-    # prepare flat list of disks
-    disks = disks_by_dev.values()
-    disks.sort(key=lambda d: d['vendor'])
-    
-    for disk in disks:
-        disk['partitions'].sort(key=lambda p: p['part_no'])
-
-    return disks
-
-
-def _list_disks_fdisk():
-    try:
-        output = subprocess.check_output('fdisk -l 2>/dev/null', shell=True)
-    
-    except Exception as e:
-        logging.error('failed to list disks using "fdisk -l": %s' % e, exc_info=True)
-        
-        return []
-
-    disks = []
-    disk = None
-    
-    def add_disk(disk):
-        logging.debug('found disk at "%s" on bus "%s": "%s %s"' %
-                (disk['target'], disk['bus'], disk['vendor'], disk['model']))
-
-        for part in disk['partitions']:
-            logging.debug('found partition "%s" at "%s" on bus "%s": "%s %s"' %
-                    (part['part_no'], part['target'], part['bus'], part['vendor'], part['model']))
-
-        disks.append(disk)
-
-    for line in output.split('\n'):
-        line = line.replace('*', '')
-        line = re.sub('\s+', ' ', line.strip())
-        if not line:
-            continue
-
-        if line.startswith('Disk /dev/'):
-            if disk and disk['partitions']:
-                add_disk(disk)
-
-            parts = line.split()
-
-            disk = {
-                'target': parts[1].strip(':'),
-                'bus': '',
-                'vendor': '',
-                'model': parts[2] + ' ' + parts[3].strip(','),
-                'partitions': []
-            }
-            
-        elif line.startswith('/dev/') and disk:
-            parts = line.split()
-            part_no = re.findall('\d+$', parts[0])
-            partition = {
-                'part_no': int(part_no[0]) if part_no else None,
-                'target': parts[0],
-                'bus': '',
-                'vendor': '',
-                'model': parts[4] + ' ' + ' '.join(parts[6:]),
-            }
-            
-            disk['partitions'].append(partition)
-
-    if disk and disk['partitions']:
-        add_disk(disk)
-
-    disks.sort(key=lambda d: d['target'])
-
-    for disk in disks:
-        disk['partitions'].sort(key=lambda p: p['part_no'])
-
-    return disks
-
-
-def list_mounted_disks():
-    mounted_disks = []
-    
-    try:
-        disks = _list_disks()
-        mounts_by_target = dict((m['target'], m) for m in _list_mounts())
-        
-        for disk in disks:
-            for partition in disk['partitions']:
-                mount = mounts_by_target.get(partition['target'])
-                if mount:
-                    partition.update(mount) 
-        
-            # filter out unmounted partitions
-            disk['partitions'] = [p for p in disk['partitions'] if p.get('mount_point')]
-        
-        # filter out unmounted disks
-        mounted_disks = [d for d in disks if d['partitions']]
-
-    except Exception as e:
-        logging.error('failed to list mounted disks: %s' % e, exc_info=True)
-        
-    return mounted_disks
-
-
-def list_mounted_partitions():
-    mounted_partitions = {}
-
-    try:
-        disks = _list_disks()
-        mounts_by_target = dict((m['target'], m) for m in _list_mounts())
-        
-        for disk in disks:
-            for partition in disk['partitions']:
-                mount = mounts_by_target.get(partition['target'])
-                if mount:
-                    partition.update(mount)
-                    mounted_partitions[partition['target']] = partition 
-        
-    except Exception as e:
-        logging.error('failed to list mounted partitions: %s' % e, exc_info=True)
-        
-    return mounted_partitions
diff --git a/src/handlers.py b/src/handlers.py
deleted file mode 100644 (file)
index 120d481..0000000
+++ /dev/null
@@ -1,1466 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import json
-import logging
-import os
-import re
-import socket
-import subprocess
-
-from tornado.web import RequestHandler, HTTPError, asynchronous
-from tornado.ioloop import IOLoop
-
-import config
-import mediafiles
-import motionctl
-import powerctl
-import remote
-import settings
-import smbctl
-import template
-import update
-import utils
-import v4l2ctl
-
-
-class BaseHandler(RequestHandler):
-    def get_data(self):
-        keys = self.request.arguments.keys()
-        data = dict([(key, self.get_argument(key)) for key in keys])
-
-        for key in self.request.files:
-            files = self.request.files[key]
-            if len(files) > 1:
-                data[key] = files
-
-            elif len(files) > 0:
-                data[key] = files[0]
-
-            else:
-                continue
-
-        return data
-    
-    def render(self, template_name, content_type='text/html', **context):
-        self.set_header('Content-Type', content_type)
-        
-        content = template.render(template_name, **context)
-        self.finish(content)
-    
-    def finish_json(self, data={}):
-        self.set_header('Content-Type', 'application/json')
-        self.finish(json.dumps(data))
-
-    def get_current_user(self):
-        main_config = config.get_main()
-        
-        username = self.get_argument('_username', None)
-        signature = self.get_argument('_signature', None)
-        login = self.get_argument('_login', None) == 'true'
-        if (username == main_config.get('@admin_username') and
-            signature == utils.compute_signature(self.request.method, self.request.uri, self.request.body, main_config.get('@admin_password'))):
-            
-            return 'admin'
-        
-        elif not username and not main_config.get('@normal_password'): # no authentication required for normal user
-            return 'normal'
-        
-        elif (username == main_config.get('@normal_username') and
-            signature == utils.compute_signature(self.request.method, self.request.uri, self.request.body, main_config.get('@normal_password'))):
-            
-            return 'normal'
-
-        elif username and username != '_' and login:
-            logging.error('authentication failed for user %(user)s' % {'user': username})
-
-        return None
-        
-    def _handle_request_exception(self, exception):
-        try:
-            if isinstance(exception, HTTPError):
-                logging.error(str(exception))
-                self.set_status(exception.status_code)
-                self.finish_json({'error': exception.log_message or getattr(exception, 'reason', None) or str(exception)})
-            
-            else:
-                logging.error(str(exception), exc_info=True)
-                self.set_status(500)
-                self.finish_json({'error':  'internal server error'})
-                
-        except RuntimeError:
-            pass # nevermind
-        
-    @staticmethod
-    def auth(admin=False, prompt=True):
-        def decorator(func):
-            def wrapper(self, *args, **kwargs):
-                _admin = self.get_argument('_admin', None) == 'true'
-                
-                user = self.current_user
-                if (user is None) or (user != 'admin' and (admin or _admin)):
-                    self.set_header('Content-Type', 'application/json')
-
-                    return self.finish_json({'error': 'unauthorized', 'prompt': prompt})
-
-                return func(self, *args, **kwargs)
-            
-            return wrapper
-        
-        return decorator
-
-    def get(self, *args, **kwargs):
-        raise HTTPError(400, 'method not allowed')
-
-    def post(self, *args, **kwargs):
-        raise HTTPError(400, 'method not allowed')
-
-
-class NotFoundHandler(BaseHandler):
-    def get(self):
-        raise HTTPError(404, 'not found')
-
-    def post(self):
-        raise HTTPError(404, 'not found')
-
-
-class MainHandler(BaseHandler):
-    def get(self):
-        import motioneye
-        
-        # additional config
-        main_sections = config.get_additional_structure(camera=False, separators=True)[0]
-        camera_sections = config.get_additional_structure(camera=True, separators=True)[0]
-
-        self.render('main.html',
-                frame=False,
-                version=motioneye.VERSION,
-                enable_update=False,
-                enable_reboot=settings.ENABLE_REBOOT,
-                add_remove_cameras=settings.ADD_REMOVE_CAMERAS,
-                main_sections=main_sections,
-                camera_sections=camera_sections,
-                hostname=socket.gethostname(),
-                title=self.get_argument('title', None),
-                admin_username=config.get_main().get('@admin_username'),
-                old_motion=config.is_old_motion())
-
-
-class ConfigHandler(BaseHandler):
-    @asynchronous
-    def get(self, camera_id=None, op=None):
-        if camera_id is not None:
-            camera_id = int(camera_id)
-        
-        if op == 'get':
-            self.get_config(camera_id)
-            
-        elif op == 'list':
-            self.list()
-        
-        elif op == 'backup':
-            self.backup()
-        
-        else:
-            raise HTTPError(400, 'unknown operation')
-    
-    @asynchronous
-    def post(self, camera_id=None, op=None):
-        if camera_id is not None:
-            camera_id = int(camera_id)
-        
-        if op == 'set':
-            self.set_config(camera_id)
-        
-        elif op == 'set_preview':
-            self.set_preview(camera_id)
-        
-        elif op == 'add':
-            self.add_camera()
-        
-        elif op == 'rem':
-            self.rem_camera(camera_id)
-            
-        elif op == 'restore':
-            self.restore()
-        
-        else:
-            raise HTTPError(400, 'unknown operation')
-    
-    @BaseHandler.auth(admin=True)
-    def get_config(self, camera_id):
-        if camera_id:
-            logging.debug('getting config for camera %(id)s' % {'id': camera_id})
-            
-            if camera_id not in config.get_camera_ids():
-                raise HTTPError(404, 'no such camera')
-            
-            local_config = config.get_camera(camera_id)
-            if utils.local_motion_camera(local_config):
-                ui_config = config.motion_camera_dict_to_ui(local_config)
-                    
-                self.finish_json(ui_config)
-            
-            elif utils.remote_camera(local_config):
-                def on_response(remote_ui_config=None, error=None):
-                    if error:
-                        return self.finish_json({'error': 'Failed to get remote camera configuration for %(url)s: %(msg)s.' % {
-                                'url': remote.pretty_camera_url(local_config), 'msg': error}})
-                    
-                    for key, value in local_config.items():
-                        remote_ui_config[key.replace('@', '')] = value
-                    
-                    # replace the real device URI with the remote camera URL
-                    remote_ui_config['device_url'] = remote.pretty_camera_url(local_config)
-                    self.finish_json(remote_ui_config)
-                
-                remote.get_config(local_config, on_response)
-                
-            else: # assuming simple mjpeg camera
-                ui_config = config.simple_mjpeg_camera_dict_to_ui(local_config)
-                    
-                self.finish_json(ui_config)
-            
-        else:
-            logging.debug('getting main config')
-            
-            ui_config = config.main_dict_to_ui(config.get_main())
-            self.finish_json(ui_config)
-    
-    @BaseHandler.auth(admin=True)
-    def set_config(self, camera_id):
-        try:
-            ui_config = json.loads(self.request.body)
-            
-        except Exception as e:
-            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
-            
-            raise
-        
-        camera_ids = config.get_camera_ids()
-        
-        def set_camera_config(camera_id, ui_config, on_finish):
-            logging.debug('setting config for camera %(id)s...' % {'id': camera_id})
-            
-            if camera_id not in camera_ids:
-                raise HTTPError(404, 'no such camera')
-            
-            local_config = config.get_camera(camera_id)
-            if utils.local_motion_camera(local_config):
-                local_config = config.motion_camera_ui_to_dict(ui_config, local_config)
-
-                config.set_camera(camera_id, local_config)
-            
-                on_finish(None, True) # (no error, motion needs restart)
-
-            elif utils.remote_camera(local_config):
-                # update the camera locally
-                local_config['@enabled'] = ui_config['enabled']
-                config.set_camera(camera_id, local_config)
-                
-                if ui_config.has_key('name'):
-                    def on_finish_wrapper(error=None):
-                        return on_finish(error, False)
-                    
-                    ui_config['enabled'] = True # never disable the camera remotely 
-                    remote.set_config(local_config, ui_config, on_finish_wrapper)
-                
-                else:
-                    # when the ui config supplied has only the enabled state
-                    # and no useful fields (such as "name"),
-                    # the camera was probably disabled due to errors
-                    on_finish(None, False)
-                    
-            else: # assuming simple mjpeg camera
-                local_config = config.simple_mjpeg_camera_ui_to_dict(ui_config, local_config)
-
-                config.set_camera(camera_id, local_config)
-            
-                on_finish(None, False) # (no error, motion doesn't need restart)
-
-        def set_main_config(ui_config):
-            logging.debug('setting main config...')
-            
-            old_main_config = config.get_main()
-            old_admin_credentials = '%s:%s' % (old_main_config.get('@admin_username', ''), old_main_config.get('@admin_password', ''))
-            old_normal_credentials = '%s:%s' % (old_main_config.get('@normal_username', ''), old_main_config.get('@normal_password', ''))
-
-            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', ''))
-            normal_credentials = '%s:%s' % (main_config.get('@normal_username', ''), main_config.get('@normal_password', ''))
-
-            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_config_names.append('@admin_password')
-            reboot = bool([k for k in reboot_config_names if old_main_config.get(k) != main_config.get(k)])
-
-            config.set_main(main_config)
-            
-            reload = False
-            restart = False
-            
-            if admin_credentials != old_admin_credentials:
-                logging.debug('admin credentials changed, reload needed')
-                
-                reload = True
-
-            if normal_credentials != old_normal_credentials:
-                logging.debug('surveillance credentials changed, all camera configs must be updated')
-                
-                # reconfigure all local cameras to update the stream authentication options
-                for camera_id in config.get_camera_ids():
-                    local_config = config.get_camera(camera_id)
-                    if not utils.local_motion_camera(local_config):
-                        continue
-                    
-                    ui_config = config.motion_camera_dict_to_ui(local_config)
-                    local_config = config.motion_camera_ui_to_dict(ui_config, local_config)
-
-                    config.set_camera(camera_id, local_config)
-                    
-                    restart = True
-
-            if reboot and settings.ENABLE_REBOOT:
-                logging.debug('system settings changed, reboot needed')
-        
-            else: 
-                reboot = False
-
-            return {'reload': reload, 'reboot': reboot, 'restart': restart}
-        
-        reload = False # indicates that browser should reload the page
-        reboot = [False] # indicates that the server will reboot immediately
-        restart = [False]  # indicates that the local motion instance was modified and needs to be restarted
-        error = [None]
-        
-        def finish():
-            if reboot[0]:
-                if settings.ENABLE_REBOOT:
-                    def call_reboot():
-                        powerctl.reboot()
-                    
-                    ioloop = IOLoop.instance()
-                    ioloop.add_timeout(datetime.timedelta(seconds=2), call_reboot)
-                    return self.finish({'reload': False, 'reboot': True, 'error': None})
-                
-                else:
-                    reboot[0] = False
-
-            if restart[0]:
-                logging.debug('motion needs to be restarted')
-                
-                motionctl.stop()
-                
-                if settings.SMB_SHARES:
-                    logging.debug('updating SMB mounts')
-                    stop, start = smbctl.update_mounts()  # @UnusedVariable
-
-                    if start:
-                        motionctl.start()
-                
-                else:
-                    motionctl.start()
-
-            self.finish({'reload': reload, 'reboot': reboot[0], 'error': error[0]})
-        
-        if camera_id is not None:
-            if camera_id == 0: # multiple camera configs
-                if len(ui_config) > 1:
-                    logging.debug('setting multiple configs')
-                
-                elif len(ui_config) == 0:
-                    logging.warn('no configuration to set')
-                    
-                    self.finish()
-                
-                so_far = [0]
-                def check_finished(e, r):
-                    restart[0] = restart[0] or r
-                    error[0] = error[0] or e
-                    so_far[0] += 1
-                    
-                    if so_far[0] >= len(ui_config): # finished
-                        finish()
-
-                # make sure main config is handled first
-                items = ui_config.items()
-                items.sort(key=lambda (key, cfg): key != 'main')
-
-                for key, cfg in items:
-                    if key == 'main':
-                        result = set_main_config(cfg)
-                        reload = result['reload'] or reload
-                        reboot[0] = result['reboot'] or reboot[0]
-                        restart[0] = result['restart'] or restart[0]
-                        check_finished(None, reload)
-                        
-                    else:
-                        set_camera_config(int(key), cfg, check_finished)
-            
-            else: # single camera config
-                def on_finish(e, r):
-                    error[0] = e
-                    restart[0] = r
-                    finish()
-
-                set_camera_config(camera_id, ui_config, on_finish)
-
-        else: # main config
-            result = set_main_config(ui_config)
-            reload = result['reload']
-            reboot[0] = result['reboot']
-            restart[0] = result['restart']
-
-    @BaseHandler.auth(admin=True)
-    def set_preview(self, camera_id):
-        try:
-            controls = json.loads(self.request.body)
-            
-        except Exception as e:
-            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
-            
-            raise
-
-        camera_config = config.get_camera(camera_id)
-        if utils.v4l2_camera(camera_config): 
-            device = camera_config['videodevice']
-            
-            if 'brightness' in controls:
-                value = int(controls['brightness'])
-                logging.debug('setting brightness to %(value)s...' % {'value': value})
-    
-                v4l2ctl.set_brightness(device, value)
-    
-            if 'contrast' in controls:
-                value = int(controls['contrast'])
-                logging.debug('setting contrast to %(value)s...' % {'value': value})
-    
-                v4l2ctl.set_contrast(device, value)
-    
-            if 'saturation' in controls:
-                value = int(controls['saturation'])
-                logging.debug('setting saturation to %(value)s...' % {'value': value})
-    
-                v4l2ctl.set_saturation(device, value)
-    
-            if 'hue' in controls:
-                value = int(controls['hue'])
-                logging.debug('setting hue to %(value)s...' % {'value': value})
-    
-                v4l2ctl.set_hue(device, value)
-            
-            self.finish_json({})
-
-        elif utils.remote_camera(camera_config):
-            def on_response(error=None):
-                if error:
-                    self.finish_json({'error': error})
-                    
-                else:
-                    self.finish_json()
-            
-            remote.set_preview(camera_config, controls, on_response)
-        
-        else: # not supported
-            self.finish_json({'error': True})
-
-    @BaseHandler.auth()
-    def list(self):
-        logging.debug('listing cameras')
-
-        proto = self.get_data().get('proto')        
-        if proto == 'motioneye':  # remote listing
-            def on_response(cameras=None, error=None):
-                if error:
-                    self.finish_json({'error': error})
-                    
-                else:
-                    cameras = [c for c in cameras if c.get('enabled')]
-                    self.finish_json({'cameras': cameras})
-
-            remote.list(self.get_data(), on_response)
-        
-        elif proto == 'netcam':
-            scheme = self.get_data().get('scheme', 'http')
-
-            def on_response(cameras=None, error=None):
-                if error:
-                    self.finish_json({'error': error})
-                    
-                else:
-                    self.finish_json({'cameras': cameras})
-            
-            if scheme in ['http', 'https']:
-                utils.test_mjpeg_url(self.get_data(), auth_modes=['basic'], allow_jpeg=True, callback=on_response)
-                
-            elif config.motion_rtsp_support() and scheme == 'rtsp':
-                utils.test_rtsp_url(self.get_data(), callback=on_response)
-                
-            else:
-                on_response(error='protocol %s not supported' % scheme)
-
-        elif proto == 'mjpeg':
-            def on_response(cameras=None, error=None):
-                if error:
-                    self.finish_json({'error': error})
-                    
-                else:
-                    self.finish_json({'cameras': cameras})
-            
-            utils.test_mjpeg_url(self.get_data(), auth_modes=['basic', 'digest'], allow_jpeg=False, callback=on_response)
-        
-        elif proto == 'v4l2':
-            configured_devices = set()
-            for camera_id in config.get_camera_ids():
-                data = config.get_camera(camera_id)
-                if utils.v4l2_camera(data):
-                    configured_devices.add(data['videodevice'])
-
-            cameras = [{'id': d[1], 'name': d[2]} for d in v4l2ctl.list_devices()
-                    if (d[0] not in configured_devices) and (d[1] not in configured_devices)]
-            
-            self.finish_json({'cameras': cameras})
-
-        else:  # assuming local motionEye camera listing
-            cameras = []
-            camera_ids = config.get_camera_ids()
-            if not config.get_main().get('@enabled'):
-                camera_ids = []
-                
-            length = [len(camera_ids)]
-            def check_finished():
-                if len(cameras) == length[0]:
-                    cameras.sort(key=lambda c: c['id'])
-                    self.finish_json({'cameras': cameras})
-                    
-            def on_response_builder(camera_id, local_config):
-                def on_response(remote_ui_config=None, error=None):
-                    if error:
-                        cameras.append({
-                            'id': camera_id,
-                            'name': '&lt;' + remote.pretty_camera_url(local_config) + '&gt;',
-                            'enabled': False,
-                            'streaming_framerate': 1,
-                            'framerate': 1
-                        })
-                    
-                    else:
-                        remote_ui_config['id'] = camera_id
-
-                        if not remote_ui_config['enabled'] and local_config['@enabled']:
-                            # if a remote camera is disabled, make sure it's disabled locally as well
-                            local_config['@enabled'] = False
-                            config.set_camera(camera_id, local_config)
-                        
-                        elif remote_ui_config['enabled'] and not local_config['@enabled']:
-                            # if a remote camera is locally disabled, make sure the remote config says the same thing
-                            remote_ui_config['enabled'] = False
-                            
-                        for key, value in local_config.items():
-                            remote_ui_config[key.replace('@', '')] = value
-
-                        cameras.append(remote_ui_config)
-                        
-                    check_finished()
-                    
-                return on_response
-            
-            for camera_id in camera_ids:
-                local_config = config.get_camera(camera_id)
-                if local_config is None:
-                    continue
-                
-                if utils.local_motion_camera(local_config):
-                    ui_config = config.motion_camera_dict_to_ui(local_config)
-                    cameras.append(ui_config)
-                    check_finished()
-
-                elif utils.remote_camera(local_config):
-                    if local_config.get('@enabled') or self.get_argument('force', None) == 'true':
-                        remote.get_config(local_config, on_response_builder(camera_id, local_config))
-                    
-                    else: # don't try to reach the remote of the camera is disabled
-                        on_response_builder(camera_id, local_config)(error=True)
-                        
-                else: # assuming simple mjpeg camera
-                    ui_config = config.simple_mjpeg_camera_dict_to_ui(local_config)
-                    cameras.append(ui_config)
-                    check_finished()
-            
-            if length[0] == 0:        
-                self.finish_json({'cameras': []})
-
-    @BaseHandler.auth(admin=True)
-    def add_camera(self):
-        logging.debug('adding new camera')
-        
-        try:
-            device_details = json.loads(self.request.body)
-            
-        except Exception as e:
-            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
-            
-            raise
-
-        camera_config = config.add_camera(device_details)
-
-        if utils.local_motion_camera(camera_config):
-            motionctl.stop()
-            
-            if settings.SMB_SHARES:
-                stop, start = smbctl.update_mounts()  # @UnusedVariable
-
-                if start:
-                    motionctl.start()
-            
-            else:
-                motionctl.start()
-            
-            ui_config = config.motion_camera_dict_to_ui(camera_config)
-            
-            self.finish_json(ui_config)
-        
-        elif utils.remote_camera(camera_config):
-            def on_response(remote_ui_config=None, error=None):
-                if error:
-                    return self.finish_json({'error': error})
-
-                for key, value in camera_config.items():
-                    remote_ui_config[key.replace('@', '')] = value
-                
-                self.finish_json(remote_ui_config)
-                
-            remote.get_config(camera_config, on_response)
-        
-        else: # assuming simple mjpeg camera
-            ui_config = config.simple_mjpeg_camera_dict_to_ui(camera_config)
-            
-            self.finish_json(ui_config)
-    
-    @BaseHandler.auth(admin=True)
-    def rem_camera(self, camera_id):
-        logging.debug('removing camera %(id)s' % {'id': camera_id})
-        
-        local = utils.local_motion_camera(config.get_camera(camera_id))
-        config.rem_camera(camera_id)
-        
-        if local:
-            motionctl.stop()
-            motionctl.start()
-            
-        self.finish_json()
-        
-    @BaseHandler.auth(admin=True)
-    def backup(self):
-        content = config.backup()
-
-        filename = 'motioneye-config.tar.gz'
-        self.set_header('Content-Type', 'application/x-compressed')
-        self.set_header('Content-Disposition', 'attachment; filename=' + filename + ';')
-
-        self.finish(content)
-
-    @BaseHandler.auth(admin=True)
-    def restore(self):
-        try:
-            content = self.request.files['files'][0]['body']
-            
-        except KeyError:
-            raise HTTPError(400, 'file attachment required')
-
-        result = config.restore(content)
-        if result:
-            self.finish_json({'ok': True, 'reboot': result['reboot']})
-            
-        else:
-            self.finish_json({'ok': False})
-
-
-class PictureHandler(BaseHandler):
-    @asynchronous
-    def get(self, camera_id, op, filename=None, group=None):
-        if camera_id is not None:
-            camera_id = int(camera_id)
-            if camera_id not in config.get_camera_ids():
-                raise HTTPError(404, 'no such camera')
-        
-        if op == 'current':
-            self.current(camera_id)
-            
-        elif op == 'list':
-            self.list(camera_id)
-            
-        elif op == 'frame':
-            self.frame(camera_id)
-            
-        elif op == 'download':
-            self.download(camera_id, filename)
-        
-        elif op == 'preview':
-            self.preview(camera_id, filename)
-        
-        elif op == 'zipped':
-            self.zipped(camera_id, group)
-        
-        elif op == 'timelapse':
-            self.timelapse(camera_id, group)
-        
-        else:
-            raise HTTPError(400, 'unknown operation')
-    
-    @asynchronous
-    def post(self, camera_id, op, filename=None, group=None):
-        if camera_id is not None:
-            camera_id = int(camera_id)
-            if camera_id not in config.get_camera_ids():
-                raise HTTPError(404, 'no such camera')
-        
-        if op == 'delete':
-            self.delete(camera_id, filename)
-
-        elif op == 'delete_all':
-            self.delete_all(camera_id, group)
-        
-        else:
-            raise HTTPError(400, 'unknown operation')
-    
-    @BaseHandler.auth(prompt=False)
-    def current(self, camera_id):
-        self.set_header('Content-Type', 'image/jpeg')
-        
-        width = self.get_argument('width', None)
-        height = self.get_argument('height', None)
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            picture = mediafiles.get_current_picture(camera_config,
-                    width=width,
-                    height=height)
-            
-            self.set_cookie('motion_detected_' + str(camera_id), str(motionctl.is_motion_detected(camera_id)).lower())
-            self.try_finish(picture)
-                
-        elif utils.remote_camera(camera_config):
-            def on_response(motion_detected=False, picture=None, error=None):
-                self.set_cookie('motion_detected_' + str(camera_id), str(motion_detected).lower())
-                self.try_finish(picture)
-            
-            remote.get_current_picture(camera_config, width=width, height=height, callback=on_response)
-            
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-            
-
-    @BaseHandler.auth()
-    def list(self, camera_id):
-        logging.debug('listing pictures for camera %(id)s' % {'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            def on_media_list(media_list):
-                if media_list is None:
-                    return self.finish_json({'error': 'Failed to get pictures list.'})
-
-                self.finish_json({
-                    'mediaList': media_list,
-                    'cameraName': camera_config['@name']
-                })
-            
-            mediafiles.list_media(camera_config, media_type='picture',
-                    callback=on_media_list, prefix=self.get_argument('prefix', None))
-
-        elif utils.remote_camera(camera_config):
-            def on_response(remote_list=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to get picture list for %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                self.finish_json(remote_list)
-            
-            remote.list_media(camera_config, media_type='picture', prefix=self.get_argument('prefix', None), callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    def frame(self, camera_id):
-        camera_config = config.get_camera(camera_id)
-        
-        if utils.local_motion_camera(camera_config) or utils.simple_mjpeg_camera(camera_config) or self.get_argument('title', None) is not None:
-            self.render('main.html',
-                    frame=True,
-                    camera_id=camera_id,
-                    camera_config=camera_config,
-                    title=self.get_argument('title', camera_config.get('@name', '')),
-                    admin_username=config.get_main().get('@admin_username'))
-
-        elif utils.remote_camera(camera_config):
-            def on_response(remote_ui_config=None, error=None):
-                if error:
-                    return self.render('main.html',
-                            frame=True,
-                            camera_id=camera_id,
-                            camera_config=camera_config,
-                            title=self.get_argument('title', ''))
-
-                # issue a fake motion_camera_ui_to_dict() call to transform
-                # the remote UI values into motion config directives
-                remote_config = config.motion_camera_ui_to_dict(remote_ui_config)
-                
-                self.render('main.html',
-                        frame=True,
-                        camera_id=camera_id,
-                        camera_config=remote_config,
-                        title=self.get_argument('title', remote_config['@name']),
-                        admin_username=config.get_main().get('@admin_username'))
-
-            remote.get_config(camera_config, on_response)
-        
-    @BaseHandler.auth()
-    def download(self, camera_id, filename):
-        logging.debug('downloading picture %(filename)s of camera %(id)s' % {
-                'filename': filename, 'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            content = mediafiles.get_media_content(camera_config, filename, 'picture')
-            
-            pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
-            self.set_header('Content-Type', 'image/jpeg')
-            self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
-            
-            self.finish(content)
-        
-        elif utils.remote_camera(camera_config):
-            def on_response(response=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to download picture from %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                pretty_filename = os.path.basename(filename) # no camera name available w/o additional request
-                self.set_header('Content-Type', 'image/jpeg')
-                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
-                
-                self.finish(response)
-
-            remote.get_media_content(camera_config, filename=filename, media_type='picture', callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth()
-    def preview(self, camera_id, filename):
-        logging.debug('previewing picture %(filename)s of camera %(id)s' % {
-                'filename': filename, 'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            content = mediafiles.get_media_preview(camera_config, filename, 'picture',
-                    width=self.get_argument('width', None),
-                    height=self.get_argument('height', None))
-            
-            if content:
-                self.set_header('Content-Type', 'image/jpeg')
-                
-            else:
-                self.set_header('Content-Type', 'image/svg+xml')
-                content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
-                
-            self.finish(content)
-        
-        elif utils.remote_camera(camera_config):
-            def on_response(content=None, error=None):
-                if content:
-                    self.set_header('Content-Type', 'image/jpeg')
-                    
-                else:
-                    self.set_header('Content-Type', 'image/svg+xml')
-                    content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
-                
-                self.finish(content)
-            
-            remote.get_media_preview(camera_config, filename=filename, media_type='picture',
-                    width=self.get_argument('width', None),
-                    height=self.get_argument('height', None),
-                    callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-    
-    @BaseHandler.auth(admin=True)
-    def delete(self, camera_id, filename):
-        logging.debug('deleting picture %(filename)s of camera %(id)s' % {
-                'filename': filename, 'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            try:
-                mediafiles.del_media_content(camera_config, filename, 'picture')
-                self.finish_json()
-                
-            except Exception as e:
-                self.finish_json({'error': unicode(e)})
-
-        elif utils.remote_camera(camera_config):
-            def on_response(response=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to delete picture from %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                self.finish_json()
-
-            remote.del_media_content(camera_config, filename=filename, media_type='picture', callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth()
-    def zipped(self, camera_id, group):
-        key = self.get_argument('key', None)
-        camera_config = config.get_camera(camera_id)
-        
-        if key:
-            logging.debug('serving zip file for group %(group)s of camera %(id)s with key %(key)s' % {
-                    'group': group, 'id': camera_id, 'key': key})
-            
-            if utils.local_motion_camera(camera_config):
-                data = mediafiles.get_prepared_cache(key)
-                if not data:
-                    logging.error('prepared cache data for key "%s" does not exist' % key)
-                    
-                    raise HTTPError(404, 'no such key')
-
-                pretty_filename = camera_config['@name'] + '_' + group
-                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
-         
-                self.set_header('Content-Type', 'application/zip')
-                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.zip;')
-                self.finish(data)
-                
-            elif utils.remote_camera(camera_config):
-                def on_response(response=None, error=None):
-                    if error:
-                        return self.finish_json({'error': 'Failed to download zip file from %(url)s: %(msg)s.' % {
-                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                    self.set_header('Content-Type', response['content_type'])
-                    self.set_header('Content-Disposition', response['content_disposition'])
-                    self.finish(response['data'])
-
-                remote.get_zipped_content(camera_config, media_type='picture', key=key, group=group, callback=on_response)
-
-            else: # assuming simple mjpeg camera
-                raise HTTPError(400, 'unknown operation')
-
-        else: # prepare
-            logging.debug('preparing zip file for group %(group)s of camera %(id)s' % {
-                    'group': group, 'id': camera_id})
-
-            if utils.local_motion_camera(camera_config):
-                def on_zip(data):
-                    if data is None:
-                        return self.finish_json({'error': 'Failed to create zip file.'})
-    
-                    key = mediafiles.set_prepared_cache(data)
-                    logging.debug('prepared zip file for group %(group)s of camera %(id)s with key %(key)s' % {
-                            'group': group, 'id': camera_id, 'key': key})
-                    self.finish_json({'key': key})
-    
-                mediafiles.get_zipped_content(camera_config, media_type='picture', group=group, callback=on_zip)
-    
-            elif utils.remote_camera(camera_config):
-                def on_response(response=None, error=None):
-                    if error:
-                        return self.finish_json({'error': 'Failed to make zip file at %(url)s: %(msg)s.' % {
-                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                    self.finish_json({'key': response['key']})
-
-                remote.make_zipped_content(camera_config, media_type='picture', group=group, callback=on_response)
-
-            else: # assuming simple mjpeg camera
-                raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth()
-    def timelapse(self, camera_id, group):
-        key = self.get_argument('key', None)
-        check = self.get_argument('check', False)
-        camera_config = config.get_camera(camera_id)
-
-        if key: # download
-            logging.debug('serving timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
-                    'group': group, 'id': camera_id, 'key': key})
-            
-            if utils.local_motion_camera(camera_config):
-                data = mediafiles.get_prepared_cache(key)
-                if data is None:
-                    logging.error('prepared cache data for key "%s" does not exist' % key)
-
-                    raise HTTPError(404, 'no such key')
-
-                pretty_filename = camera_config['@name'] + '_' + group
-                pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
-    
-                self.set_header('Content-Type', 'video/x-msvideo')
-                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.avi;')
-                self.finish(data)
-
-            elif utils.remote_camera(camera_config):
-                def on_response(response=None, error=None):
-                    if error:
-                        return self.finish_json({'error': 'Failed to download timelapse movie from %(url)s: %(msg)s.' % {
-                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                    self.set_header('Content-Type', response['content_type'])
-                    self.set_header('Content-Disposition', response['content_disposition'])
-                    self.finish(response['data'])
-
-                remote.get_timelapse_movie(camera_config, key, group=group, callback=on_response)
-
-            else: # assuming simple mjpeg camera
-                raise HTTPError(400, 'unknown operation')
-
-        elif check:
-            logging.debug('checking timelapse movie status for group %(group)s of camera %(id)s' % {
-                    'group': group, 'id': camera_id})
-
-            if utils.local_motion_camera(camera_config):
-                status = mediafiles.check_timelapse_movie()
-                if status['progress'] == -1 and status['data']:
-                    key = mediafiles.set_prepared_cache(status['data'])
-                    logging.debug('prepared timelapse movie for group %(group)s of camera %(id)s with key %(key)s' % {
-                            'group': group, 'id': camera_id, 'key': key})
-                    self.finish_json({'key': key, 'progress': -1})
-
-                else:
-                    self.finish_json(status)
-
-            elif utils.remote_camera(camera_config):
-                def on_response(response=None, error=None):
-                    if error:
-                        return self.finish_json({'error': 'Failed to check timelapse movie progress at %(url)s: %(msg)s.' % {
-                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                    if response['progress'] == -1 and response.get('key'):
-                        self.finish_json({'key': response['key'], 'progress': -1})
-                    
-                    else:
-                        self.finish_json(response)
-
-                remote.check_timelapse_movie(camera_config, group=group, callback=on_response)
-
-            else: # assuming simple mjpeg camera
-                raise HTTPError(400, 'unknown operation')
-
-        else: # start timelapse
-            interval = int(self.get_argument('interval'))
-            framerate = int(self.get_argument('framerate'))
-
-            logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with rate %(framerate)s/%(int)s' % {
-                    'group': group, 'id': camera_id, 'framerate': framerate, 'int': interval})
-
-            if utils.local_motion_camera(camera_config):
-                status = mediafiles.check_timelapse_movie()
-                if status['progress'] != -1:
-                    self.finish_json({'progress': status['progress']}) # timelapse already active
-
-                else:
-                    mediafiles.make_timelapse_movie(camera_config, framerate, interval, group=group)
-                    self.finish_json({'progress': -1})
-
-            elif utils.remote_camera(camera_config):
-                def on_status(response=None, error=None):
-                    if error:
-                        return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % {
-                                'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-                    
-                    if response['progress'] != -1:
-                        return self.finish_json({'progress': response['progress']}) # timelapse already active
-    
-                    def on_make(response=None, error=None):
-                        if error:
-                            return self.finish_json({'error': 'Failed to make timelapse movie at %(url)s: %(msg)s.' % {
-                                    'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-    
-                        self.finish_json({'progress': -1})
-                    
-                    remote.make_timelapse_movie(camera_config, framerate, interval, group=group, callback=on_make)
-
-                remote.check_timelapse_movie(camera_config, group=group, callback=on_status)
-
-            else: # assuming simple mjpeg camera
-                raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth(admin=True)
-    def delete_all(self, camera_id, group):
-        logging.debug('deleting picture group %(group)s of camera %(id)s' % {
-                'group': group, 'id': camera_id})
-
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            try:
-                mediafiles.del_media_group(camera_config, group, 'picture')
-                self.finish_json()
-                
-            except Exception as e:
-                self.finish_json({'error': unicode(e)})
-
-        elif utils.remote_camera(camera_config):
-            def on_response(response=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to delete picture group from %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                self.finish_json()
-
-            remote.del_media_group(camera_config, group=group, media_type='picture', callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    def try_finish(self, content):
-        try:
-            self.finish(content)
-            
-        except IOError as e:
-            logging.warning('could not write response: %(msg)s' % {'msg': unicode(e)})
-
-
-class MovieHandler(BaseHandler):
-    @asynchronous
-    def get(self, camera_id, op, filename=None):
-        if camera_id is not None:
-            camera_id = int(camera_id)
-            if camera_id not in config.get_camera_ids():
-                raise HTTPError(404, 'no such camera')
-        
-        if op == 'list':
-            self.list(camera_id)
-            
-        elif op == 'download':
-            self.download(camera_id, filename)
-        
-        elif op == 'preview':
-            self.preview(camera_id, filename)
-        
-        else:
-            raise HTTPError(400, 'unknown operation')
-    
-    @asynchronous
-    def post(self, camera_id, op, filename=None, group=None):
-        if camera_id is not None:
-            camera_id = int(camera_id)
-            if camera_id not in config.get_camera_ids():
-                raise HTTPError(404, 'no such camera')
-        
-        if op == 'delete':
-            self.delete(camera_id, filename)
-        
-        elif op == 'delete_all':
-            self.delete_all(camera_id, group)
-        
-        else:
-            raise HTTPError(400, 'unknown operation')
-    
-    @BaseHandler.auth()
-    def list(self, camera_id):
-        logging.debug('listing movies for camera %(id)s' % {'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            def on_media_list(media_list):
-                if media_list is None:
-                    return self.finish_json({'error': 'Failed to get movies list.'})
-
-                self.finish_json({
-                    'mediaList': media_list,
-                    'cameraName': camera_config['@name']
-                })
-            
-            mediafiles.list_media(camera_config, media_type='movie',
-                    callback=on_media_list, prefix=self.get_argument('prefix', None))
-        
-        elif utils.remote_camera(camera_config):
-            def on_response(remote_list=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to get movie list for %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                self.finish_json(remote_list)
-            
-            remote.list_media(camera_config, media_type='movie', prefix=self.get_argument('prefix', None), callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth()
-    def download(self, camera_id, filename):
-        logging.debug('downloading movie %(filename)s of camera %(id)s' % {
-                'filename': filename, 'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            content = mediafiles.get_media_content(camera_config, filename, 'movie')
-            
-            pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
-            self.set_header('Content-Type', 'video/mpeg')
-            self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
-            
-            self.finish(content)
-        
-        elif utils.remote_camera(camera_config):
-            def on_response(response=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to download movie from %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                pretty_filename = os.path.basename(filename) # no camera name available w/o additional request
-                self.set_header('Content-Type', 'video/mpeg')
-                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
-                
-                self.finish(response)
-
-            remote.get_media_content(camera_config, filename=filename, media_type='movie', callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth()
-    def preview(self, camera_id, filename):
-        logging.debug('previewing movie %(filename)s of camera %(id)s' % {
-                'filename': filename, 'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            content = mediafiles.get_media_preview(camera_config, filename, 'movie',
-                    width=self.get_argument('width', None),
-                    height=self.get_argument('height', None))
-            
-            if content:
-                self.set_header('Content-Type', 'image/jpeg')
-                
-            else:
-                self.set_header('Content-Type', 'image/svg+xml')
-                content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
-            
-            self.finish(content)
-        
-        elif utils.remote_camera(camera_config):
-            def on_response(content=None, error=None):
-                if content:
-                    self.set_header('Content-Type', 'image/jpeg')
-                    
-                else:
-                    self.set_header('Content-Type', 'image/svg+xml')
-                    content = open(os.path.join(settings.STATIC_PATH, 'img', 'no-preview.svg')).read()
-
-                self.finish(content)
-            
-            remote.get_media_preview(camera_config, filename=filename, media_type='movie',
-                    width=self.get_argument('width', None),
-                    height=self.get_argument('height', None),
-                    callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth(admin=True)
-    def delete(self, camera_id, filename):
-        logging.debug('deleting movie %(filename)s of camera %(id)s' % {
-                'filename': filename, 'id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            try:
-                mediafiles.del_media_content(camera_config, filename, 'movie')
-                self.finish_json()
-                
-            except Exception as e:
-                self.finish_json({'error': unicode(e)})
-
-        elif utils.remote_camera(camera_config):
-            def on_response(response=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to delete movie from %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                self.finish_json()
-
-            remote.del_media_content(camera_config, filename=filename, media_type='movie', callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-    @BaseHandler.auth(admin=True)
-    def delete_all(self, camera_id, group):
-        logging.debug('deleting movie group %(group)s of camera %(id)s' % {
-                'group': group, 'id': camera_id})
-
-        camera_config = config.get_camera(camera_id)
-        if utils.local_motion_camera(camera_config):
-            try:
-                mediafiles.del_media_group(camera_config, group, 'movie')
-                self.finish_json()
-                
-            except Exception as e:
-                self.finish_json({'error': unicode(e)})
-
-        elif utils.remote_camera(camera_config):
-            def on_response(response=None, error=None):
-                if error:
-                    return self.finish_json({'error': 'Failed to delete movie group from %(url)s: %(msg)s.' % {
-                            'url': remote.pretty_camera_url(camera_config), 'msg': error}})
-
-                self.finish_json()
-
-            remote.del_media_group(camera_config, group=group, media_type='movie', callback=on_response)
-
-        else: # assuming simple mjpeg camera
-            raise HTTPError(400, 'unknown operation')
-
-
-class RelayEventHandler(BaseHandler):
-    @BaseHandler.auth(admin=True)
-    def post(self):
-        event = self.get_argument('event')
-        thread_id = int(self.get_argument('thread_id'))
-        logging.debug('recevied relayed event %(event)s for thread id %(id)s' % {'event': event, 'id': thread_id})
-        
-        camera_id = motionctl.thread_id_to_camera_id(thread_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_motion_camera(camera_config):
-            logging.warn('ignoring event for non-local camera with id %s' % camera_id)
-            return self.finish_json()
-        
-        if event == 'start':
-            if not camera_config['@motion_detection']:
-                logging.debug('ignoring start event for camera with id %s and motion detection disabled' % camera_id)
-                return self.finish_json()
-
-            motionctl.set_motion_detected(camera_id, True)
-            
-        elif event == 'stop':
-            motionctl.set_motion_detected(camera_id, False)
-
-        else:
-            logging.warn('unknown event %s' % event)
-
-        self.finish_json()
-
-
-class LogHandler(BaseHandler):
-    LOGS = {
-        'motion': (os.path.join(settings.LOG_PATH, 'motion.log'),  'motion.log'),
-    }
-
-    @BaseHandler.auth(admin=True)
-    def get(self, name):
-        log = self.LOGS.get(name)
-        if log is None:
-            raise HTTPError(404, 'no such log')
-
-        (path, filename) = log
-
-        self.set_header('Content-Type', 'text/plain')
-        self.set_header('Content-Disposition', 'attachment; filename=' + filename + ';')
-
-        if path.startswith('/'): # an actual path        
-            logging.debug('serving log file "%s" from "%s"' % (filename, path))
-
-            with open(path) as f:
-                self.finish(f.read())
-                
-        else: # a command to execute 
-            logging.debug('serving log file "%s" from command "%s"' % (filename, path))
-
-            try:
-                output = subprocess.check_output(path, shell=True)
-            
-            except Exception as e:
-                output = 'failed to execute command: %s' % e
-                
-            self.finish(output)
-                
-
-class UpdateHandler(BaseHandler):
-    @BaseHandler.auth(admin=True)
-    def get(self):
-        logging.debug('listing versions')
-        
-        versions = update.get_all_versions()
-        current_version = update.get_version()
-        update_version = None
-        if versions and update.compare_versions(versions[-1], current_version) > 0:
-            update_version = versions[-1]
-
-        self.finish_json({
-            'update_version': update_version,
-            'current_version': current_version
-        })
-
-    @BaseHandler.auth(admin=True)
-    def post(self):
-        version = self.get_argument('version')
-        
-        logging.debug('performing update to version %(version)s' % {'version': version})
-        
-        result = update.perform_update(version)
-        
-        self.finish_json(result)
-
-
-class PowerHandler(BaseHandler):
-    @BaseHandler.auth(admin=True)
-    def post(self, op):
-        if op == 'shutdown':
-            self.shut_down()
-            
-        elif op == 'reboot':
-            self.reboot()
-    
-    def shut_down(self):
-        IOLoop.instance().add_timeout(datetime.timedelta(seconds=2), powerctl.shut_down)
-
-    def reboot(self):
-        IOLoop.instance().add_timeout(datetime.timedelta(seconds=2), powerctl.reboot)
-
-
-class VersionHandler(BaseHandler):
-    def get(self):
-        self.render('version.html',
-                version=update.get_version(),
-                hostname=socket.gethostname())
-
-    post = get
-
-
-# this will only trigger the login mechanism on the client side, if required 
-class LoginHandler(BaseHandler):
-    @BaseHandler.auth()
-    def get(self):
-        self.finish_json()
-
-    def post(self):
-        self.set_header('Content-Type', 'text/html')
-        self.finish()
diff --git a/src/mediafiles.py b/src/mediafiles.py
deleted file mode 100644 (file)
index c4f615e..0000000
+++ /dev/null
@@ -1,801 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import errno
-import fcntl
-import functools
-import hashlib
-import logging
-import multiprocessing
-import os.path
-import re
-import stat
-import StringIO
-import subprocess
-import time
-import tornado
-import zipfile
-
-from PIL import Image
-from tornado import ioloop
-
-import config
-import settings
-import utils
-
-
-_PICTURE_EXTS = ['.jpg']
-_MOVIE_EXTS = ['.avi', '.mp4']
-
-# a cache list of paths to movies without preview
-_previewless_movie_files = []
-
-# a cache of prepared files (whose preparing time is significant)
-_prepared_files = {}
-
-_timelapse_process = None
-_timelapse_data = None
-
-
-def _list_media_files(dir, exts, prefix=None):
-    media_files = []
-    
-    if prefix is not None:
-        if prefix == 'ungrouped':
-            prefix = ''
-        
-        root = os.path.join(dir, prefix)
-        for name in os.listdir(root):
-            if name == 'lastsnap.jpg' or name.startswith('.'): # ignore the lastsnap.jpg and hidden files
-                continue
-                
-            full_path = os.path.join(root, name)
-            try:
-                st = os.stat(full_path)
-            
-            except Exception as e:
-                logging.error('stat failed: ' + unicode(e))
-                continue
-                
-            if not stat.S_ISREG(st.st_mode): # not a regular file
-                continue
-
-            full_path_lower = full_path.lower()
-            if not [e for e in exts if full_path_lower.endswith(e)]:
-                continue
-            
-            media_files.append((full_path, st))
-
-    else:    
-        for root, dirs, files in os.walk(dir):  # @UnusedVariable # TODO os.walk can be rewritten to return stat info
-            for name in files:
-                if name == 'lastsnap.jpg' or name.startswith('.'): # ignore the lastsnap.jpg and hidden files
-                    continue
-                
-                full_path = os.path.join(root, name)
-                try:
-                    st = os.stat(full_path)
-                
-                except Exception as e:
-                    logging.error('stat failed: ' + unicode(e))
-                    continue
-                
-                if not stat.S_ISREG(st.st_mode): # not a regular file
-                    continue
-                 
-                full_path_lower = full_path.lower()
-                if not [e for e in exts if full_path_lower.endswith(e)]:
-                    continue
-                
-                media_files.append((full_path, st))
-
-    return media_files
-
-
-def _remove_older_files(dir, moment, exts):
-    for (full_path, st) in _list_media_files(dir, exts):
-        file_moment = datetime.datetime.fromtimestamp(st.st_mtime)
-        if file_moment < moment:
-            logging.debug('removing file %(path)s...' % {'path': full_path})
-            
-            # remove the file itself
-            try:
-                os.remove(full_path)
-            
-            except OSError as e:
-                if e.errno == errno.ENOENT:
-                    pass # the file might have been removed in the meantime
-                
-                else:
-                    logging.error('failed to remove %s: %s' % (full_path, e))
-
-            # remove the parent directories if empty or contain only thumb files
-            dir_path = os.path.dirname(full_path)
-            if not os.path.exists(dir_path):
-                continue
-            
-            listing = os.listdir(dir_path)
-            thumbs = [l for l in listing if l.endswith('.thumb')]
-            
-            if len(listing) == len(thumbs): # only thumbs
-                for p in thumbs:
-                    try:
-                        os.remove(os.path.join(dir_path, p))
-                    
-                    except:
-                        logging.error('failed to remove %s: %s' % (p, e))
-
-            if not listing or len(listing) == len(thumbs):
-                # this will possibly cause following paths that are in the media files for loop
-                # to be removed in advance; the os.remove call will raise ENOENT which is silently ignored 
-                logging.debug('removing empty directory %(path)s...' % {'path': dir_path})
-                try:
-                    os.removedirs(dir_path)
-                
-                except:
-                    logging.error('failed to remove %s: %s' % (dir_path, e))
-
-
-def find_ffmpeg():
-    try:
-        return subprocess.check_output('which ffmpeg', shell=True).strip()
-    
-    except subprocess.CalledProcessError: # not found
-        return None
-
-
-def cleanup_media(media_type):
-    logging.debug('cleaning up %(media_type)ss...' % {'media_type': media_type})
-    
-    if media_type == 'picture':
-        exts = _PICTURE_EXTS
-        
-    elif media_type == 'movie':
-        exts = _MOVIE_EXTS + ['.thumb']
-        
-    for camera_id in config.get_camera_ids():
-        camera_config = config.get_camera(camera_id)
-        if not utils.local_motion_camera(camera_config):
-            continue
-        
-        preserve_media = camera_config.get('@preserve_%(media_type)ss' % {'media_type': media_type}, 0)
-        if preserve_media == 0:
-            return # preserve forever
-        
-        still_images_enabled = bool(
-                ((camera_config['emulate_motion'] or camera_config['output_pictures']) and camera_config['picture_filename']) or
-                (camera_config['snapshot_interval'] and camera_config['snapshot_filename']))
-        
-        movies_enabled = camera_config['ffmpeg_output_movies']
-
-        if media_type == 'picture' and not still_images_enabled:
-            continue # only cleanup pictures for cameras with still images enabled
-        
-        elif media_type == 'movie' and not movies_enabled:
-            continue # only cleanup movies for cameras with movies enabled
-
-        preserve_moment = datetime.datetime.now() - datetime.timedelta(days=preserve_media)
-            
-        target_dir = camera_config.get('target_dir')
-        _remove_older_files(target_dir, preserve_moment, exts=exts)
-
-
-def make_movie_preview(camera_config, full_path):
-    framerate = camera_config['framerate']
-    pre_capture = camera_config['pre_capture']
-    offs = pre_capture / framerate
-    offs = max(4, offs * 2)
-    
-    logging.debug('creating movie preview for %(path)s with an offset of %(offs)s seconds...' % {
-            'path': full_path, 'offs': offs})
-
-    cmd = 'ffmpeg -i "%(path)s" -f mjpeg -vframes 1 -ss %(offs)s -y %(path)s.thumb'
-    
-    try:
-        subprocess.check_output(cmd % {'path': full_path, 'offs': offs}, shell=True, stderr=subprocess.STDOUT)
-    
-    except subprocess.CalledProcessError as e:
-        logging.error('failed to create movie preview for %(path)s: %(msg)s' % {
-                'path': full_path, 'msg': unicode(e)})
-        
-        return None
-    
-    try:
-        st = os.stat(full_path + '.thumb')
-    
-    except os.error:
-        logging.error('failed to create movie preview for %(path)s: ffmpeg error' % {
-                'path': full_path})
-
-        return None
-
-    if st.st_size == 0:
-        logging.debug('movie is too short, grabbing first frame from %(path)s...' % {'path': full_path})
-        
-        # try again, this time grabbing the very first frame
-        try:
-            subprocess.check_output(cmd % {'path': full_path, 'offs': 0}, shell=True, stderr=subprocess.STDOUT)
-        
-        except subprocess.CalledProcessError as e:
-            logging.error('failed to create movie preview for %(path)s: %(msg)s' % {
-                    'path': full_path, 'msg': unicode(e)})
-            
-            return None
-    
-    return full_path + '.thumb'
-
-
-def make_next_movie_preview():
-    global _previewless_movie_files
-    
-    logging.debug('making preview for the next movie...')
-    
-    if _previewless_movie_files:
-        (camera_config, path) = _previewless_movie_files.pop(0)
-        
-        make_movie_preview(camera_config, path)
-    
-    else:
-        logging.debug('gathering movies without preview...')
-        
-        count = 0
-        for camera_id in config.get_camera_ids():
-            camera_config = config.get_camera(camera_id)
-            if not utils.local_motion_camera(camera_config):
-                continue
-            
-            target_dir = camera_config['target_dir']
-            
-            for (full_path, st) in _list_media_files(target_dir, _MOVIE_EXTS):  # @UnusedVariable
-                if os.path.exists(full_path + '.thumb'):
-                    continue
-                
-                logging.debug('found a movie without preview: %(path)s' % {
-                        'path': full_path})
-                
-                _previewless_movie_files.append((camera_config, full_path))
-                count += 1
-        
-        logging.debug('found %(count)d movies without preview' % {'count': count})    
-        
-        if count:
-            make_next_movie_preview()
-
-
-def list_media(camera_config, media_type, callback, prefix=None):
-    target_dir = camera_config.get('target_dir')
-
-    if media_type == 'picture':
-        exts = _PICTURE_EXTS
-        
-    elif media_type == 'movie':
-        exts = _MOVIE_EXTS
-
-    # create a subprocess to retrieve media files
-    def do_list_media(pipe):
-        mf = _list_media_files(target_dir, exts=exts, prefix=prefix)
-        for (p, st) in mf:
-            path = p[len(target_dir):]
-            if not path.startswith('/'):
-                path = '/' + path
-    
-            timestamp = st.st_mtime
-            size = st.st_size
-            
-            pipe.send({
-                'path': path,
-                'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp)),
-                'momentStrShort': utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp), short=True),
-                'sizeStr': utils.pretty_size(size),
-                'timestamp': timestamp
-            })
-        
-        pipe.close()
-    
-    logging.debug('starting media listing process...')
-    
-    (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
-    process = multiprocessing.Process(target=do_list_media, args=(child_pipe, ))
-    process.start()
-    
-    # poll the subprocess to see when it has finished
-    started = datetime.datetime.now()
-    media_list = []
-    
-    def read_media_list():
-        while parent_pipe.poll():
-            media_list.append(parent_pipe.recv())
-    
-    def poll_process():
-        ioloop = tornado.ioloop.IOLoop.instance()
-        if process.is_alive(): # not finished yet
-            now = datetime.datetime.now()
-            delta = now - started
-            if delta.seconds < 120:
-                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process)
-                read_media_list()
-            
-            else: # process did not finish within 2 minutes
-                logging.error('timeout waiting for the media listing process to finish')
-                
-                callback(None)
-
-        else: # finished
-            read_media_list()
-            logging.debug('media listing process has returned %(count)s files' % {'count': len(media_list)})
-            callback(media_list)
-    
-    poll_process()
-
-
-def get_media_content(camera_config, path, media_type):
-    target_dir = camera_config.get('target_dir')
-
-    full_path = os.path.join(target_dir, path)
-    
-    try:
-        with open(full_path) as f:
-            return f.read()
-    
-    except Exception as e:
-        logging.error('failed to read file %(path)s: %(msg)s' % {
-                'path': full_path, 'msg': unicode(e)})
-        
-        return None
-
-
-def get_zipped_content(camera_config, media_type, group, callback):
-    target_dir = camera_config.get('target_dir')
-
-    if media_type == 'picture':
-        exts = _PICTURE_EXTS
-        
-    elif media_type == 'movie':
-        exts = _MOVIE_EXTS
-        
-    working = multiprocessing.Value('b')
-    working.value = True
-
-    # create a subprocess to add files to zip
-    def do_zip(pipe):
-        mf = _list_media_files(target_dir, exts=exts, prefix=group)
-        paths = []
-        for (p, st) in mf:  # @UnusedVariable
-            path = p[len(target_dir):]
-            if path.startswith('/'):
-                path = path[1:]
-
-            paths.append(path)
-            
-        zip_filename = os.path.join(settings.MEDIA_PATH, '.zip-%s' % int(time.time()))
-        logging.debug('adding %d files to zip file "%s"' % (len(paths), zip_filename))
-
-        try:
-            with zipfile.ZipFile(zip_filename, mode='w') as f:
-                for path in paths:
-                    full_path = os.path.join(target_dir, path)
-                    f.write(full_path, path)
-
-        except Exception as e:
-            logging.error('failed to create zip file "%s": %s' % (zip_filename, e))
-
-            working.value = False
-            pipe.close()
-            return
-
-        logging.debug('reading zip file "%s" into memory' % zip_filename)
-
-        try:
-            with open(zip_filename, mode='r') as f:
-                data = f.read()
-
-            working.value = False
-            pipe.send(data)
-            logging.debug('zip data ready')
-
-        except Exception as e:
-            logging.error('failed to read zip file "%s": %s' % (zip_filename, e))
-            working.value = False
-
-        finally:
-            os.remove(zip_filename)
-            pipe.close()
-
-    logging.debug('starting zip process...')
-
-    (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
-    process = multiprocessing.Process(target=do_zip, args=(child_pipe, ))
-    process.start()
-
-    # poll the subprocess to see when it has finished
-    started = datetime.datetime.now()
-
-    def poll_process():
-        ioloop = tornado.ioloop.IOLoop.instance()
-        if working.value:
-            now = datetime.datetime.now()
-            delta = now - started
-            if delta.seconds < settings.ZIP_TIMEOUT:
-                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process)
-
-            else: # process did not finish within 2 minutes
-                logging.error('timeout waiting for the zip process to finish')
-
-                callback(None)
-
-        else: # finished
-            try:
-                data = parent_pipe.recv()
-                logging.debug('zip process has returned %d bytes' % len(data))
-                
-            except:
-                data = None
-            
-            callback(data)
-
-    poll_process()
-
-
-def make_timelapse_movie(camera_config, framerate, interval, group):
-    global _timelapse_process
-    global _timelapse_data
-    
-    target_dir = camera_config.get('target_dir')
-    
-    # create a subprocess to retrieve media files
-    def do_list_media(pipe):
-        mf = _list_media_files(target_dir, exts=_PICTURE_EXTS, prefix=group)
-        for (p, st) in mf:
-            timestamp = st.st_mtime
-
-            pipe.send({
-                'path': p,
-                'timestamp': timestamp
-            })
-
-        pipe.close()
-
-    logging.debug('starting media listing process...')
-    
-    (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
-    _timelapse_process = multiprocessing.Process(target=do_list_media, args=(child_pipe, ))
-    _timelapse_process.progress = 0
-    _timelapse_process.start()
-    _timelapse_data = None
-
-    started = [datetime.datetime.now()]
-    media_list = []
-    
-    tmp_filename = os.path.join(settings.MEDIA_PATH, '.%s.avi' % int(time.time()))
-
-    def read_media_list():
-        while parent_pipe.poll():
-            media_list.append(parent_pipe.recv())
-        
-    def poll_media_list_process():
-        ioloop = tornado.ioloop.IOLoop.instance()
-        if _timelapse_process.is_alive(): # not finished yet
-            now = datetime.datetime.now()
-            delta = now - started[0]
-            if delta.seconds < 300: # the subprocess has 5 minutes to complete its job
-                ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_media_list_process)
-                read_media_list()
-
-            else: # process did not finish within 2 minutes
-                logging.error('timeout waiting for the media listing process to finish')
-                
-                _timelapse_process.progress = -1
-
-        else: # finished
-            read_media_list()
-            logging.debug('media listing process has returned %(count)s files' % {'count': len(media_list)})
-            
-            if not media_list:
-                _timelapse_process.progress = -1
-                
-                return
-
-            pictures = select_pictures(media_list)
-            make_movie(pictures)
-
-    def select_pictures(media_list):
-        media_list.sort(key=lambda e: e['timestamp'])
-        start = media_list[0]['timestamp']
-        slices = {}
-        max_idx = 0
-        for m in media_list:
-            offs = m['timestamp'] - start
-            pos = float(offs) / interval - 0.5
-            idx = int(round(pos))
-            max_idx = idx
-            m['delta'] = abs(pos - idx)
-            slices.setdefault(idx, []).append(m)
-
-        selected = []
-        for i in xrange(max_idx + 1):
-            slice = slices.get(i)
-            if not slice:
-                continue
-
-            selected.append(min(slice, key=lambda m: m['delta']))
-
-        logging.debug('selected %d/%d media files' % (len(selected), len(media_list)))
-        
-        return selected
-
-    def make_movie(pictures):
-        global _timelapse_process
-
-        cmd =  'rm -f %(tmp_filename)s;'
-        cmd += 'cat %(jpegs)s | ffmpeg -framerate %(framerate)s -f image2pipe -vcodec mjpeg -i - -vcodec mpeg4 -b:v %(bitrate)s -qscale:v 0.1 -f avi %(tmp_filename)s'
-
-        bitrate = 9999999
-
-        cmd = cmd % {
-            'tmp_filename': tmp_filename,
-            'jpegs': ' '.join((('"' + p['path'] + '"') for p in pictures)),
-            'framerate': framerate,
-            'bitrate': bitrate
-        }
-        
-        logging.debug('executing "%s"' % cmd)
-        
-        _timelapse_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
-        _timelapse_process.progress = 0.01 # 1%
-        
-        # make subprocess stdout pipe non-blocking
-        fd = _timelapse_process.stdout.fileno()
-        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
-        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
-        poll_movie_process(pictures)
-
-    def poll_movie_process(pictures):
-        global _timelapse_process
-        global _timelapse_data
-        
-        ioloop = tornado.ioloop.IOLoop.instance()
-        if _timelapse_process.poll() is None: # not finished yet
-            ioloop.add_timeout(datetime.timedelta(seconds=0.5), functools.partial(poll_movie_process, pictures))
-
-            try:
-                output = _timelapse_process.stdout.read()
-            
-            except IOError as e:
-                if e.errno == errno.EAGAIN:
-                    output = ''
-                
-                else:
-                    raise
-                
-            frame_index = re.findall('frame=\s*(\d+)', output)
-            try:
-                frame_index = int(frame_index[-1])
-            
-            except (IndexError, ValueError):
-                return
-
-            _timelapse_process.progress = max(0.01, float(frame_index) / len(pictures))
-            
-            logging.debug('timelapse progress: %s' % int(100 * _timelapse_process.progress))
-
-        else: # finished
-            exit_code = _timelapse_process.poll()
-            _timelapse_process = None
-            
-            if exit_code != 0:
-                logging.error('ffmpeg process failed')
-                _timelapse_data = None
-
-                try:
-                    os.remove(tmp_filename)
-
-                except:
-                    pass
-
-            else:
-                logging.debug('reading timelapse movie file "%s" into memory' % tmp_filename)
-    
-                try:
-                    with open(tmp_filename, mode='r') as f:
-                        _timelapse_data = f.read()
-    
-                    logging.debug('timelapse movie process has returned %d bytes' % len(_timelapse_data))
-    
-                except Exception as e:
-                    logging.error('failed to read timelapse movie file "%s": %s' % (tmp_filename, e))
-    
-                finally:
-                    try:
-                        os.remove(tmp_filename)
-
-                    except:
-                        pass
-
-    poll_media_list_process()
-
-
-def check_timelapse_movie():
-    if _timelapse_process:
-        if ((hasattr(_timelapse_process, 'poll') and _timelapse_process.poll() is None) or
-            (hasattr(_timelapse_process, 'is_alive') and _timelapse_process.is_alive())):
-        
-            return {'progress': _timelapse_process.progress, 'data': None}
-        
-        else:
-            return {'progress': _timelapse_process.progress, 'data': _timelapse_data}
-
-    else:
-        return {'progress': -1, 'data': _timelapse_data}
-
-
-def get_media_preview(camera_config, path, media_type, width, height):
-    target_dir = camera_config.get('target_dir')
-    full_path = os.path.join(target_dir, path)
-    
-    if media_type == 'movie':
-        if not os.path.exists(full_path + '.thumb'):
-            if not make_movie_preview(camera_config, full_path):
-                return None
-        
-        full_path += '.thumb'
-    
-    try:
-        with open(full_path) as f:
-            content = f.read()
-    
-    except Exception as e:
-        logging.error('failed to read file %(path)s: %(msg)s' % {
-                'path': full_path, 'msg': unicode(e)})
-        
-        return None
-    
-    if width is height is None:
-        return content
-    
-    sio = StringIO.StringIO(content)
-    image = Image.open(sio)
-    width = width and int(width) or image.size[0]
-    height = height and int(height) or image.size[1]
-    
-    image.thumbnail((width, height), Image.LINEAR)
-
-    sio = StringIO.StringIO()
-    image.save(sio, format='JPEG')
-
-    return sio.getvalue()
-
-
-def del_media_content(camera_config, path, media_type):
-    target_dir = camera_config.get('target_dir')
-
-    full_path = os.path.join(target_dir, path)
-    
-    try:
-        # remove the file itself
-        os.remove(full_path)
-
-        # remove the parent directories if empty or contains only thumb files
-        dir_path = os.path.dirname(full_path)
-        listing = os.listdir(dir_path)
-        thumbs = [l for l in listing if l.endswith('.thumb')]
-        
-        if len(listing) == len(thumbs): # only thumbs
-            for p in thumbs:
-                os.remove(os.path.join(dir_path, p))
-
-        if not listing or len(listing) == len(thumbs):
-            logging.debug('removing empty directory %(path)s...' % {'path': dir_path})
-            os.removedirs(dir_path)
-    
-    except Exception as e:
-        logging.error('failed to remove file %(path)s: %(msg)s' % {
-                'path': full_path, 'msg': unicode(e)})
-        
-        raise
-
-
-def del_media_group(camera_config, group, media_type):
-    if media_type == 'picture':
-        exts = _PICTURE_EXTS
-        
-    elif media_type == 'movie':
-        exts = _MOVIE_EXTS
-        
-    target_dir = camera_config.get('target_dir')
-    full_path = os.path.join(target_dir, group)
-
-    mf = _list_media_files(target_dir, exts=exts, prefix=group)
-    for (path, st) in mf:  # @UnusedVariable
-        try:
-            os.remove(path)
-    
-        except Exception as e:
-            logging.error('failed to remove file %(path)s: %(msg)s' % {
-                    'path': full_path, 'msg': unicode(e)})
-
-            raise
-
-    # remove the group directory if empty or contains only thumb files
-    listing = os.listdir(full_path)
-    thumbs = [l for l in listing if l.endswith('.thumb')]
-
-    if len(listing) == len(thumbs): # only thumbs
-        for p in thumbs:
-            os.remove(os.path.join(full_path, p))
-
-    if not listing or len(listing) == len(thumbs):
-        logging.debug('removing empty directory %(path)s...' % {'path': full_path})
-        os.removedirs(full_path)
-
-
-def get_current_picture(camera_config, width, height):
-    import mjpgclient
-
-    jpg = mjpgclient.get_jpg(camera_config['@id'])
-    
-    if jpg is None:
-        return None
-    
-    if width is height is None:
-        return jpg # no server-side resize needed
-
-    sio = StringIO.StringIO(jpg)
-    image = Image.open(sio)
-    
-    width = width and int(width) or image.size[0]
-    height = height and int(height) or image.size[1]
-    
-    webcam_resolution = camera_config['@webcam_resolution']
-    max_width = image.size[0] * webcam_resolution / 100
-    max_height = image.size[1] * webcam_resolution / 100
-    
-    width = min(max_width, width)
-    height = min(max_height, height)
-    
-    if width >= image.size[0] and height >= image.size[1]:
-        return jpg # no enlarging of the picture on the server side
-    
-    image.thumbnail((width, height), Image.CUBIC)
-
-    sio = StringIO.StringIO()
-    image.save(sio, format='JPEG')
-
-    return sio.getvalue()
-
-
-def get_prepared_cache(key):
-    return _prepared_files.pop(key, None)
-
-
-def set_prepared_cache(data):
-    key = hashlib.sha1(str(time.time())).hexdigest()
-
-    if key in _prepared_files:
-        logging.warn('key "%s" already present in prepared cache' % key)
-        
-    _prepared_files[key] = data
-    
-    def clear():
-        if _prepared_files.pop(key, None) is not None:
-            logging.warn('key "%s" was still present in the prepared cache, removed' % key)
-
-    timeout = 3600 # the user has 1 hour to download the file after creation
-    ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=timeout), clear)
-
-    return key
diff --git a/src/mjpgclient.py b/src/mjpgclient.py
deleted file mode 100644 (file)
index 088a962..0000000
+++ /dev/null
@@ -1,301 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import errno
-import logging
-import re
-import socket
-import time
-
-from tornado import iostream, ioloop
-
-import config
-import motionctl
-import settings
-import utils
-
-
-class MjpgClient(iostream.IOStream):
-    clients = {} # dictionary of clients indexed by camera id
-    last_jpgs = {} # dictionary of jpg contents indexed by camera id
-    last_jpg_moment = {} # dictionary of moments of the last received jpeg indexed by camera id
-    last_access = {} # dictionary of access moments indexed by camera id
-    last_erroneous_close_time = 0 # helps detecting erroneous connections and restart motion
-    
-    def __init__(self, camera_id, port, username, password):
-        self._camera_id = camera_id
-        self._port = port
-        self._username = (username or '').encode('utf8')
-        self._password = (password or '').encode('utf8')
-        self._auth_digest_state = {}
-        
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-        iostream.IOStream.__init__(self, s)
-        
-        self.set_close_callback(self.on_close)
-        
-    def connect(self):
-        iostream.IOStream.connect(self, ('localhost', self._port), self._on_connect)
-        MjpgClient.clients[self._camera_id] = self
-        
-        logging.debug('mjpg client for camera %(camera_id)s connecting on port %(port)s...' % {
-                'port': self._port, 'camera_id': self._camera_id})
-    
-    def on_close(self):
-        logging.debug('connection closed for mjpg client for camera %(camera_id)s on port %(port)s' % {
-                'port': self._port, 'camera_id': self._camera_id})
-        
-        if MjpgClient.clients.pop(self._camera_id, None):
-            MjpgClient.last_access.pop(self._camera_id, None)
-            MjpgClient.last_jpg_moment.pop(self._camera_id, None)
-             
-            logging.debug('mjpg client for camera %(camera_id)s on port %(port)s removed' % {
-                    'port': self._port, 'camera_id': self._camera_id})
-         
-        if getattr(self, 'error', None) and self.error.errno != errno.ECONNREFUSED:
-            now = time.time()
-            if now - MjpgClient.last_erroneous_close_time < settings.MJPG_CLIENT_TIMEOUT:
-                logging.error('connection problem detected for mjpg client for camera %(camera_id)s on port %(port)s' % {
-                        'port': self._port, 'camera_id': self._camera_id})
-                motionctl.stop(invalidate=True) # this will close all the mjpg clients
-                motionctl.start(deferred=True)
-            MjpgClient.last_erroneous_close_time = now
-
-    def _check_error(self):
-        if self.socket is None:
-            logging.warning('mjpg client connection for camera %(camera_id)s on port %(port)s is closed' % {
-                    'port': self._port, 'camera_id': self._camera_id})
-            
-            self.close()
-            
-            return True
-            
-        error = getattr(self, 'error', None)
-        if (error is None) or (getattr(error, 'errno', None) == 0): # error could also be ESUCCESS for some reason
-            return False
-        
-        self._error(error)
-        
-        return True
-     
-    def _error(self, error):
-        logging.error('mjpg client for camera %(camera_id)s on port %(port)s error: %(msg)s' % {
-                'port': self._port, 'camera_id': self._camera_id, 'msg': unicode(error)})
-        
-        try:
-            self.close()
-        
-        except:
-            pass
-    
-    def _on_connect(self):
-        logging.debug('mjpg client for camera %(camera_id)s connected on port %(port)s' % {
-                'port': self._port, 'camera_id': self._camera_id})
-        
-        if self._username:
-            auth_header = utils.build_basic_header(self._username, self._password)
-            self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
-            
-        else:
-            self.write('GET / HTTP/1.0\r\n\r\n')
-
-        self._seek_http()
-
-    def _seek_http(self):
-        if self._check_error():
-            return
-        
-        self.read_until_regex('HTTP/1.\d \d+ ', self._on_http)
-
-    def _on_http(self, data):
-        if data.endswith('401 '):
-            self._seek_www_authenticate()
-
-        else: # no authorization required, skip to content length
-            self._seek_content_length()
-
-    def _seek_www_authenticate(self):
-        if self._check_error():
-            return
-        
-        self.read_until('WWW-Authenticate:', self._on_before_www_authenticate)
-
-    def _on_before_www_authenticate(self, data):
-        if self._check_error():
-            return
-        
-        self.read_until('\r\n', self._on_www_authenticate)
-    
-    def _on_www_authenticate(self, data):
-        if self._check_error():
-            return
-        
-        m = re.match('Basic\s*realm="([a-zA-Z0-9\-\s]+)"', data.strip())
-        if m:
-            logging.debug('mjpgclient: using basic authentication')
-            
-            auth_header = utils.build_basic_header(self._username, self._password)
-            self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
-            self._seek_http()
-
-            return
-
-        m = re.match('Digest\s*realm="([a-zA-Z0-9\-\s]+)",\s*nonce="([a-zA-Z0-9]+)"', data.strip())
-        if m:
-            logging.debug('mjpgclient: using digest authentication')
-
-            realm, nonce = m.groups()
-            self._auth_digest_state['realm'] = realm
-            self._auth_digest_state['nonce'] = nonce
-    
-            auth_header = utils.build_digest_header('GET', '/', self._username, self._password, self._auth_digest_state)
-            self.write('GET / HTTP/1.0\r\n\r\nAuthorization: %s\r\n\r\n' % auth_header)
-            self._seek_http()
-            
-            return
-
-        logging.error('mjpgclient: unknown authentication header: "%s"' % data)
-        self._seek_content_length()
-
-    def _seek_content_length(self):
-        if self._check_error():
-            return
-        
-        self.read_until('Content-Length:', self._on_before_content_length)
-    
-    def _on_before_content_length(self, data):
-        if self._check_error():
-            return
-        
-        self.read_until('\r\n\r\n', self._on_content_length)
-    
-    def _on_content_length(self, data):
-        if self._check_error():
-            return
-        
-        matches = re.findall('(\d+)', data)
-        if not matches:
-            self._error('could not find content length in mjpg header line "%(header)s"' % {
-                    'header': data})
-            
-            return
-        
-        length = int(matches[0])
-        
-        self.read_bytes(length, self._on_jpg)
-    
-    def _on_jpg(self, data):
-        MjpgClient.last_jpgs[self._camera_id] = data
-        MjpgClient.last_jpg_moment[self._camera_id] = datetime.datetime.utcnow()
-        self._seek_content_length()
-
-
-def _garbage_collector():
-    logging.debug('running garbage collector for mjpg clients...')
-    
-    now = datetime.datetime.utcnow()
-    for client in MjpgClient.clients.values():
-        camera_id = client._camera_id
-        port = client._port
-        
-        # check for last jpg moment timeout
-        last_jpg_moment = MjpgClient.last_jpg_moment.get(camera_id)
-        if last_jpg_moment is None:
-            MjpgClient.last_jpg_moment[camera_id] = now
-            
-            continue
-        
-        if client.closed():
-            continue
-
-        delta = now - last_jpg_moment
-        delta = delta.days * 86400 + delta.seconds
-        
-        if delta > settings.MJPG_CLIENT_TIMEOUT:
-            logging.error('mjpg client timed out receiving data for camera %(camera_id)s on port %(port)s' % {
-                    'camera_id': camera_id, 'port': port})
-            
-            motionctl.stop(invalidate=True) # this will close all the mjpg clients
-            motionctl.start(deferred=True)
-            
-            break
-
-        # check for last access timeout
-        last_access = MjpgClient.last_access.get(camera_id)
-        if last_access is None:
-            continue
-        
-        delta = now - last_access
-        delta = delta.days * 86400 + delta.seconds
-        
-        if settings.MJPG_CLIENT_IDLE_TIMEOUT and delta > settings.MJPG_CLIENT_IDLE_TIMEOUT:
-            logging.debug('mjpg client for camera %(camera_id)s on port %(port)s has been idle for %(timeout)s seconds, removing it' % {
-                    'camera_id': camera_id, 'port': port, 'timeout': settings.MJPG_CLIENT_IDLE_TIMEOUT})
-            
-            client.close()
-
-            continue
-        
-    io_loop = ioloop.IOLoop.instance()
-    io_loop.add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT), _garbage_collector)
-
-
-def get_jpg(camera_id):
-    if camera_id not in MjpgClient.clients:
-        # mjpg client not started yet for this camera
-        
-        logging.debug('creating mjpg client for camera %(camera_id)s' % {
-                'camera_id': camera_id})
-        
-        camera_config = config.get_camera(camera_id)
-        if not camera_config['@enabled'] or not utils.local_motion_camera(camera_config):
-            logging.error('could not start mjpg client for camera id %(camera_id)s: not enabled or not local' % {
-                    'camera_id': camera_id})
-            
-            return None
-        
-        port = camera_config['stream_port']
-        username, password = None, None
-        if camera_config.get('stream_auth_method') > 0:
-            username, password = camera_config.get('stream_authentication', ':').split(':')
-
-        client = MjpgClient(camera_id, port, username, password)
-        client.connect()
-
-    MjpgClient.last_access[camera_id] = datetime.datetime.utcnow()
-    
-    return MjpgClient.last_jpgs.get(camera_id)
-
-
-def close_all(invalidate=False):
-    for client in MjpgClient.clients.values():
-        client.close()
-    
-    if invalidate:
-        MjpgClient.clients = {}
-        MjpgClient.last_jpgs = {}
-        MjpgClient.last_jpg_moment = {}
-        MjpgClient.last_access = {}
-        MjpgClient.last_erroneous_close_time = 0
-
-
-# schedule the garbage collector
-io_loop = ioloop.IOLoop.instance()
-io_loop.add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT), _garbage_collector)
diff --git a/src/motionctl.py b/src/motionctl.py
deleted file mode 100644 (file)
index df27ae6..0000000
+++ /dev/null
@@ -1,332 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import errno
-import logging
-import os.path
-import re
-import signal
-import subprocess
-import time
-
-from tornado.httpclient import HTTPClient, AsyncHTTPClient, HTTPRequest
-from tornado.ioloop import IOLoop
-
-import config
-import mjpgclient
-import powerctl
-import settings
-import utils
-
-
-_started = False
-_motion_binary_cache = None
-_motion_detected = {}
-
-
-def find_motion():
-    global _motion_binary_cache
-    if _motion_binary_cache:
-        return _motion_binary_cache
-    
-    if settings.MOTION_BINARY:
-        if os.path.exists(settings.MOTION_BINARY):
-            binary = settings.MOTION_BINARY
-        
-        else:
-            return None
-
-    else: # autodetect motion binary path
-        try:
-            binary = subprocess.check_output('which motion', shell=True).strip()
-        
-        except subprocess.CalledProcessError: # not found
-            return None
-
-    try:
-        help = subprocess.check_output(binary + ' -h || true', shell=True)
-    
-    except subprocess.CalledProcessError: # not found
-        return None
-    
-    result = re.findall('^motion Version ([^,]+)', help)
-    version = result and result[0] or ''
-    
-    _motion_binary_cache = (binary, version)
-    
-    return _motion_binary_cache
-
-
-def _disable_initial_motion_detection():
-    for camera_id in config.get_camera_ids():
-        camera_config = config.get_camera(camera_id)
-        if not utils.local_motion_camera(camera_config):
-            continue
-
-        if not camera_config['@motion_detection']:
-            logging.debug('motion detection disabled by config for camera with id %s' % camera_id)
-            set_motion_detection(camera_id, False)
-
-
-def start(deferred=False):
-    if deferred:
-        return IOLoop.instance().add_callback(start, deferred=False)
-
-    global _started
-    
-    _started = True
-    
-    enabled_local_motion_cameras = config.get_enabled_local_motion_cameras()
-    if running() or not enabled_local_motion_cameras:
-        return
-    
-    logging.debug('starting motion')
-    program = find_motion()
-    if not program:
-        raise Exception('motion executable could not be found')
-    
-    program, version = program  # @UnusedVariable
-    
-    logging.debug('using motion binary "%s"' % program)
-
-    motion_config_path = os.path.join(settings.CONF_PATH, 'motion.conf')
-    motion_log_path = os.path.join(settings.LOG_PATH, 'motion.log')
-    motion_pid_path = os.path.join(settings.RUN_PATH, 'motion.pid')
-    
-    args = [program,
-            '-c', motion_config_path,
-            '-n',
-            '-d']
-    
-    if settings.LOG_LEVEL == logging.DEBUG:
-        args.append('9')
-    
-    else:
-        args.append('1')
-
-    log_file = open(motion_log_path, 'w')
-    
-    process = subprocess.Popen(args, stdout=log_file, stderr=log_file, close_fds=True, cwd=settings.CONF_PATH)
-    
-    # wait 2 seconds to see that the process has successfully started
-    for i in xrange(20):  # @UnusedVariable
-        time.sleep(0.1)
-        exit_code = process.poll()
-        if exit_code is not None and exit_code != 0:
-            raise Exception('motion failed to start')
-
-    pid = process.pid
-    
-    # write the pid to file
-    with open(motion_pid_path, 'w') as f:
-        f.write(str(pid) + '\n')
-    
-    _disable_initial_motion_detection()
-    
-    # if mjpg client idle timeout is disabled, create mjpg clients for all cameras by default
-    if not settings.MJPG_CLIENT_IDLE_TIMEOUT:
-        logging.debug('creating default mjpg clients for local cameras')
-        for camera in enabled_local_motion_cameras:
-            mjpgclient.get_jpg(camera['@id'])
-
-
-def stop(invalidate=False):
-    global _started
-    
-    _started = False
-    
-    if not running():
-        return
-    
-    logging.debug('stopping motion')
-
-    mjpgclient.close_all(invalidate=invalidate)
-    
-    pid = _get_pid()
-    if pid is not None:
-        try:
-            # send the TERM signal once
-            os.kill(pid, signal.SIGTERM)
-            
-            # wait 5 seconds for the process to exit
-            for i in xrange(50):  # @UnusedVariable
-                os.waitpid(pid, os.WNOHANG)
-                time.sleep(0.1)
-
-            # send the KILL signal once
-            os.kill(pid, signal.SIGKILL)
-            
-            # wait 2 seconds for the process to exit
-            for i in xrange(20):  # @UnusedVariable
-                time.sleep(0.1)
-                os.waitpid(pid, os.WNOHANG)
-                
-            # the process still did not exit
-            if settings.ENABLE_REBOOT:
-                logging.error('could not terminate the motion process')
-                powerctl.reboot()
-
-            else:
-                raise Exception('could not terminate the motion process')
-        
-        except OSError as e:
-            if e.errno not in (errno.ESRCH, errno.ECHILD):
-                raise
-
-
-def running():
-    pid = _get_pid()
-    if pid is None:
-        return False
-    
-    try:
-        os.waitpid(pid, os.WNOHANG)
-        os.kill(pid, 0)
-        
-        # the process is running
-        return True
-    
-    except OSError as e:
-        if e.errno not in (errno.ESRCH, errno.ECHILD):
-            raise
-
-    return False
-
-
-def started():
-    return _started
-
-
-def get_motion_detection(camera_id):
-    thread_id = camera_id_to_thread_id(camera_id)
-    if thread_id is None:
-        return logging.error('could not find thread id for camera with id %s' % camera_id)
-    
-    url = 'http://127.0.0.1:7999/%(id)s/detection/status' % {'id': thread_id}
-    
-    request = HTTPRequest(url, connect_timeout=5, request_timeout=5)
-    http_client = HTTPClient()
-    try:
-        response = http_client.fetch(request)
-        if response.error:
-            raise response.error 
-    
-    except Exception as e:
-        logging.error('failed to get motion detection status for camera with id %(id)s: %(msg)s' % {
-                'id': camera_id,
-                'msg': unicode(e)})
-
-        return None
-    
-    enabled = bool(response.body.lower().count('active'))
-    
-    logging.debug('motion detection is %(what)s for camera with id %(id)s' % {
-            'what': ['disabled', 'enabled'][enabled],
-            'id': camera_id})
-    
-    return enabled
-
-
-def set_motion_detection(camera_id, enabled):
-    thread_id = camera_id_to_thread_id(camera_id)
-    if thread_id is None:
-        return logging.error('could not find thread id for camera with id %s' % camera_id)
-    
-    if not enabled:
-        _motion_detected[camera_id] = False
-    
-    logging.debug('%(what)s motion detection for camera with id %(id)s' % {
-            'what': ['disabling', 'enabling'][enabled],
-            'id': camera_id})
-    
-    url = 'http://127.0.0.1:7999/%(id)s/detection/%(enabled)s' % {
-            'id': thread_id,
-            'enabled': ['pause', 'start'][enabled]}
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to %(what)s motion detection for camera with id %(id)s: %(msg)s' % {
-                    'what': ['disable', 'enable'][enabled],
-                    'id': camera_id,
-                    'msg': utils.pretty_http_error(response)})
-        
-        else:
-            logging.debug('successfully %(what)s motion detection for camera with id %(id)s' % {
-                    'what': ['disabled', 'enabled'][enabled],
-                    'id': camera_id})
-
-    request = HTTPRequest(url, connect_timeout=4, request_timeout=4)
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, on_response)
-
-
-def is_motion_detected(camera_id):
-    return _motion_detected.get(camera_id, False)
-
-
-def set_motion_detected(camera_id, motion_detected):
-    if motion_detected:
-        logging.debug('marking motion detected for camera with id %s' % camera_id)
-
-    else:
-        logging.debug('clearing motion detected for camera with id %s' % camera_id)
-        
-    _motion_detected[camera_id] = motion_detected
-
-
-def camera_id_to_thread_id(camera_id):
-    # find the corresponding thread_id
-    # (which can be different from camera_id)
-    camera_ids = config.get_camera_ids()
-    thread_id = 0
-    for cid in camera_ids:
-        camera_config = config.get_camera(cid)
-        if utils.local_motion_camera(camera_config):
-            thread_id += 1
-        
-        if cid == camera_id:
-            return thread_id or None
-
-    return None
-    
-
-def thread_id_to_camera_id(thread_id):
-    # find the corresponding camera_id
-    # (which can be different from thread_id)
-    camera_ids = config.get_camera_ids()
-    tid = 0
-    for cid in camera_ids:
-        camera_config = config.get_camera(cid)
-        if utils.local_motion_camera(camera_config):
-            tid += 1
-            if tid == thread_id:
-                return cid
-    
-    return None
-
-
-def _get_pid():
-    motion_pid_path = os.path.join(settings.RUN_PATH, 'motion.pid')
-    
-    # read the pid from file
-    try:
-        with open(motion_pid_path, 'r') as f:
-            return int(f.readline().strip())
-        
-    except (IOError, ValueError):
-        return None
diff --git a/src/ordereddict.py b/src/ordereddict.py
deleted file mode 100644 (file)
index 0874135..0000000
+++ /dev/null
@@ -1,258 +0,0 @@
-# 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)
diff --git a/src/powerctl.py b/src/powerctl.py
deleted file mode 100644 (file)
index 3b82ab7..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import logging
-import os
-import subprocess
-
-
-def _find_prog(prog):
-    try:
-        return subprocess.check_output('which %s' % prog, shell=True).strip()
-    
-    except subprocess.CalledProcessError: # not found
-        return None
-
-
-def _exec_prog(prog):
-    logging.info('executing "%s"' % prog)
-    
-    return os.system(prog) == 0
-
-
-def shut_down():
-    logging.info('shutting down')
-    
-    prog = _find_prog('poweroff')
-    if prog:
-        return _exec_prog(prog)
-    
-    prog = _find_prog('shutdown')
-    if prog:
-        return _exec_prog(prog + ' -h now')
-    
-    prog = _find_prog('systemctl')
-    if prog:
-        return _exec_prog(prog + ' poweroff')
-    
-    prog = _find_prog('init')
-    if prog:
-        return _exec_prog(prog + ' 0')
-    
-    return False
-
-
-def reboot():
-    logging.info('rebooting')
-    
-    prog = _find_prog('reboot')
-    if prog:
-        return _exec_prog(prog)
-    
-    prog = _find_prog('shutdown')
-    if prog:
-        return _exec_prog(prog + ' -r now')
-    
-    prog = _find_prog('systemctl')
-    if prog:
-        return _exec_prog(prog + ' reboot')
-    
-    prog = _find_prog('init')
-    if prog:
-        return _exec_prog(prog + ' 6')
-    
-    return False
diff --git a/src/remote.py b/src/remote.py
deleted file mode 100644 (file)
index a10599a..0000000
+++ /dev/null
@@ -1,646 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import functools
-import json
-import logging
-import re
-
-from tornado.httpclient import AsyncHTTPClient, HTTPRequest
-
-import settings
-import utils
-
-_DOUBLE_SLASH_REGEX = re.compile('//+')
-
-
-def _make_request(scheme, host, port, username, password, uri, method='GET', data=None, query=None, timeout=None):
-    uri = _DOUBLE_SLASH_REGEX.sub('/', uri)
-    url = '%(scheme)s://%(host)s%(port)s%(uri)s' % {
-            'scheme': scheme,
-            'host': host,
-            'port': ':' + str(port) if port else '',
-            'uri': uri or ''}
-    
-    query = dict(query or {})
-    query['_username'] = username or ''
-    query['_admin'] = 'true' # always use the admin account
-    
-    if url.count('?'):
-        url += '&'
-        
-    else:
-        url += '?'
-    
-    url += '&'.join([(n + '=' + v) for (n, v) in query.iteritems()])
-    url += '&_signature=' + utils.compute_signature(method, url, data, password)
-
-    if timeout is None:
-        timeout = settings.REMOTE_REQUEST_TIMEOUT
-        
-    return HTTPRequest(url, method, body=data, connect_timeout=timeout, request_timeout=timeout)
-
-
-def _callback_wrapper(callback):
-    @functools.wraps(callback)
-    def wrapper(response):
-        try:
-            decoded = json.loads(response.body)
-            if decoded['error'] == 'unauthorized':
-                response.error = 'Authentication Error'
-                
-            elif decoded['error']:
-                response.error = decoded['error']
-
-        except:
-            pass
-        
-        return callback(response)
-    
-    return wrapper
-
-
-def pretty_camera_url(local_config, camera=True):
-    scheme = local_config.get('@scheme', local_config.get('scheme')) or 'http'
-    host = local_config.get('@host', local_config.get('host'))
-    port = local_config.get('@port', local_config.get('port'))
-    uri = local_config.get('@uri', local_config.get('uri')) or ''
-
-    url = scheme + '://' + host
-    if port and str(port) not in ['80', '443']:
-        url += ':' + str(port)
-    
-    if uri:
-        url += uri
-        
-    if url.endswith('/'):
-        url = url[:-1]
-
-    if camera:
-        if camera is True:
-            url += '/config/' + str(local_config.get('@remote_camera_id', local_config.get('remote_camera_id')))
-        
-        else:
-            url += '/config/' + str(camera)
-
-    return url
-
-
-def _remote_params(local_config):
-    return (
-            local_config.get('@scheme', local_config.get('scheme')) or 'http',
-            local_config.get('@host', local_config.get('host')),
-            local_config.get('@port', local_config.get('port')),
-            local_config.get('@username', local_config.get('username')),
-            local_config.get('@password', local_config.get('password')),
-            local_config.get('@uri', local_config.get('uri')) or '',
-            local_config.get('@remote_camera_id', local_config.get('remote_camera_id')))
-
-
-def list(local_config, callback):
-    scheme, host, port, username, password, uri, _ = _remote_params(local_config)
-    
-    logging.debug('listing remote cameras on %(url)s' % {
-            'url': pretty_camera_url(local_config, camera=False)})
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/config/list/')
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to list remote cameras on %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config, camera=False),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-        
-        try:
-            response = json.loads(response.body)
-            
-        except Exception as e:
-            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config, camera=False),
-                    'msg': unicode(e)})
-            
-            return callback(error=unicode(e))
-        
-        cameras = response['cameras']
-        
-        # filter out simple mjpeg cameras
-        cameras = [c for c in cameras if c['proto'] != 'mjpeg']
-        
-        callback(cameras)
-    
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-    
-
-def get_config(local_config, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-     
-    logging.debug('getting config for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/config/%(id)s/get/' % {'id': camera_id})
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to get config for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-    
-        try:
-            response = json.loads(response.body)
-        
-        except Exception as e:
-            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config),
-                    'msg': unicode(e)})
-            
-            return callback(error=unicode(e))
-        
-        response['host'] = host
-        response['port'] = port
-            
-        callback(response)
-    
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-    
-
-def set_config(local_config, ui_config, callback):
-    scheme = local_config.get('@scheme', local_config.get('scheme'))
-    host = local_config.get('@host', local_config.get('host')) 
-    port = local_config.get('@port', local_config.get('port'))
-    username = local_config.get('@username', local_config.get('username'))
-    password = local_config.get('@password', local_config.get('password'))
-    uri = local_config.get('@uri', local_config.get('uri')) or ''
-    camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
-
-    logging.debug('setting config for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    ui_config = json.dumps(ui_config)
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/config/%(id)s/set/' % {'id': camera_id}, method='POST', data=ui_config)
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to set config for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-    
-        callback()
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def set_preview(local_config, controls, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('setting preview for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    data = json.dumps(controls)
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/config/%(id)s/set_preview/' % {'id': camera_id}, method='POST', data=data)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to set preview for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-        
-            return callback(error=utils.pretty_http_error(response))
-        
-        callback()
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def get_current_picture(local_config, width, height, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('getting current picture for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    query = {}
-    
-    if width:
-        query['width'] = str(width)
-        
-    if height:
-        query['height'] = str(height)
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/current/' % {'id': camera_id}, query=query)
-    
-    def on_response(response):
-        motion_detected = False
-        
-        cookies = response.headers.get('Set-Cookie')
-        if cookies:
-            cookies = cookies.split(';')
-            cookies = [[i.strip() for i in c.split('=')] for c in cookies]
-            cookies = dict([c for c in cookies if len(c) == 2])
-            motion_detected = cookies.get('motion_detected_' + str(camera_id)) == 'true'
-        
-        if response.error:
-            logging.error('failed to get current picture for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-
-        callback(motion_detected, response.body)
-    
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def list_media(local_config, media_type, prefix, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('getting media list for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    query = {}
-    if prefix is not None:
-        query['prefix'] = prefix
-    
-    # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list
-    request = _make_request(scheme, host, port, username, password, uri + '/%(media_type)s/%(id)s/list/' % {
-            'id': camera_id, 'media_type': media_type}, query=query, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to get media list for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-        
-        try:
-            response = json.loads(response.body)
-            
-        except Exception as e:
-            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config),
-                    'msg': unicode(e)})
-            
-            return callback(error=unicode(e))
-        
-        return callback(response)
-    
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def get_media_content(local_config, filename, media_type, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('downloading file %(filename)s of remote camera %(id)s on %(url)s' % {
-            'filename': filename,
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    uri += '/%(media_type)s/%(id)s/download/%(filename)s' % {
-            'media_type': media_type,
-            'id': camera_id,
-            'filename': filename}
-    
-    # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list
-    request = _make_request(scheme, host, port, username, password, uri, timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to download file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'filename': filename,
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-        
-        return callback(response.body)
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def make_zipped_content(local_config, media_type, group, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('preparing zip file for group %(group)s of remote camera %(id)s on %(url)s' % {
-            'group': group,
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-
-    prepare_uri = uri + '/%(media_type)s/%(id)s/zipped/%(group)s/' % {
-            'media_type': media_type,
-            'id': camera_id,
-            'group': group}
-    # timeout here is 100 times larger than usual - we expect a big delay
-    request = _make_request(scheme, host, port, username, password, prepare_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to prepare zip file for group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'group': group,
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-
-            return callback(error=utils.pretty_http_error(response))
-        
-        try:
-            key = json.loads(response.body)['key']
-
-        except Exception as e:
-            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config),
-                    'msg': unicode(e)})
-
-            return callback(error=unicode(e))
-
-        callback({'key': key})
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def get_zipped_content(local_config, media_type, key, group, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('downloading zip file for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/%(media_type)s/%(id)s/zipped/%(group)s/?key=%(key)s' % {
-            'media_type': media_type,
-            'group': group,
-            'id': camera_id,
-            'key': key},
-            timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to download zip file for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-
-            return callback(error=utils.pretty_http_error(response))
-
-        callback({
-            'data': response.body,
-            'content_type': response.headers.get('Content-Type'),
-            'content_disposition': response.headers.get('Content-Disposition')
-        })
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def make_timelapse_movie(local_config, framerate, interval, group, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-
-    logging.debug('making timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s' % {
-            'group': group,
-            'id': camera_id,
-            'framerate': framerate,
-            'int': interval,
-            'url': pretty_camera_url(local_config)})
-
-    uri += '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s&framerate=%(framerate)s' % {
-            'id': camera_id,
-            'int': interval,
-            'framerate': framerate,
-            'group': group}
-    
-    request = _make_request(scheme, host, port, username, password, uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to make timelapse movie for group %(group)s of remote camera %(id)s with rate %(framerate)s/%(int)s on %(url)s: %(msg)s' % {
-                    'group': group,
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'int': interval,
-                    'framerate': framerate,
-                    'msg': utils.pretty_http_error(response)})
-
-            return callback(error=utils.pretty_http_error(response))
-        
-        try:
-            response = json.loads(response.body)
-
-        except Exception as e:
-            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config),
-                    'msg': unicode(e)})
-
-            return callback(error=unicode(e))
-        
-        callback(response)
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def check_timelapse_movie(local_config, group, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('checking timelapse movie status for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?check=true' % {
-            'id': camera_id,
-            'group': group})
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to check timelapse movie status for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-
-            return callback(error=utils.pretty_http_error(response))
-        
-        try:
-            response = json.loads(response.body)
-
-        except Exception as e:
-            logging.error('failed to decode json answer from %(url)s: %(msg)s' % {
-                    'url': pretty_camera_url(local_config),
-                    'msg': unicode(e)})
-
-            return callback(error=unicode(e))
-        
-        callback(response)
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def get_timelapse_movie(local_config, key, group, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('downloading timelapse movie for remote camera %(id)s on %(url)s' % {
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    request = _make_request(scheme, host, port, username, password, uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % {
-            'id': camera_id,
-            'group': group,
-            'key': key},
-            timeout=10 * settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to download timelapse movie for remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-
-            return callback(error=utils.pretty_http_error(response))
-
-        callback({
-            'data': response.body,
-            'content_type': response.headers.get('Content-Type'),
-            'content_disposition': response.headers.get('Content-Disposition')
-        })
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def get_media_preview(local_config, filename, media_type, width, height, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('getting file preview for %(filename)s of remote camera %(id)s on %(url)s' % {
-            'filename': filename,
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    uri += '/%(media_type)s/%(id)s/preview/%(filename)s' % {
-            'media_type': media_type,
-            'id': camera_id,
-            'filename': filename}
-    
-    query = {}
-    
-    if width:
-        query['width'] = str(width)
-        
-    if height:
-        query['height'] = str(height)
-    
-    request = _make_request(scheme, host, port, username, password, uri, query=query)
-    
-    def on_response(response):
-        if response.error:
-            logging.error('failed to get file preview for %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'filename': filename,
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-        
-        callback(response.body)
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def del_media_content(local_config, filename, media_type, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('deleting file %(filename)s of remote camera %(id)s on %(url)s' % {
-            'filename': filename,
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    uri += '/%(media_type)s/%(id)s/delete/%(filename)s' % {
-            'media_type': media_type,
-            'id': camera_id,
-            'filename': filename}
-
-    request = _make_request(scheme, host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to delete file %(filename)s of remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'filename': filename,
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-        
-        callback()
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
-
-
-def del_media_group(local_config, group, media_type, callback):
-    scheme, host, port, username, password, uri, camera_id = _remote_params(local_config)
-    
-    logging.debug('deleting group %(group)s of remote camera %(id)s on %(url)s' % {
-            'group': group,
-            'id': camera_id,
-            'url': pretty_camera_url(local_config)})
-    
-    uri += '/%(media_type)s/%(id)s/delete_all/%(group)s/' % {
-            'media_type': media_type,
-            'id': camera_id,
-            'group': group}
-
-    request = _make_request(scheme, host, port, username, password, uri, method='POST', data='{}', timeout=settings.REMOTE_REQUEST_TIMEOUT)
-
-    def on_response(response):
-        if response.error:
-            logging.error('failed to delete group %(group)s of remote camera %(id)s on %(url)s: %(msg)s' % {
-                    'group': group,
-                    'id': camera_id,
-                    'url': pretty_camera_url(local_config),
-                    'msg': utils.pretty_http_error(response)})
-            
-            return callback(error=utils.pretty_http_error(response))
-        
-        callback()
-
-    http_client = AsyncHTTPClient()
-    http_client.fetch(request, _callback_wrapper(on_response))
diff --git a/src/server.py b/src/server.py
deleted file mode 100644 (file)
index 623b9a4..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-from tornado.web import Application
-
-import handlers
-import logging
-import settings
-import template
-
-
-def log_request(handler):
-    if handler.get_status() < 400:
-        log_method = logging.debug
-    
-    elif handler.get_status() < 500:
-        log_method = logging.warning
-    
-    else:
-        log_method = logging.error
-    
-    request_time = 1000.0 * handler.request.request_time()
-    log_method("%d %s %.2fms", handler.get_status(),
-               handler._request_summary(), request_time)
-
-
-application = Application(
-    [
-        (r'^/$', handlers.MainHandler),
-        (r'^/config/main/(?P<op>set|get)/?$', handlers.ConfigHandler),
-        (r'^/config/(?P<camera_id>\d+)/(?P<op>get|set|rem|set_preview)/?$', handlers.ConfigHandler),
-        (r'^/config/(?P<op>add|list|backup|restore)/?$', handlers.ConfigHandler),
-        (r'^/picture/(?P<camera_id>\d+)/(?P<op>current|list|frame)/?$', handlers.PictureHandler),
-        (r'^/picture/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.PictureHandler),
-        (r'^/picture/(?P<camera_id>\d+)/(?P<op>zipped|timelapse|delete_all)/(?P<group>.+?)/?$', handlers.PictureHandler),
-        (r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
-        (r'^/movie/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.MovieHandler),
-        (r'^/movie/(?P<camera_id>\d+)/(?P<op>delete_all)/(?P<group>.+?)/?$', handlers.MovieHandler),
-        (r'^/_relay_event/?$', handlers.RelayEventHandler),
-        (r'^/log/(?P<name>\w+)/?$', handlers.LogHandler),
-        (r'^/update/?$', handlers.UpdateHandler),
-        (r'^/power/(?P<op>shutdown|reboot)/?$', handlers.PowerHandler),
-        (r'^/version/?$', handlers.VersionHandler),
-        (r'^/login/?$', handlers.LoginHandler),
-        (r'^.*$', handlers.NotFoundHandler),
-    ],
-    debug=False,
-    log_function=log_request,
-    static_path=settings.STATIC_PATH,
-    static_url_prefix=settings.STATIC_URL
-)
-
-template.add_context('STATIC_URL', settings.STATIC_URL)
diff --git a/src/smbctl.py b/src/smbctl.py
deleted file mode 100644 (file)
index 12e4505..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import logging
-import os
-import re
-import subprocess
-import time
-
-from tornado import ioloop
-
-import config
-import settings
-
-
-def find_mount_cifs():
-    try:
-        return subprocess.check_output('which mount.cifs', shell=True).strip()
-    
-    except subprocess.CalledProcessError: # not found
-        return None
-
-
-def make_mount_point(server, share, username):
-    server = re.sub('[^a-zA-Z0-9]', '_', server).lower()
-    share = re.sub('[^a-zA-Z0-9]', '_', share).lower()
-    
-    if username:
-        username = re.sub('[^a-zA-Z0-9]', '_', username).lower()
-        mount_point = os.path.join(settings.SMB_MOUNT_ROOT, 'motioneye_%s_%s_%s' % (server, share, username))
-    
-    else:
-        mount_point = os.path.join(settings.SMB_MOUNT_ROOT, 'motioneye_%s_%s' % (server, share))
-
-    return mount_point
-
-
-def _is_motioneye_mount(mount_point):
-    mount_point_root = os.path.join(settings.SMB_MOUNT_ROOT, 'motioneye_')
-    return bool(re.match('^' + mount_point_root + '\w+$', mount_point))
-
-
-def list_mounts():
-    logging.debug('listing smb mounts...')
-    
-    mounts = []
-    with open('/proc/mounts', 'r') as f:
-        for line in f:
-            line = line.strip()
-            if not line:
-                continue
-            parts = line.split()
-            if len(parts) < 4:
-                continue
-            
-            target = parts[0]
-            mount_point = parts[1]
-            fstype = parts[2]
-            opts = ' '.join(parts[3:])
-            
-            if fstype != 'cifs':
-                continue
-            
-            if not _is_motioneye_mount(mount_point):
-                continue
-            
-            match = re.match('//([^/]+)/(.+)', target)
-            if not match:
-                continue
-            
-            if len(match.groups()) != 2:
-                continue
-            
-            server, share = match.groups()
-            share = share.replace('\\040', ' ') # spaces are reported oddly by /proc/mounts
-            
-            match = re.search('username=([\w\s]+)', opts)
-            if match:
-                username = match.group(1)
-            
-            else:
-                username = None
-                
-            logging.debug('found smb mount "//%s/%s" at "%s"' % (server, share, mount_point))
-            
-            mounts.append({
-                'server': server,
-                'share': share,
-                'username': username,
-                'mount_point': mount_point
-            })
-
-    return mounts
-
-
-def mount(server, share, username, password):
-    mount_point = make_mount_point(server, share, username)
-    
-    logging.debug('making sure mount point "%s" exists' % mount_point)
-    
-    if not os.path.exists(mount_point):    
-        os.makedirs(mount_point)
-        
-    if username:
-        opts = 'username=%s,password=%s' % (username, password)
-        sec_types = ['ntlm', 'ntlmv2', 'none']
-
-    else:
-        opts = 'guest'
-        sec_types = ['none', 'ntlm', 'ntlmv2']
-
-    for sec in sec_types:
-        actual_opts = opts + ',sec=' + sec
-        try:
-            logging.debug('mounting "//%s/%s" at "%s" (sec=%s)' % (server, share, mount_point, sec))
-            subprocess.check_call('mount.cifs "//%s/%s" "%s" -o "%s"' % (server, share, mount_point, actual_opts), shell=True)
-            break
-
-        except subprocess.CalledProcessError:
-            pass
-            
-    else:
-        logging.error('failed to mount smb share "//%s/%s" at "%s"' % (server, share, mount_point))
-        return None
-    
-    # test to see if mount point is writable
-    try:
-        path = os.path.join(mount_point, '.motioneye_' + str(int(time.time())))
-        os.mkdir(path)
-        os.rmdir(path)
-        logging.debug('directory at "%s" is writable' % mount_point)
-    
-    except:
-        logging.error('directory at "%s" is not writable' % mount_point)
-        
-        return None
-    
-    return mount_point
-
-
-def umount(server, share, username):
-    mount_point = make_mount_point(server, share, username)
-    logging.debug('unmounting "//%s/%s" from "%s"' % (server, share, mount_point))
-    
-    try:
-        subprocess.check_call('umount "%s"' % mount_point, shell=True)
-
-    except subprocess.CalledProcessError:
-        logging.error('failed to unmount smb share "//%s/%s" from "%s"' % (server, share, mount_point))
-        
-        return False
-    
-    try:
-        os.rmdir(mount_point)
-    
-    except Exception as e:
-        logging.error('failed to remove smb mount point "%s": %s' % (mount_point, e))
-        
-        return False
-        
-    return True
-
-
-def update_mounts():
-    network_shares = config.get_network_shares()
-    
-    mounts = list_mounts()
-    mounts = dict(((m['server'], m['share'], m['username'] or ''), False) for m in mounts)
-    
-    should_stop = False # indicates that motion should be stopped immediately
-    should_start = True # indicates that motion can be started afterwards
-    for network_share in network_shares:
-        key = (network_share['server'], network_share['share'], network_share['username'] or '')
-        if key in mounts: # found
-            mounts[key] = True
-        
-        else: # needs to be mounted
-            should_stop = True
-            if not mount(network_share['server'], network_share['share'], network_share['username'], network_share['password']):
-                should_start = False
-    
-    # unmount the no longer necessary mounts
-    for (server, share, username), required in mounts.items():
-        if not required:
-            umount(server, share, username)
-            should_stop = True
-    
-    return (should_stop, should_start)
-
-
-def umount_all():
-    for mount in list_mounts():
-        umount(mount['server'], mount['share'], mount['username'])
-
-
-def _check_mounts():
-    import motionctl
-    
-    logging.debug('checking SMB mounts...')
-    
-    stop, start = update_mounts()
-    if stop:
-        motionctl.stop()
-
-    if start:
-        motionctl.start()
-        
-    io_loop = ioloop.IOLoop.instance()
-    io_loop.add_timeout(datetime.timedelta(seconds=settings.MOUNT_CHECK_INTERVAL), _check_mounts)
-
-
-if settings.SMB_SHARES:
-    # schedule the mount checker
-    io_loop = ioloop.IOLoop.instance()
-    io_loop.add_timeout(datetime.timedelta(seconds=settings.MOUNT_CHECK_INTERVAL), _check_mounts)
diff --git a/src/template.py b/src/template.py
deleted file mode 100644 (file)
index 49ce299..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-from jinja2 import Environment, FileSystemLoader
-
-import settings
-import utils
-
-
-_jinja_env = None
-
-
-def _init_jinja():
-    global _jinja_env
-    
-    _jinja_env = Environment(
-            loader=FileSystemLoader(settings.TEMPLATE_PATH),
-            trim_blocks=False)
-
-    # globals
-    _jinja_env.globals['settings'] = settings
-    
-    # filters
-    _jinja_env.filters['pretty_date_time'] = utils.pretty_date_time
-    _jinja_env.filters['pretty_date'] = utils.pretty_date
-    _jinja_env.filters['pretty_time'] = utils.pretty_time
-    _jinja_env.filters['pretty_duration'] = utils.pretty_duration
-
-
-def add_template_path(path):
-    global _jinja_env
-    if _jinja_env is None:
-        _init_jinja()
-    
-    _jinja_env.loader.searchpath.append(path)
-
-
-def add_context(name, value):
-    global _jinja_env
-    if _jinja_env is None:
-        _init_jinja()
-    
-    _jinja_env.globals[name] = value
-
-
-def render(template_name, **context):
-    global _jinja_env
-    if _jinja_env is None:
-        _init_jinja()
-    
-    template = _jinja_env.get_template(template_name)
-    return template.render(**context)
diff --git a/src/thumbnailer.py b/src/thumbnailer.py
deleted file mode 100644 (file)
index c702863..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import logging
-import multiprocessing
-import os
-import signal
-import tornado
-
-import cleanup
-import mediafiles
-import settings
-
-
-_process = None
-
-
-def start():
-    # schedule the first call a bit later to improve performance at startup
-    ioloop = tornado.ioloop.IOLoop.instance()
-    ioloop.add_timeout(datetime.timedelta(seconds=30), _run_process)
-
-
-def stop():
-    global _process
-    
-    if not running():
-        _process = None
-        return
-    
-    if _process.is_alive():
-        _process.join(timeout=10)
-    
-    if _process.is_alive():
-        logging.error('thumbnailer process did not finish in time, killing it...')
-        os.kill(_process.pid, signal.SIGKILL)
-
-    _process = None
-
-
-def running():
-    return _process is not None and _process.is_alive()
-
-
-def _run_process():
-    global _process
-    
-    # schedule the next call
-    ioloop = tornado.ioloop.IOLoop.instance()
-    ioloop.add_timeout(datetime.timedelta(seconds=settings.THUMBNAILER_INTERVAL), _run_process)
-
-    if not running() and not cleanup.running(): # check that the previous process has finished and that cleanup is not running
-        logging.debug('running thumbnailer process...')
-
-        _process = multiprocessing.Process(target=_do_next_movie_thumbail)
-        _process.start()
-
-
-def _do_next_movie_thumbail():
-    # this will be executed in a separate subprocess
-    
-    # ignore the terminate and interrupt signals in this subprocess
-    signal.signal(signal.SIGINT, signal.SIG_IGN)
-    signal.signal(signal.SIGTERM, signal.SIG_IGN)
-    
-    try:
-        mediafiles.make_next_movie_preview()
-         
-    except Exception as e:
-        logging.error('failed to make movie thumbnail: %(msg)s' % {
-                'msg': unicode(e)}, exc_info=True)
diff --git a/src/tzctl.py b/src/tzctl.py
deleted file mode 100644 (file)
index ff36ab7..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import hashlib
-import logging
-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
-    if not file:
-        return None
-    
-    for i in xrange(8): # recursively follow the symlinks @UnusedVariable
-        try:
-            file = os.readlink(file)
-
-        except OSError:
-            break
-    
-    if file and file.startswith('/usr/share/zoneinfo/'):
-        file = file[20:]
-    
-    else:
-        file = None
-
-    time_zone = file or None
-    if time_zone:
-        logging.debug('found time zone by symlink method: %s' % time_zone)
-    
-    return time_zone
-
-
-def _get_time_zone_md5():
-    if settings.LOCAL_TIME_FILE:
-        return None
-
-    try:
-        output = subprocess.check_output('cd /usr/share/zoneinfo; find * -type f | xargs md5sum', shell=True)
-
-    except Exception as e:
-        logging.error('getting md5 of zoneinfo files failed: %s' % e)
-
-        return None
-    
-    lines = [l for l in output.split('\n') if l]
-    lines = [l.split(None, 1) for l in lines]
-    time_zone_by_md5 = dict(lines)
-
-    try:
-        with open(settings.LOCAL_TIME_FILE, 'r') as f:
-            data = f.read()
-    
-    except Exception as e:
-        logging.error('failed to read local time file: %s' % e)
-        
-        return None
-
-    md5 = hashlib.md5(data).hexdigest()
-    time_zone = time_zone_by_md5.get(md5)
-    
-    if time_zone:
-        logging.debug('found time zone by md5 method: %s' % time_zone)
-    
-    return time_zone
-
-
-def _get_time_zone():
-    return _get_time_zone_symlink() or _get_time_zone_md5() or 'UTC'
-
-
-def _set_time_zone(time_zone):
-    time_zone = time_zone or 'UTC'
-
-    zoneinfo_file = '/usr/share/zoneinfo/' + time_zone
-    if not os.path.exists(zoneinfo_file):
-        logging.error('%s file does not exist' % zoneinfo_file)
-        
-        return False
-
-    logging.debug('linking "%s" to "%s"' % (settings.LOCAL_TIME_FILE, zoneinfo_file))
-
-    try:
-        os.remove(settings.LOCAL_TIME_FILE)
-    
-    except:
-        pass # nevermind
-    
-    try:
-        os.symlink(zoneinfo_file, settings.LOCAL_TIME_FILE)
-        
-        return True
-    
-    except Exception as e:
-        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,
-        'reboot': True,
-        'get': _get_time_zone,
-        'set': _set_time_zone
-    }
diff --git a/src/update.py b/src/update.py
deleted file mode 100644 (file)
index 0c53b25..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import logging
-import re
-
-
-# versions
-
-def get_version():
-    import motioneye
-    
-    return motioneye.VERSION
-
-
-def get_all_versions():
-    return []
-
-
-def compare_versions(version1, version2):
-    version1 = re.sub('[^0-9.]', '', version1)
-    version2 = re.sub('[^0-9.]', '', version2)
-    
-    version1 = [int(n) for n in version1.split('.')]
-    version2 = [int(n) for n in version2.split('.')]
-    
-    len1 = len(version1)
-    len2 = len(version2)
-    length = min(len1, len2)
-    for i in xrange(length):
-        p1 = version1[i]
-        p2 = version2[i]
-        
-        if p1 < p2:
-            return -1
-        
-        elif p1 > p2:
-            return 1
-    
-    if len1 < len2:
-        return -1
-    
-    elif len1 > len2:
-        return 1
-    
-    else:
-        return 0
-
-
-def perform_update(version):
-    logging.error('updating is not implemented')
-    
-    return False
diff --git a/src/utils.py b/src/utils.py
deleted file mode 100644 (file)
index a3fe0b8..0000000
+++ /dev/null
@@ -1,681 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import base64
-import datetime
-import functools
-import hashlib
-import logging
-import os
-import re
-import socket
-import time
-import urllib
-import urlparse
-
-from tornado.httpclient import AsyncHTTPClient, HTTPRequest
-from tornado.iostream import IOStream
-from tornado.ioloop import IOLoop
-
-import settings
-
-
-try:
-    from collections import OrderedDict  # @UnusedImport
-
-except:
-    from ordereddict import OrderedDict  # @UnusedImport @Reimport
-
-
-_SIGNATURE_REGEX = re.compile('[^a-zA-Z0-9/?_.=&{}\[\]":, _-]')
-
-
-COMMON_RESOLUTIONS = [
-    (320, 240),
-    (640, 480),
-    (800, 480),
-    (1024, 576),
-    (1024, 768),
-    (1280, 720),
-    (1280, 800),
-    (1280, 960),
-    (1280, 1024),
-    (1440, 960),
-    (1440, 1024),
-    (1600, 1200)
-]
-
-
-def pretty_date_time(date_time, tzinfo=None, short=False):
-    if date_time is None:
-        return '('+  _('never') + ')'
-
-    if isinstance(date_time, int):
-        return pretty_date_time(datetime.datetime.fromtimestamp(date_time))
-
-    if short:
-        text = u'{day} {month}, {hm}'.format(
-            day=date_time.day,
-            month=date_time.strftime('%b'),
-            hm=date_time.strftime('%H:%M')
-        )
-    
-    else:
-        text = u'{day} {month} {year}, {hm}'.format(
-            day=date_time.day,
-            month=date_time.strftime('%B'),
-            year=date_time.year,
-            hm=date_time.strftime('%H:%M')
-        )
-    
-    if tzinfo:
-        offset = tzinfo.utcoffset(datetime.datetime.utcnow()).seconds
-        tz = 'GMT'
-        if offset >= 0:
-            tz += '+'
-
-        else:
-            tz += '-'
-            offset = -offset
-
-        tz += '%.2d' % (offset / 3600) + ':%.2d' % ((offset % 3600) / 60)
-
-        text += ' (' + tz + ')'
-
-    return text
-
-
-def pretty_date(date):
-    if date is None:
-        return '('+  _('never') + ')'
-
-    if isinstance(date, int):
-        return pretty_date(datetime.datetime.fromtimestamp(date))
-
-    return u'{day} {month} {year}'.format(
-        day=date.day,
-        month=_(date.strftime('%B')),
-        year=date.year
-    )
-
-
-def pretty_time(time):
-    if time is None:
-        return ''
-
-    if isinstance(time, datetime.timedelta):
-        hour = time.seconds / 3600
-        minute = (time.seconds % 3600) / 60
-        time = datetime.time(hour, minute)
-
-    return '{hm}'.format(
-        hm=time.strftime('%H:%M')
-    )
-
-
-def pretty_duration(duration):
-    if duration is None:
-        duration = 0
-
-    if isinstance(duration, datetime.timedelta):
-        duration = duration.seconds + duration.days * 86400
-
-    if duration < 0:
-        negative = True
-        duration = -duration
-
-    else:
-        negative = False
-
-    days = int(duration / 86400)
-    duration %= 86400
-    hours = int(duration / 3600)
-    duration %= 3600
-    minutes = int(duration / 60)
-    duration %= 60
-    seconds = duration
-
-    # treat special cases
-    special_result = None
-    if days != 0 and hours == 0 and minutes == 0 and seconds == 0:
-        if days == 1:
-            special_result = str(days) + ' ' + _('day')
-
-        elif days == 7:
-            special_result = '1 ' + _('week')
-
-        elif days in [30, 31, 32]:
-            special_result = '1 ' + _('month')
-
-        elif days in [365, 366]:
-            special_result = '1 ' + _('year')
-
-        else:
-            special_result = str(days) + ' ' + _('days')
-
-    elif days == 0 and hours != 0 and minutes == 0 and seconds == 0:
-        if hours == 1:
-            special_result = str(hours) + ' ' + _('hour')
-
-        else:
-            special_result = str(hours) + ' ' + _('hours')
-
-    elif days == 0 and hours == 0 and minutes != 0 and seconds == 0:
-        if minutes == 1:
-            special_result = str(minutes) + ' ' + _('minute')
-
-        else:
-            special_result = str(minutes) + ' ' + _('minutes')
-
-    elif days == 0 and hours == 0 and minutes == 0 and seconds != 0:
-        if seconds == 1:
-            special_result = str(seconds) + ' ' + _('second')
-
-        else:
-            special_result = str(seconds) + ' ' + _('seconds')
-
-    elif days == 0 and hours == 0 and minutes == 0 and seconds == 0:
-        special_result = str(0) + ' ' + _('seconds')
-
-    if special_result:
-        if negative:
-            special_result = _('minus') + ' ' + special_result
-
-        return special_result
-
-    if days:
-        format = "{d}d{h}h{m}m"
-
-    elif hours:
-        format = "{h}h{m}m"
-
-    elif minutes:
-        format = "{m}m"
-        if seconds:
-            format += "{s}s"
-
-    else:
-        format = "{s}s"
-
-    if negative:
-        format = '-' + format
-
-    return format.format(d=days, h=hours, m=minutes, s=seconds)
-
-
-def pretty_size(size):
-    if size < 1024: # less than 1kB
-        size, unit = size, 'B'
-    
-    elif size < 1024 * 1024: # less than 1MB
-        size, unit = size / 1024.0, 'kB'
-        
-    elif size < 1024 * 1024 * 1024: # less than 1GB
-        size, unit = size / 1024.0 / 1024, 'MB'
-    
-    else: # greater than or equal to 1GB
-        size, unit = size / 1024.0 / 1024 / 1024, 'GB'
-    
-    return '%.1f %s' % (size, unit)
-
-
-def pretty_http_error(response):
-    if response.code == 401 or response.error == 'Authentication Error':
-        return 'authentication failed'
-
-    if not response.error:
-        return 'ok'
-    
-    msg = unicode(response.error)
-    if msg.startswith('HTTP '):
-        msg = msg.split(':', 1)[-1].strip()
-
-    if msg.startswith('[Errno '):
-        msg = msg.split(']', 1)[-1].strip()
-    
-    if 'timeout' in msg.lower() or 'timed out' in msg.lower():
-        msg = 'request timed out' 
-
-    return msg
-
-
-def make_str(s):
-    if isinstance(s, str):
-        return s
-
-    try:
-        return str(s)
-
-    except:
-        try:
-            return unicode(s, encoding='utf8').encode('utf8')
-    
-        except:
-            return unicode(s).encode('utf8')
-
-
-def make_unicode(s):
-    if isinstance(s, unicode):
-        return s
-
-    try:
-        return unicode(s, encoding='utf8')
-    
-    except:
-        try:
-            return unicode(s)
-        
-        except:
-            return str(s).decode('utf8')
-
-
-def get_disk_usage(path):
-    logging.debug('getting disk usage for path %(path)s...' % {
-            'path': path})
-
-    try:    
-        result = os.statvfs(path)
-    
-    except OSError as e:
-        logging.error('failed to execute statvfs: %(msg)s' % {'msg': unicode(e)})
-        
-        return None
-
-    block_size = result.f_frsize
-    free_blocks = result.f_bfree
-    total_blocks = result.f_blocks
-    
-    free_size = free_blocks * block_size
-    total_size = total_blocks * block_size
-    used_size = total_size - free_size
-    
-    return (used_size, total_size)
-
-
-def local_motion_camera(config):
-    '''Tells if a camera is managed by the local motion instance.'''
-    return bool(config.get('videodevice') or config.get('netcam_url'))
-
-
-def remote_camera(config):
-    '''Tells if a camera is managed by a remote motionEye server.'''
-    return config.get('@proto') == 'motioneye'
-
-
-def v4l2_camera(config):
-    '''Tells if a camera is a v4l2 device managed by the local motion instance.'''
-    return bool(config.get('videodevice'))
-
-
-def net_camera(config):
-    '''Tells if a camera is a network camera managed by the local motion instance.'''
-    return bool(config.get('netcam_url'))
-
-
-def simple_mjpeg_camera(config):
-    '''Tells if a camera is a simple MJPEG camera not managed by any motion instance.'''
-    return bool(config.get('@proto') == 'mjpeg')
-
-
-def test_mjpeg_url(data, auth_modes, allow_jpeg, callback):
-    data = dict(data)
-    data.setdefault('scheme', 'http')
-    data.setdefault('host', '127.0.0.1')
-    data.setdefault('port', '80')
-    data.setdefault('uri', '')
-    data.setdefault('username', None)
-    data.setdefault('password', None)
-
-    url = '%(scheme)s://%(host)s%(port)s%(uri)s' % {
-            'scheme': data['scheme'],
-            'host': data['host'],
-            'port': ':' + str(data['port']) if data['port'] else '',
-            'uri': data['uri'] or ''}
-    
-    called = [False]
-    status_2xx = [False]
-    http_11 = [False]
-
-    def do_request(on_response):
-        if data['username']:
-            auth = auth_modes[0]
-            
-        else:
-            auth = 'no'
-
-        logging.debug('testing (m)jpg netcam at %s using %s authentication' % (url, auth))
-
-        request = HTTPRequest(url, auth_username=username, auth_password=password, auth_mode=auth_modes.pop(0),
-                connect_timeout=settings.REMOTE_REQUEST_TIMEOUT, request_timeout=settings.REMOTE_REQUEST_TIMEOUT,
-                header_callback=on_header)
-
-        http_client = AsyncHTTPClient(force_instance=True)    
-        http_client.fetch(request, on_response)
-
-    def on_header(header):
-        header = header.lower()
-        if header.startswith('content-type') and status_2xx[0]:
-            content_type = header.split(':')[1].strip()
-            called[0] = True
-
-            if content_type in ['image/jpg', 'image/jpeg', 'image/pjpg'] and allow_jpeg:
-                callback([{'id': 1, 'name': 'JPEG Network Camera', 'keep_alive': http_11[0]}])
-            
-            elif content_type.startswith('multipart/x-mixed-replace'):
-                callback([{'id': 1, 'name': 'MJPEG Network Camera', 'keep_alive': http_11[0]}])
-            
-            else:
-                callback(error='not a supported network camera')
-
-        else:
-            # check for the status header
-            m = re.match('^http/1.(\d) (\d+) ', header)
-            if m:
-                if int(m.group(2)) / 100 == 2:
-                    status_2xx[0] = True
-                
-                if m.group(1) == '1':
-                    http_11[0] = True
-
-    def on_response(response):
-        if not called[0]:
-            if response.code == 401 and auth_modes and data['username']:
-                status_2xx[0] = False
-                do_request(on_response)
-                
-            else:
-                called[0] = True
-                callback(error=pretty_http_error(response) if response.error else 'not a supported network camera')
-    
-    username = data['username'] or None
-    password = data['password'] or None
-    
-    do_request(on_response)
-
-
-def test_rtsp_url(data, callback):
-    import config
-    
-    data = dict(data)
-    data.setdefault('scheme', 'rtsp')
-    data.setdefault('host', '127.0.0.1')
-    data['port'] = data.get('port') or '554'
-    data.setdefault('uri', '')
-    data.setdefault('username', None)
-    data.setdefault('password', None)
-
-    url = '%(scheme)s://%(host)s%(port)s%(uri)s' % {
-            'scheme': data['scheme'],
-            'host': data['host'],
-            'port': ':' + str(data['port']) if data['port'] else '',
-            'uri': data['uri'] or ''}
-    
-    called = [False]
-    timeout = [None]
-    stream = None
-
-    def connect():
-        logging.debug('testing rtsp netcam at %s' % url)
-
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-        s.settimeout(settings.MJPG_CLIENT_TIMEOUT)
-        stream = IOStream(s)
-        stream.set_close_callback(on_close)
-        stream.connect((data['host'], int(data['port'])), on_connect)
-        
-        timeout[0] = IOLoop.instance().add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT),
-                functools.partial(on_connect, _timeout=True))
-        
-        return stream
-    
-    def on_connect(_timeout=False):
-        IOLoop.instance().remove_timeout(timeout[0])
-        
-        if _timeout:
-            return handle_error('timeout connecting to rtsp netcam')
-
-        if not stream:
-            return handle_error('failed to connect to rtsp netcam') 
-
-        logging.debug('connected to rtsp netcam')
-        
-        stream.write('\r\n'.join([
-            'OPTIONS %s RTSP/1.0' % url.encode('utf8'),
-            'CSeq: 1',
-            'User-Agent: motionEye',
-            '',
-            ''
-        ]))
-
-        seek_rtsp()
-        
-    def seek_rtsp():
-        if check_error():
-            return
-
-        stream.read_until_regex('RTSP/1.0 \d+ ', on_rtsp)
-        timeout[0] = IOLoop.instance().add_timeout(datetime.timedelta(seconds=settings.MJPG_CLIENT_TIMEOUT), on_rtsp)
-
-    def on_rtsp(data):
-        IOLoop.instance().remove_timeout(timeout[0])
-
-        if data:
-            if data.endswith('200 '):
-                seek_server()
-    
-            else:
-                handle_error('rtsp netcam returned erroneous response: %s' % data)
-        
-        else:
-            handle_error('timeout waiting for rtsp netcam response')
-
-    def seek_server():
-        if check_error():
-            return
-
-        stream.read_until_regex('Server: .*', on_server)
-        timeout[0] = IOLoop.instance().add_timeout(datetime.timedelta(seconds=1), on_server)
-
-    def on_server(data=None):
-        IOLoop.instance().remove_timeout(timeout[0])
-
-        if data:
-            identifier = re.findall('Server: (.*)', data)[0].strip()
-            logging.debug('rtsp netcam identifier is "%s"' % identifier)
-        
-        else:
-            identifier = None
-            logging.debug('no rtsp netcam identifier')
-
-        handle_success(identifier)
-
-    def on_close():
-        if called[0]:
-            return
-        if not check_error():
-            handle_error('connection closed')
-
-    def handle_success(identifier):
-        if called[0]:
-            return
-        
-        called[0] = True
-        cameras = []
-        rtsp_support = config.motion_rtsp_support()
-        if identifier:
-            identifier = ' ' + identifier
-            
-        else:
-            identifier = ''
-
-        if 'udp' in rtsp_support:
-            cameras.append({'id': 'udp', 'name': '%sRTSP/UDP Camera' % identifier})
-        
-        if 'tcp' in rtsp_support:
-            cameras.append({'id': 'tcp', 'name': '%sRTSP/TCP Camera' % identifier})
-
-        callback(cameras)
-
-    def handle_error(e):
-        if called[0]:
-            return
-        
-        called[0] = True
-        logging.error('rtsp client error: %s' % unicode(e))
-
-        try:
-            stream.close()
-        
-        except:
-            pass
-        
-        callback(error=unicode(e))
-
-    def check_error():
-        error = getattr(stream, 'error', None)
-        if error and getattr(error, 'errno', None) != 0:
-            handle_error(error.strerror)
-            return True
-
-        if stream and stream.socket is None:
-            handle_error('connection closed')
-            stream.close()
-
-            return True
-        
-        return False
-
-    stream = connect()
-
-
-def compute_signature(method, uri, body, key):
-    parts = list(urlparse.urlsplit(uri))
-    query = [q for q in urlparse.parse_qsl(parts[3], keep_blank_values=True) if (q[0] != '_signature')]
-    query.sort(key=lambda q: q[0])
-    # "safe" characters here are set to match the encodeURIComponent JavaScript counterpart
-    query = [(n, urllib.quote(v, safe="!'()*~")) for (n, v) in query]
-    query = '&'.join([(q[0] + '=' + q[1]) for q in query])
-    parts[0] = parts[1] = ''
-    parts[3] = query
-    uri = urlparse.urlunsplit(parts)
-    uri = _SIGNATURE_REGEX.sub('-', uri)
-    key = _SIGNATURE_REGEX.sub('-', key)
-
-    if body and body.startswith('---'):
-        body = None # file attachment
-
-    body = body and _SIGNATURE_REGEX.sub('-', body.decode('utf8'))
-
-    return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower()
-
-
-def build_basic_header(username, password):
-    return 'Basic ' + base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
-
-
-def build_digest_header(method, url, username, password, state):
-    realm = state['realm']
-    nonce = state['nonce']
-    last_nonce = state.get('last_nonce', '')
-    nonce_count = state.get('nonce_count', 0)
-    qop = state.get('qop')
-    algorithm = state.get('algorithm')
-    opaque = state.get('opaque')
-
-    if algorithm is None:
-        _algorithm = 'MD5'
-
-    else:
-        _algorithm = algorithm.upper()
-
-    if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':
-        def md5_utf8(x):
-            if isinstance(x, str):
-                x = x.encode('utf-8')
-            return hashlib.md5(x).hexdigest()
-        hash_utf8 = md5_utf8
-
-    elif _algorithm == 'SHA':
-        def sha_utf8(x):
-            if isinstance(x, str):
-                x = x.encode('utf-8')
-            return hashlib.sha1(x).hexdigest()
-        hash_utf8 = sha_utf8
-
-    KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
-
-    if hash_utf8 is None:
-        return None
-
-    entdig = None
-    p_parsed = urlparse.urlparse(url)
-    path = p_parsed.path
-    if p_parsed.query:
-        path += '?' + p_parsed.query
-
-    A1 = '%s:%s:%s' % (username, realm, password)
-    A2 = '%s:%s' % (method, path)
-
-    HA1 = hash_utf8(A1)
-    HA2 = hash_utf8(A2)
-
-    if nonce == last_nonce:
-        nonce_count += 1
-
-    else:
-        nonce_count = 1
-    
-    ncvalue = '%08x' % nonce_count
-    s = str(nonce_count).encode('utf-8')
-    s += nonce.encode('utf-8')
-    s += time.ctime().encode('utf-8')
-    s += os.urandom(8)
-
-    cnonce = (hashlib.sha1(s).hexdigest()[:16])
-    if _algorithm == 'MD5-SESS':
-        HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
-
-    if qop is None:
-        respdig = KD(HA1, "%s:%s" % (nonce, HA2))
-    
-    elif qop == 'auth' or 'auth' in qop.split(','):
-        noncebit = "%s:%s:%s:%s:%s" % (
-            nonce, ncvalue, cnonce, 'auth', HA2
-            )
-        respdig = KD(HA1, noncebit)
-    
-    else:
-        return None
-
-    last_nonce = nonce
-
-    base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
-           'response="%s"' % (username, realm, nonce, path, respdig)
-    if opaque:
-        base += ', opaque="%s"' % opaque
-    if algorithm:
-        base += ', algorithm="%s"' % algorithm
-    if entdig:
-        base += ', digest="%s"' % entdig
-    if qop:
-        base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)
-    
-    state['last_nonce'] = last_nonce
-    state['nonce_count'] = nonce_count
-
-    return 'Digest %s' % (base)
diff --git a/src/v4l2ctl.py b/src/v4l2ctl.py
deleted file mode 100644 (file)
index 10637f4..0000000
+++ /dev/null
@@ -1,418 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import fcntl
-import logging
-import os.path
-import re
-import stat
-import subprocess
-import time
-import utils
-
-
-_resolutions_cache = {}
-_ctrls_cache = {}
-_ctrl_values_cache = {}
-
-_DEV_V4L_BY_ID = '/dev/v4l/by-id/'
-
-
-def find_v4l2_ctl():
-    try:
-        return subprocess.check_output('which v4l2-ctl', shell=True).strip()
-    
-    except subprocess.CalledProcessError: # not found
-        return None
-
-
-def list_devices():
-    global _resolutions_cache, _ctrls_cache, _ctrl_values_cache
-    
-    logging.debug('listing v4l2 devices...')
-    
-    try:
-        output = ''
-        started = time.time()
-        p = subprocess.Popen('v4l2-ctl --list-devices 2>/dev/null', shell=True, stdout=subprocess.PIPE, bufsize=1)
-
-        fd = p.stdout.fileno()
-        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
-        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
-        while True:
-            try:
-                data = p.stdout.read(1024)
-                if not data:
-                    break
-            
-            except IOError:
-                data = ''
-                time.sleep(0.01)
-
-            output += data
-
-            if len(output) > 10240:
-                logging.warn('v4l2-ctl command returned more than 10k of output')
-                break
-            
-            if time.time() - started > 3:
-                logging.warn('v4l2-ctl command ran for more than 3 seconds')
-                break
-
-    except subprocess.CalledProcessError:
-        logging.debug('failed to list devices (probably no devices installed)')
-        return []
-
-    try:
-        # try to kill the v4l2-ctl subprocess
-        p.kill()
-
-    except:
-        pass # nevermind
-
-    name = None
-    devices = []
-    for line in output.split('\n'):
-        if line.startswith('\t'):
-            device = line.strip()
-            persistent_device = find_persistent_device(device)
-            devices.append((device, persistent_device, name))
-        
-            logging.debug('found device %(name)s: %(device)s, %(persistent_device)s' % {
-                    'name': name, 'device': device, 'persistent_device': persistent_device})
-
-        else:
-            name = line.split('(')[0].strip()
-    
-    # clear the cache
-    _resolutions_cache = {}
-    _ctrls_cache = {}
-    _ctrl_values_cache = {}
-
-    return devices
-
-
-def list_resolutions(device):
-    global _resolutions_cache
-    
-    device = utils.make_str(device)
-    
-    if device in _resolutions_cache:
-        return _resolutions_cache[device]
-    
-    logging.debug('listing resolutions of device %(device)s...' % {'device': device})
-    
-    resolutions = set()
-    output = ''
-    started = time.time()
-    p = subprocess.Popen('v4l2-ctl -d "%(device)s" --list-formats-ext | grep -vi stepwise | grep -oE "[0-9]+x[0-9]+" || true' % {
-            'device': device}, shell=True, stdout=subprocess.PIPE, bufsize=1)
-
-    fd = p.stdout.fileno()
-    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
-    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
-    while True:
-        try:
-            data = p.stdout.read(1024)
-            if not data:
-                break
-
-        except IOError:
-            data = ''
-            time.sleep(0.01)
-
-        output += data
-
-        if len(output) > 10240:
-            logging.warn('v4l2-ctl command returned more than 10k of output')
-            break
-        
-        if time.time() - started > 3:
-            logging.warn('v4l2-ctl command ran for more than 3 seconds')
-            break
-    
-    try:
-        # try to kill the v4l2-ctl subprocess
-        p.kill()
-    
-    except:
-        pass # nevermind
-
-    for pair in output.split('\n'):
-        pair = pair.strip()
-        if not pair:
-            continue
-        
-        width, height = pair.split('x')
-        width = int(width)
-        height = int(height)
-
-        if (width, height) in resolutions:
-            continue # duplicate resolution
-
-        if width < 96 or height < 96: # some reasonable minimal values
-            continue
-        
-        if width % 16 or height % 16: # ignore non-modulo 16 resolutions
-            continue
-
-        resolutions.add((width, height))
-        
-        logging.debug('found resolution %(width)sx%(height)s for device %(device)s' % {
-                'device': device, 'width': width, 'height': height})
-    
-    if not resolutions:
-        logging.debug('no resolutions found for device %(device)s, using common values' % {'device': device})
-
-        # no resolution returned by v4l2-ctl call, add common default resolutions
-        resolutions = utils.COMMON_RESOLUTIONS
-
-    resolutions = list(sorted(resolutions, key=lambda r: (r[0], r[1])))
-    _resolutions_cache[device] = resolutions
-    
-    return resolutions
-
-
-def device_present(device):
-    device = utils.make_str(device)
-    
-    try:
-        st = os.stat(device)
-        return stat.S_ISCHR(st.st_mode)
-
-    except:
-        return False
-    
-
-def find_persistent_device(device):
-    device = utils.make_str(device)
-    
-    try:
-        devs_by_id = os.listdir(_DEV_V4L_BY_ID)
-
-    except OSError:
-        return device
-    
-    for p in devs_by_id:
-        p = os.path.join(_DEV_V4L_BY_ID, p)
-        if os.path.realpath(p) == device:
-            return p
-    
-    return device
-
-
-def get_brightness(device):
-    return _get_ctrl(device, 'brightness')
-
-
-def set_brightness(device, value):
-    _set_ctrl(device, 'brightness', value)
-
-
-def get_contrast(device):
-    return _get_ctrl(device, 'contrast')
-
-
-def set_contrast(device, value):
-    _set_ctrl(device, 'contrast', value)
-
-
-def get_saturation(device):
-    return _get_ctrl(device, 'saturation')
-
-
-def set_saturation(device, value):
-    _set_ctrl(device, 'saturation', value)
-
-
-def get_hue(device):
-    return _get_ctrl(device, 'hue')
-
-
-def set_hue(device, value):
-    _set_ctrl(device, 'hue', value)
-
-
-def _get_ctrl(device, control):
-    global _ctrl_values_cache
-    
-    device = utils.make_str(device)
-    
-    if not device_present(device):
-        return None
-    
-    if device in _ctrl_values_cache and control in _ctrl_values_cache[device]:
-        return _ctrl_values_cache[device][control]
-    
-    controls = _list_ctrls(device)
-    properties = controls.get(control)
-    if properties is None:
-        logging.warn('control %(control)s not found for device %(device)s' % {
-                'control': control, 'device': device})
-        
-        return None
-    
-    value = int(properties['value'])
-    
-    # adjust the value range
-    if 'min' in properties and 'max' in properties:
-        min_value = int(properties['min'])
-        max_value = int(properties['max'])
-        
-        value = int(round((value - min_value) * 100.0 / (max_value - min_value)))
-    
-    else:
-        logging.warn('min and max values not found for control %(control)s of device %(device)s' % {
-                'control': control, 'device': device})
-    
-    logging.debug('control %(control)s of device %(device)s is %(value)s%%' % {
-            'control': control, 'device': device, 'value': value})
-    
-    return value
-
-
-def _set_ctrl(device, control, value):
-    global _ctrl_values_cache
-    
-    device = utils.make_str(device)
-    
-    if not device_present(device):
-        return
-
-    controls = _list_ctrls(device)
-    properties = controls.get(control)
-    if properties is None:
-        logging.warn('control %(control)s not found for device %(device)s' % {
-                'control': control, 'device': device})
-        
-        return
-    
-    _ctrl_values_cache.setdefault(device, {})[control] = value
-
-    # adjust the value range
-    if 'min' in properties and 'max' in properties:
-        min_value = int(properties['min'])
-        max_value = int(properties['max'])
-        
-        value = int(round(min_value + value * (max_value - min_value) / 100.0))
-    
-    else:
-        logging.warn('min and max values not found for control %(control)s of device %(device)s' % {
-                'control': control, 'device': device})
-    
-    logging.debug('setting control %(control)s of device %(device)s to %(value)s' % {
-            'control': control, 'device': device, 'value': value})
-
-    output = ''
-    started = time.time()
-    p = subprocess.Popen('v4l2-ctl -d "%(device)s" --set-ctrl %(control)s=%(value)s' % {
-            'device': device, 'control': control, 'value': value}, shell=True, stdout=subprocess.PIPE, bufsize=1)
-
-    fd = p.stdout.fileno()
-    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
-    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
-    while True:
-        try:
-            data = p.stdout.read(1024)
-            if not data:
-                break
-        
-        except IOError:
-            data = ''
-            time.sleep(0.01)
-
-        output += data
-
-        if len(output) > 10240:
-            logging.warn('v4l2-ctl command returned more than 10k of output')
-            break
-
-        if time.time() - started > 3:
-            logging.warn('v4l2-ctl command ran for more than 3 seconds')
-            break
-
-    try:
-        # try to kill the v4l2-ctl subprocess
-        p.kill()
-
-    except:
-        pass # nevermind
-
-
-def _list_ctrls(device):
-    global _ctrls_cache
-    
-    device = utils.make_str(device)
-
-    if device in _ctrls_cache:
-        return _ctrls_cache[device]
-    
-    output = ''
-    started = time.time()
-    p = subprocess.Popen('v4l2-ctl -d "%(device)s" --list-ctrls' % {
-            'device': device}, shell=True, stdout=subprocess.PIPE, bufsize=1)
-
-    fd = p.stdout.fileno()
-    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
-    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
-    while True:
-        try:
-            data = p.stdout.read(1024)
-            if not data:
-                break
-        
-        except IOError:
-            data = ''
-            time.sleep(0.01)
-
-        output += data
-
-        if len(output) > 10240:
-            logging.warn('v4l2-ctl command returned more than 10k of output')
-            break
-
-        if time.time() - started > 3:
-            logging.warn('v4l2-ctl command ran for more than 3 seconds')
-            break
-
-    try:
-        # try to kill the v4l2-ctl subprocess
-        p.kill()
-
-    except:
-        pass # nevermind
-
-    controls = {}
-    for line in output.split('\n'):
-        if not line:
-            continue
-        
-        match = re.match('^\s*(\w+)\s+\(\w+\)\s+\:\s*(.+)', line)
-        if not match:
-            continue
-        
-        (control, properties) = match.groups()
-        properties = dict([v.split('=', 1) for v in properties.split(' ') if v.count('=')])
-        controls[control] = properties
-    
-    _ctrls_cache[device] = controls
-    
-    return controls
diff --git a/src/wifictl.py b/src/wifictl.py
deleted file mode 100644 (file)
index 32ec562..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import logging
-import re
-import settings
-
-from config import additional_config, additional_section
-
-
-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' % WPA_SUPPLICANT_CONF)
-    
-    try:
-        conf_file = open(WPA_SUPPLICANT_CONF, 'r')
-    
-    except Exception as e:
-        logging.error('could open wifi settings file %(path)s: %(msg)s' % {
-                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
-        
-        return {
-            'wifiEnabled': False,
-            'wifiNetworkName': '',
-            'wifiNetworkKey': ''
-        }
-    
-    lines = conf_file.readlines()
-    conf_file.close()
-    
-    ssid = psk = ''
-    in_section = False
-    for line in lines:
-        line = line.strip()
-        if line.startswith('#'):
-            continue
-        
-        if '{' in line:
-            in_section = True
-            
-        elif '}' in line:
-            in_section = False
-            break
-            
-        elif in_section:
-            m = re.search('ssid\s*=\s*"(.*?)"', line)
-            if m:
-                ssid = m.group(1)
-    
-            m = re.search('psk\s*=\s*"(.*?)"', line)
-            if m:
-                psk = m.group(1)
-
-    if ssid:
-        logging.debug('wifi is enabled (ssid = "%s")' % ssid)
-    
-        return {
-            'wifiEnabled': True,
-            'wifiNetworkName': ssid,
-            'wifiNetworkKey': psk
-        }
-
-    else:
-        logging.debug('wifi is disabled')
-
-        return {
-            'wifiEnabled': False,
-            'wifiNetworkName': ssid,
-            'wifiNetworkKey': psk
-        }
-
-
-def _set_wifi_settings(s):
-    s.setdefault('wifiEnabled', False)
-    s.setdefault('wifiNetworkName', '')
-    s.setdefault('wifiNetworkKey', '')
-    
-    logging.debug('writing wifi settings to %s: enabled=%s, ssid="%s"' % (
-            WPA_SUPPLICANT_CONF, s['wifiEnabled'], s['wifiNetworkName']))
-
-    enabled = s['wifiEnabled']
-    ssid = s['wifiNetworkName']
-    psk = s['wifiNetworkKey']
-    
-    # will update the first configured network
-    try:
-        conf_file = open(WPA_SUPPLICANT_CONF, 'r')
-    
-    except Exception as e:
-        logging.error('could open wifi settings file %(path)s: %(msg)s' % {
-                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
-
-        return
-    
-    lines = conf_file.readlines()
-    conf_file.close()
-    
-    in_section = False
-    found_ssid = False
-    found_psk = False
-    i = 0
-    while i < len(lines):
-        line = lines[i].strip()
-        if line.startswith('#'):
-            i += 1
-            continue
-        
-        if '{' in line:
-            in_section = True
-            
-        elif '}' in line:
-            in_section = False
-            if enabled and ssid and not found_ssid:
-                lines.insert(i, '    ssid="' + ssid + '"\n')
-            if enabled and psk and not found_psk:
-                lines.insert(i, '    psk="' + psk + '"\n')
-            
-            found_psk = found_ssid = True
-            
-            break
-            
-        elif in_section:
-            if enabled:
-                if re.match('ssid\s*=\s*".*?"', line):
-                    lines[i] = '    ssid="' + ssid + '"\n'
-                    found_ssid = True
-                
-                elif re.match('psk\s*=\s*".*?"', line):
-                    if psk:
-                        lines[i] = '    psk="' + psk + '"\n'
-                        found_psk = True
-                
-                    else:
-                        lines.pop(i)
-                        i -= 1
-        
-            else: # wifi disabled
-                if re.match('ssid\s*=\s*".*?"', line) or re.match('psk\s*=\s*".*?"', line):
-                    lines.pop(i)
-                    i -= 1
-        
-        i += 1
-
-    if enabled and not found_ssid:
-        lines.append('network={\n')
-        lines.append('    scan_ssid=1\n')
-        lines.append('    ssid="' + ssid + '"\n')
-        lines.append('    psk="' + psk + '"\n')
-        lines.append('}\n\n')
-
-    try:
-        conf_file = open(WPA_SUPPLICANT_CONF, 'w')
-    
-    except Exception as e:
-        logging.error('could open wifi settings file %(path)s: %(msg)s' % {
-                'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
-
-        return
-    
-    for line in lines:
-        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': False,
-        'reboot': True,
-        'depends': ['wifiEnabled'],
-        'get': _get_wifi_settings,
-        'set': _set_wifi_settings,
-        'get_set_dict': True
-    }
diff --git a/src/wsswitch.py b/src/wsswitch.py
deleted file mode 100644 (file)
index be65c27..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import datetime
-import logging
-import tornado
-
-import config
-import motionctl
-import utils
-
-
-def start():
-    ioloop = tornado.ioloop.IOLoop.instance()
-    ioloop.add_timeout(datetime.timedelta(seconds=10), _check_ws)
-
-
-def _during_working_schedule(now, working_schedule):
-    parts = working_schedule.split('|')
-    if len(parts) < 7:
-        return False # invalid ws
-
-    ws_day = parts[now.weekday()]
-    parts = ws_day.split('-')
-    if len(parts) != 2:
-        return False # invalid ws
-    
-    _from, to = parts
-    if not _from or not to:
-        return False # ws disabled for this day
-    
-    _from = _from.split(':')
-    to = to.split(':')
-    if len(_from) != 2 or len(to) != 2:
-        return False # invalid ws
-    
-    try:
-        from_h = int(_from[0])
-        from_m = int(_from[1])
-        to_h = int(to[0])
-        to_m = int(to[1])
-    
-    except ValueError:
-        return False # invalid ws
-    
-    if now.hour < from_h or now.hour > to_h:
-        return False
-
-    if now.hour == from_h and now.minute < from_m:
-        return False
-
-    if now.hour == to_h and now.minute > to_m:
-        return False
-    
-    return True
-
-
-def _check_ws():
-    # schedule the next call
-    ioloop = tornado.ioloop.IOLoop.instance()
-    ioloop.add_timeout(datetime.timedelta(seconds=10), _check_ws)
-
-    if not motionctl.running():
-        return
-
-    now = datetime.datetime.now()
-    for camera_id in config.get_camera_ids():
-        camera_config = config.get_camera(camera_id)
-        if not utils.local_motion_camera(camera_config):
-            continue
-        
-        working_schedule = camera_config.get('@working_schedule')
-        motion_detection = camera_config.get('@motion_detection')
-        working_schedule_type = camera_config.get('@working_schedule_type') or 'outside'
-        
-        if not working_schedule: # working schedule disabled, motion detection left untouched
-            continue
-        
-        if not motion_detection: # motion detection explicitly disabled
-            continue
-        
-        now_during = _during_working_schedule(now, working_schedule)
-        must_be_enabled = (now_during and working_schedule_type == 'during') or (not now_during and working_schedule_type == 'outside')
-        
-        currently_enabled = motionctl.get_motion_detection(camera_id)
-        if currently_enabled is None: # could not detect current status
-            logging.warn('skipping motion detection status update for camera with id %(id)s' % {'id': camera_id})
-            continue
-            
-        if currently_enabled and not must_be_enabled:
-            logging.debug('must disable motion detection for camera with id %(id)s (%(what)s working schedule)' % {
-                    'id': camera_id,
-                    'what': working_schedule_type})
-            
-            motionctl.set_motion_detection(camera_id, False)
-
-        elif not currently_enabled and must_be_enabled:
-            logging.debug('must enable motion detection for camera with id %(id)s (%(what)s working schedule)' % {
-                    'id': camera_id,
-                    'what': working_schedule_type})
-            
-            motionctl.set_motion_detection(camera_id, True)
diff --git a/static/css/frame.css b/static/css/frame.css
deleted file mode 100644 (file)
index ff0b35f..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-    /* basic */
-
-body {
-    color: #dddddd;
-    background-color: #212121;
-}
-
-
-    /* camera frame */
-
-div.camera-frame {
-    position: relative;
-    padding: 0px;
-    margin: 0px;
-    width: 100%;
-    height: 100%;
-}
-
-div.camera-container {
-    height: 100%;
-    text-align: center;
-    overflow: hidden;
-}
-
-img.camera {
-    height: auto;
-    margin: auto;
-}
-
-img.camera.error,
-img.camera.loading {
-    height: 100% !important;
-}
-
-div.camera-placeholder {
-    overflow: hidden;
-}
-
-div.camera-progress {
-    cursor: default;
-}
diff --git a/static/css/jquery.timepicker.css b/static/css/jquery.timepicker.css
deleted file mode 100755 (executable)
index ad4665d..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-\r
-.ui-timepicker-wrapper {\r
-       overflow-y: auto;\r
-       height: 150px;\r
-       width: 6.5em;\r
-       background: #414141;\r
-       border: 1px solid #515151;\r
-       -webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);\r
-       -moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);\r
-       box-shadow:0 5px 10px rgba(0,0,0,0.2);\r
-       outline: none;\r
-       z-index: 10001;\r
-       margin: 0;\r
-}\r
-\r
-.ui-timepicker-wrapper.ui-timepicker-with-duration {\r
-       width: 11em;\r
-}\r
-\r
-.ui-timepicker-list {\r
-       margin: 0;\r
-       padding: 0;\r
-       list-style: none;\r
-}\r
-\r
-.ui-timepicker-duration {\r
-       margin-left: 5px; color: #888;\r
-}\r
-\r
-.ui-timepicker-list:hover .ui-timepicker-duration {\r
-       color: #888;\r
-}\r
-\r
-.ui-timepicker-list li {\r
-       padding: 3px 0 3px 5px;\r
-       cursor: pointer;\r
-       white-space: nowrap;\r
-       color: white;\r
-       list-style: none;\r
-       font-size: 0.8em;\r
-       margin: 0;\r
-}\r
-\r
-.ui-timepicker-list:hover .ui-timepicker-selected {\r
-       background: #aaa; color: black;\r
-}\r
-\r
-li.ui-timepicker-selected,\r
-.ui-timepicker-list li:hover,\r
-.ui-timepicker-list .ui-timepicker-selected:hover {\r
-       background: #1980EC; color: #fff;\r
-}\r
-\r
-li.ui-timepicker-selected .ui-timepicker-duration,\r
-.ui-timepicker-list li:hover .ui-timepicker-duration {\r
-       color: #ccc;\r
-}\r
-\r
-.ui-timepicker-list li.ui-timepicker-disabled,\r
-.ui-timepicker-list li.ui-timepicker-disabled:hover,\r
-.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {\r
-       color: #888;\r
-       cursor: default;\r
-}\r
-\r
-.ui-timepicker-list li.ui-timepicker-disabled:hover,\r
-.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {\r
-       background: #f2f2f2;\r
-}\r
diff --git a/static/css/main.css b/static/css/main.css
deleted file mode 100644 (file)
index c02e29b..0000000
+++ /dev/null
@@ -1,1043 +0,0 @@
-
-
-    /* basic */
-
-* {
-    padding: 0px;
-    border: 0px solid black;
-    margin: 0px;
-    outline: 0px;
-    border-spacing: 0px;
-    border-collapse: separate;
-}
-
-html {
-    height: 100%;
-}
-
-body {
-    height: 100%;
-    color: #dddddd;
-    font-size: 22px;
-    background-color: #212121;
-}
-
-select,
-input[type=text],
-input[type=password],
-textarea {
-    box-sizing: border-box;
-}
-
-
-    /* fonts */
-
-@font-face {
-    font-family: 'Maven Pro';
-    src: url('../fnt/mavenpro-regular-webfont.eot');
-    src: url('../fnt/mavenpro-regular-webfont.eot?#iefix') format('embedded-opentype'),
-         url('../fnt/mavenpro-regular-webfont.woff') format('woff'),
-         url('../fnt/mavenpro-regular-webfont.ttf') format('truetype'),
-         url('../fnt/mavenpro-regular-webfont.svg#maven_proregular') format('svg');
-    font-weight: normal;
-    font-style: normal;
-}
-@font-face {
-    font-family: 'Maven Pro';
-    src: url('../fnt/mavenpro-bold-webfont.eot');
-    src: url('../fnt/mavenpro-bold-webfont.eot?#iefix') format('embedded-opentype'),
-         url('../fnt/mavenpro-bold-webfont.woff') format('woff'),
-         url('../fnt/mavenpro-bold-webfont.ttf') format('truetype'),
-         url('../fnt/mavenpro-bold-webfont.svg#maven_probold') format('svg');
-    font-weight: bold;
-    font-style: normal;
-}
-
-    /* layout */
-
-html {
-    font-family: 'Maven Pro';
-}
-
-div.page,
-div.header-container {
-    position: relative;
-    min-width: 320px;
-    width: 100%;
-}
-
-div.page {
-    font-size: 1em;
-    transition: all 0.5s linear;
-    min-height: 100%;
-}
-
-div.header {
-    background-color: rgba(64, 64, 64, 0.5);
-    box-shadow: 0px 0px 5px rgba(0,0,0,0.3);
-    top: 0px;
-    width: 100%;
-    height: 50px;
-    position: fixed;
-    overflow: hidden;
-    z-index: 10000;
-}
-
-div.header-container {
-    transition: all 0.5s linear;
-}
-
-div.footer {
-    position: absolute;
-    bottom: 5px;
-    width: 100%;
-    height: 3em;
-    font-size: 0.7em;
-    color: #aaa;
-    text-align: center;
-}
-
-div.copyright-note {
-    border-top: 1px solid #333;
-    padding-top: 0.2em;
-    margin: 0px 15%;
-}
-
-div.page-container {
-    transition: all 0.2s linear;
-    padding: 55px 5px 3em 2%;
-}
-
-div.page-container.stretched {
-    margin-left: 40%;
-    padding-left: 5px;
-}
-
-
-    /* icons & icon buttons */
-
-div.button.settings-button {
-    margin: 1px;
-    vertical-align: middle;
-    background-image: url(../img/settings.svg);
-    width: 48px;
-    height: 48px;
-}
-
-div.button.logout-button {
-    margin: 1px;
-    vertical-align: middle;
-    background-image: url(../img/logout.svg);
-    width: 48px;
-    height: 48px;
-}
-
-body.admin div.logout-button {
-    display: none;
-}
-
-body.admin div.settings-top-bar.closed div.logout-button {
-    display: inline-block;
-}
-
-body:not(.admin) div.settings-top-bar div.logout-button {
-    display: none;
-}
-
-div.button.rem-camera-button {
-    display: none;
-    margin: 1px;
-    vertical-align: middle;
-    background-image: url(../img/settings.svg);
-    width: 48px;
-    height: 48px;
-    background-position: -48px 0px;
-}
-
-div.settings-top-bar.open div.button.rem-camera-button {
-    display: inline-block;
-}
-
-div.logo {
-    float: right;
-    display: inline-block;
-    white-space: nowrap;
-    opacity: 0.86;
-}
-
-span.logo {
-    color: white;
-    vertical-align: middle;
-    font-size: 27px;
-    font-weight: bold;
-    position: relative;
-    top: 3px;
-}
-
-img.logo {
-    width: 36px;
-    height: 36px;
-    padding: 7px 3px;
-    vertical-align: middle;
-}
-
-img.background-logo {
-    position: absolute;
-    width: 30%;
-    left: 35%;
-    top: 10em;
-    opacity: 0.03;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    user-select: none;
-}
-
-img.main-loading-progress {
-    display: block;
-    margin: auto;
-    margin-top: 50px;
-}
-
-div.add-camera-message {
-    text-align: center;
-    margin-top: 30px;
-}
-
-div.hostname {
-    vertical-align: middle;
-    display: inline-block;
-    font-size: 27px;
-}
-
-
-    /* settings */
-
-div.settings {
-    background-color: #313131;
-    position: fixed;
-    z-index: 1;
-    top: 50px;
-    left: 0px;
-    width: 0px;
-    bottom: 0px;
-    transition: all 0.2s linear;
-    overflow: auto;
-}
-
-div.settings.open {
-    width: 40%;
-    min-width: 320px;
-}
-
-body:not(.admin) div.settings {
-    display: none !important;
-}
-
-div.settings-container {
-    position: relative;
-    padding-top: 10px;
-    display: none;
-    white-space: nowrap;
-}
-
-div.settings.open div.settings-container {
-    display: block;
-}
-
-div.settings-progress {
-    position: absolute;
-    top: 0px;
-    width: 0px;
-    bottom: 0px;
-    left: 0px;
-    background-color: #313131;
-    opacity: 0;
-    transition: opacity 0.1s linear;
-}
-
-div.settings-top-bar {
-    position: relative;
-    display: inline-block;
-    width: 40%;
-    height: 50px;
-}
-
-div.settings-top-bar.open {
-    background-color: #414141;
-    min-width: 320px;
-}
-
-body:not(.admin) div.settings-top-bar {
-    display: none !important;
-}
-
-div.settings-top-bar.closed div.apply-button {
-    display: none !important;
-}
-
-div.settings-section-title {
-    position: relative;
-    text-align: right;
-    background-color: rgba(100, 100, 100, 0.3);
-    padding: 5px 0.5em 5px 5px;
-}
-
-a.settings-section-title {
-}
-
-table.settings {
-    width: 100%;
-    padding: 0.5em 0.5em 1em 0.5em;
-}
-
-td.settings-item-label {
-    width: 50%;
-    text-align: right;
-    padding-right: 5px;
-}
-
-td.settings-item-value {
-    width: 50%;
-    text-align: left;
-    padding-left: 5px;
-}
-
-span.settings-item-label {
-    font-size: 0.9em;
-}
-
-span.settings-item-unit {
-    font-size: 0.6em;
-    padding: 0px 0.2em;
-}
-
-div.settings-item-separator {
-    height: 1px;
-    border-top: 1px solid #414141;
-    margin: 0.5em 1em;
-}
-
-#cameraSelect {
-    display: none;
-    padding: 4px 1.5em 4px 4px;
-    vertical-align: middle;
-    font-size: 1.1em;
-    width: auto;
-    max-width: 35%;
-}
-
-div.apply-button {
-    position: relative;
-    display: none;
-    opacity: 0;
-    float: right;
-    width: 4em;
-    height: 30px;
-    line-height: 30px;
-    text-align: center;
-    margin: 10px;
-    color: white;
-    background-color: #FF6F00;
-    border-radius: 3px;
-    transition: all 0.1s linear;
-}
-
-div.apply-button:HOVER {
-    background-color: #FF7D19;
-}
-
-div.apply-button:ACTIVE {
-    background-color: #F06800;
-}
-
-div.apply-button.progress {
-    background-color: #FF6F00;
-}
-
-img.apply-progress {
-    margin-top: 3px;
-}
-
-div.normal-button {
-    position: relative;
-    height: 1.5em;
-    line-height: 1.5em;
-    text-align: center;
-    margin: 2px 0px;
-    color: white;
-    font-size: 0.9em;
-    border-radius: 3px;
-    transition: all 0.1s linear;
-    width: 7em;
-}
-
-div.update-button,
-div.backup-button,
-div.restore-button {
-    background: #317CAD;
-}
-
-div.shut-down-button,
-div.reboot-button {
-    background: #c0392b;
-}
-
-div.update-button:HOVER,
-div.backup-button:HOVER,
-div.restore-button:HOVER {
-    background: #3498db;
-}
-
-div.shut-down-button:HOVER,
-div.reboot-button:HOVER {
-    background: #D43F2F;
-}
-
-div.update-button:ACTIVE,
-div.backup-button:ACTIVE,
-div.restore-button:ACTIVE {
-    background: #317CAD;
-}
-
-div.shut-down-button:ACTIVE,
-div.reboot-button:ACTIVE {
-    background: #B03427;
-}
-
-div.settings-top-bar.open #cameraSelect {
-    display: inline;
-}
-
-div.settings-top-bar.open div.logout-button {
-    display: none;
-}
-
-div.check-box.section {
-    margin: 0px;
-    float: left;
-}
-
-div.check-box.section div.check-box-button {
-    background-color: #515151;
-}
-
-div.check-box.on.section div.check-box-button {
-    background-color: #317CAD;
-}
-
-div.check-box.on.section:FOCUS div.check-box-button,
-div.check-box.on.section:HOVER div.check-box-button {
-    background-color: #3498db;
-}
-
-input[type=text].working-schedule.number {
-    width: 50px;
-}
-
-#diskUsageProgressBar {
-    width: 90%;
-}
-
-div.hidden,
-tr.hidden {
-    display: none !important;
-}
-
-span.help-mark {
-    display: inline-block;
-    visibility: hidden;
-    text-align: center;
-    background-color: #414141;
-    color: #3498db;
-    font-size: 0.75em;
-    font-family: monospace;
-    width: 1.2em;
-    height: 1.2em;
-    border-radius: 100em;
-    cursor: pointer;
-    vertical-align: middle;
-    position: relative;
-    top: -0.1em;
-}
-
-div.settings-section-title > span.help-mark {
-    background-color: #515151;
-}
-
-div.settings-section-title:HOVER > span.help-mark,
-tr:HOVER span.help-mark {
-    visibility: visible;
-}
-
-span.minimize {
-    display: inline-block;
-    background-image: url(../img/combo-box-arrow.svg);
-    background-size: cover;
-    width: 0.8em;
-    height: 0.8em;
-    cursor: pointer;
-    vertical-align: middle;
-    position: relative;
-    top: -0.1em;
-    transition: transform 0.1s linear;
-    -webkit-transform: rotate(90deg);
-    -moz-transform: rotate(90deg);
-    -ms-transform: rotate(90deg);
-    -o-transform: rotate(90deg);
-    transform: rotate(90deg);
-}
-
-span.minimize.open {
-    -webkit-transform: rotate(0deg);
-    -moz-transform: rotate(0deg);
-    -ms-transform: rotate(0deg);
-    -o-transform: rotate(0deg);
-    transform: rotate(0deg);
-}
-
-
-    /* dialogs */
-    
-table.login-dialog {
-    margin: auto;
-    font-size: 22px; /* always bigger, regardless of screen size */
-}
-
-table.add-camera-dialog {
-    margin: auto;
-}
-
-table.add-camera-dialog select,
-table.add-camera-dialog input[type=text],
-table.add-camera-dialog input[type=password] {
-    width: 17em;
-}
-
-span#cameraMsgLabel {
-    color: red;
-    font-size: 0.7em;
-}
-
-div#addCameraInfo {
-    font-size: 0.7em;
-    max-width: 33em;
-}
-
-div.media-dialog {
-}
-
-div.media-dialog-groups {
-    float: left;
-    width: 11em;
-    text-align: center;
-    overflow: auto;
-    white-space: nowrap;
-}
-
-div.media-dialog-groups.small-screen {
-    float: none;
-}
-
-div.media-dialog-group-button {
-    height: 1.5em;
-    width: 10.5em;
-    box-sizing: border-box;
-    line-height: 1.5em;
-    text-align: center;
-    margin: 0em 0.2em 0.2em 0.2em;
-    padding: 0px 0.5em;
-    background-color: #414141;
-    color: #3498db;
-    border-radius: 3px;
-    transition: all 0.1s linear;
-    cursor: pointer;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-div.media-dialog-groups.small-screen div.media-dialog-group-button {
-    display: inline-block;
-}
-
-div.media-dialog-group-button:HOVER {
-    background-color: #515151;
-}
-
-div.media-dialog-group-button:ACTIVE {
-    background-color: #414141;
-}
-
-div.media-dialog-group-button.current {
-    background-color: #317CAD;
-    color: white;
-}
-
-div.media-dialog-group-button.current:HOVER {
-    background-color: #3498db;
-}
-
-div.media-dialog-group-button.current:ACTIVE {
-    background-color: #317CAD;
-}
-
-div.media-dialog-list {
-    overflow: auto;
-    position: relative;
-}
-
-div.media-list-group-title {
-    background-color: #313131;
-    font-size: 1.3em;
-    font-weight: bold;
-    text-align: center;
-    padding: 1em 0px 0.2em 0px;
-}
-
-img.media-list-progress {
-    position: relative;
-    top: 35%;
-    display: block;
-    margin: auto;
-}
-
-div.media-list-entry {
-    height: 4em;
-    background-color: #414141;
-    border-bottom: 1px solid #313131;
-    cursor: pointer;
-    transition: background-color 0.1s linear;
-}
-
-div.media-list-entry:HOVER {
-    background-color: #494949;
-}
-
-div.media-list-entry:ACTIVE {
-    background-color: #3b3b3b;
-}
-
-img.media-list-preview {
-    float: left;
-    height: 3em;
-    margin: 0.45em;
-    border: 1px solid #212121;
-    box-shadow: 1px 1px 6px rgba(0,0,0,0.3);
-}
-
-div.media-list-entry-name {
-    font-weight: bold;
-    font-size: 1.3em;
-    padding: 0.4em 0em;
-    text-align: center;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-div.media-list-entry-details {
-    font-size: 1em;
-    text-align: center;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-}
-
-div.media-list-entry-details span.details-moment-short {
-    display: none;
-}
-
-div.media-list-download-button,
-div.media-list-delete-button {
-    float: right;
-    clear: right;
-    height: 1.5em;
-    width: 5em;
-    line-height: 1.5em;
-    text-align: center;
-    margin: 0px 0.5em;
-    padding: 0px 0.5em;
-    color: white;
-    border-radius: 3px;
-    transition: all 0.1s linear;
-}
-
-div.media-list-download-button {
-    margin-top: 0.4em;
-    margin-bottom: 0.1em;
-    background: #317CAD;
-}
-
-div.media-list-download-button:HOVER {
-    background-color: #3498db;
-}
-
-div.media-list-download-button:ACTIVE {
-    background-color: #317CAD;
-}
-
-div.media-list-delete-button {
-    margin-top: 0.1em;
-    margin-bottom: 0.4em;
-    background: #c0392b;
-}
-
-div.media-list-delete-button:HOVER {
-    background-color: #D43F2F;
-}
-
-div.media-list-delete-button:ACTIVE {
-    background-color: #B03427;
-}
-
-div.media-dialog-buttons {
-    margin: 0.5em 0px 0px 0px;
-    text-align: center;
-}
-
-div.media-dialog-button {
-    cursor: pointer;
-    display: inline-block;
-    height: 1.5em;
-    line-height: 1.5em;
-    text-align: center;
-    padding: 0px 0.5em;
-    margin: 0px 5px 0px 0px;
-    color: white;
-    background-color: #317CAD;
-    border-radius: 3px;
-    transition: all 0.1s linear;
-}
-
-div.media-dialog-button:HOVER {
-    background-color: #3498db;
-}
-
-div.media-dialog-button:ACTIVE {
-    background-color: #317CAD;
-}
-
-div.picture-dialog-content {
-    position: relative;
-    text-align: center;
-    min-height: 100px;
-}
-
-div.picture-dialog-prev-arrow,
-div.picture-dialog-next-arrow {
-    position: absolute;
-    top: 45%;
-    background-color: rgba(0, 0, 0, 0.6);
-    background-image: url(../img/arrows.svg);
-    background-size: cover;
-    width: 3em;
-    height: 3em;
-    border-radius: 0.3em;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    user-select: none;
-    cursor: pointer;
-}
-
-div.picture-dialog-prev-arrow {
-    left: 1em;
-}
-
-div.picture-dialog-next-arrow {
-    right: 1em;
-    background-position: -100% 0%;
-}
-
-img.picture-dialog-content {
-    border: 1px solid #292929;
-}
-
-img.picture-dialog-progress {
-    position: absolute;
-    background-color: #313131;
-    padding: 10px;
-    border-radius: 10px;
-    opacity: 0.7;
-}
-
-table.timelapse-dialog select {
-    width: 10em;
-}
-
-td.timelapse-warning {
-    font-size: 80%;
-    display: none;
-    color: red;
-    max-width: 20em;
-    text-align: center;
-    white-space: normal;
-    padding-bottom: 1em;
-}
-
-div.media-dialog-delete-all-button {
-    margin-top: 0.1em;
-    margin-bottom: 0.4em;
-    background: #c0392b;
-}
-
-div.media-dialog-delete-all-button:HOVER {
-    background-color: #D43F2F;
-}
-
-div.media-dialog-delete-all-button:ACTIVE {
-    background-color: #B03427;
-}
-
-td.login-dialog-error {
-    color: red;
-    display: none;
-}
-
-
-    /* camera frames */
-
-div.camera-list {
-    text-align: center;
-}
-
-div.camera-frame,
-div.camera-frame-place-holder {
-    position: relative;
-    width: 32%;
-    text-align: left;
-    background-color: #313131;
-    display: inline-block;
-    padding: 0px 3px 3px 3px;
-    border-radius: 3px;
-    transition: all 0.2s, opacity 0s;
-    margin: 2px;
-    opacity: 0;
-    vertical-align: top;
-}
-
-div.camera-frame:only-child,
-div.camera-frame-place-holder:only-child {
-    width: 48%;
-}
-
-div.camera-frame-place-holder {
-    visibility: hidden;
-}
-
-div.camera-frame.motion-detected {
-    background-color: #712727;
-}
-
-div.modal-container div.camera-frame {
-    width: auto;
-    padding: 0px;
-    margin: -7px;
-    background-color: #414141;
-}
-
-div.camera-frame:HOVER {
-    background-color: #414141;
-}
-
-div.camera-frame.motion-detected:HOVER {
-    background-color: #8B3636;
-}
-
-div.camera-top-bar {
-    padding: 3px 0px;
-    font-size: 20px;
-    height: 25px;
-}
-
-div.modal-container div.camera-top-bar {
-    display: none;
-}
-
-span.camera-name {
-    float: left;
-    line-height: 25px;
-}
-
-div.camera-buttons {
-    float: right;
-}
-
-div.camera-button {
-    display: inline-block;
-    width: 24px;
-    height: 24px;
-    background-image: url(../img/top-bar-buttons.svg);
-    background-size: cover;
-    margin-left: 3px;
-    cursor: pointer;
-    transition: all 0.1s linear;
-}
-
-div.camera-button.close {
-    background-position: 0px 0px;
-}
-
-div.camera-button.full-screen {
-    background-position: -200% 0px;
-}
-
-div.camera-button.configure {
-    background-position: -100% 0px;
-}
-
-div.camera-button.media-pictures {
-    background-position: -300% 0px;
-}
-
-div.camera-button.media-movies {
-    background-position: -400% 0px;
-}
-
-div.camera-container {
-    position: relative;
-    padding: 0px;
-}
-
-img.camera {
-    position: relative;
-    width: 100%;
-    display: block;
-    transition: opacity 0.2s linear;
-    opacity: 1;
-    min-height: 160px;
-}
-
-img.camera.error,
-img.camera.loading {
-    opacity: 0;
-}
-
-div.camera-placeholder {
-    position: absolute;
-    top: 0px;
-    right: 0px;
-    bottom: 0px;
-    left: 0px;
-    text-align: center;
-    transition: opacity 0.2s linear;
-}
-
-img.no-camera {
-    margin-top: 20%;
-    width: 30%;
-    opacity: 0.8;
-}
-
-div.camera-progress {
-    background: rgba(0, 0, 0, 0.001); /* otherwise IE would not extend this as expected */
-    position: absolute;
-    top: 0px;
-    right: 0px;
-    bottom: 0px;
-    left: 0px;
-    opacity: 0;
-    transition: all 0.2s linear;
-    text-align: center;
-    cursor: pointer;
-}
-
-div.camera-progress.visible {
-    opacity: 0.4;
-}
-
-img.camera-progress {
-    border: 10px solid white;
-    border-radius: 10px;
-    position: absolute;
-    top: 0px;
-    left: 0px;
-    bottom: 0px;
-    right: 0px;
-    margin: auto;
-}
-
-
-    /* media queries */
-
-@media all and (max-width: 1440px) {
-    /* smaller screens */
-    
-    body {
-        font-size: 17px;
-    }
-}
-
-@media all and (max-width: 1000px) {
-    /* small screens (mobile devices) */
-    
-    div.page-container.stretched {
-        margin-left: 0px;
-    }
-    
-    div.settings.open {
-        box-shadow: 0px 0px 10px rgba(0,0,0,0.5);
-        background-color: rgba(40, 40, 40, 0.9);
-    }
-    
-    div.hostname {
-        display: none;
-    }
-
-    div.media-list-entry-name {
-        font-size: 1em;
-        padding: 0.2em 0em 0em 0em;
-    }
-
-    div.media-list-entry-details {
-        padding-top: 0.2em;
-        font-size: 1em;
-        text-align: center;
-        white-space: normal;
-    }
-
-    div.media-list-entry-details span.details-moment {
-        display: none;
-    }
-
-    div.media-list-entry-details span.details-moment-short {
-        display: block;
-    }
-}
-
-@media all and (max-width: 400px) {
-    /* very small screens */
-    
-    body {
-        font-size: 13px;
-    }
-    
-    div.camera-button {
-        background-size: cover;
-        width: 24px;
-        height: 24px;
-    }
-}
-
-@media all and (max-width: 1900px) {
-    div.camera-frame, 
-    div.camera-frame-place-holder {
-        width: 48%;
-    }
-}
-
-@media all and (max-width: 1200px) {
-    div.page-container {
-        padding-left: 1%;
-    }
-
-    div.camera-frame,
-    div.camera-frame-place-holder {
-        width: 98%;
-    }
-
-    div.camera-frame:only-child,
-    div.camera-frame-place-holder:only-child {
-        width: 98%;
-    }
-}
diff --git a/static/css/ui.css b/static/css/ui.css
deleted file mode 100644 (file)
index f114a8b..0000000
+++ /dev/null
@@ -1,469 +0,0 @@
-
-    /* general */
-
-::selection,
-::-moz-selection,
-::-webkit-selection {
-    background: #3498db;
-}
-
-option::selection,
-option::-moz-selection,
-option::-webkit-selection {
-    background: transparent;
-}
-
-input[type=checkbox].styled {
-    display: none;
-}
-
-a {
-    color: #3498db;
-    text-decoration: inherit;
-    transition: color 0.1s ease;
-    cursor: pointer;
-}
-
-a:HOVER {
-}
-
-a:ACTIVE {
-    color: #317CAD;
-}
-
-
-    /* buttons */
-
-div.button {
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    user-select: none;
-    cursor: pointer;
-    display: inline-block;    
-}
-
-div.button.dialog {
-    background-color: #414141;
-    min-width: 60px;
-    height: 1.2em;
-    line-height: 1.2em;
-    text-align: center;
-    padding: 0.2em 0.4em;
-    border: 1px solid #317CAD;
-    border-radius: 2px;
-    color: white;
-    transition: all 0.1s ease;
-}
-
-div.button.dialog.default {
-    background-color: #317CAD;
-}
-
-div.button.mouse-effect {
-    opacity: 0.7;
-    transition: opacity 0.1s ease;
-}
-
-div.button.mouse-effect:HOVER {
-    opacity: 1;
-}
-
-div.button.mouse-effect:ACTIVE {
-    opacity: 0.7;
-}
-
-
-    /* check box */
-
-div.check-box {
-    display: inline-block;
-    position: relative;
-    width: 2.5em;
-    height: 1em;
-    border: 1px solid #317CAD;
-    border-radius: 2px;
-    color: #aaaaaa;
-    vertical-align: middle;
-    cursor: pointer;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    user-select: none;
-    margin: 2px;
-    transition: all 0.2s ease;
-}
-
-div.check-box:FOCUS,
-div.check-box:HOVER {
-    border-color: #3498db;
-}
-
-div.check-box-button {
-    width: 50%;
-    height: 100%;
-    left: 0px;
-    background-color: #414141;
-    color: #aaaaaa;
-    position: absolute;
-    text-align: center;
-    line-height: 1em;
-    transition: all 0.1s ease;
-}
-
-span.check-box-text {
-    font-size: 0.5em;
-    font-weight: bold;
-    vertical-align: top;
-}
-
-div.check-box.on div.check-box-button {
-    left: 50%;
-    background-color: #317CAD;
-    color: white;
-}
-
-div.check-box.on:FOCUS div.check-box-button,
-div.check-box.on:HOVER div.check-box-button {
-    background-color: #3498db;
-}
-
-
-    /* input boxes */
-
-input[type=password].styled,
-input[type=text].styled,
-textarea.styled {
-    width: 90%;
-    border: 1px solid #317CAD;
-    border-radius: 2px;
-    background-color: transparent;
-    padding: 1px;
-    color: #dddddd;
-    font-family: inherit;
-    font-size: 0.8em;
-    margin: 2px;
-    transition: all 0.1s ease;
-    resize: none;
-    vertical-align: middle;
-    white-space: nowrap;
-}
-
-input[type=password].styled:FOCUS,
-input[type=text].styled:FOCUS {
-    background-color: #414141;
-}
-
-input[type=password].styled:HOVER,
-input[type=password].styled:FOCUS,
-input[type=text].styled:HOVER,
-input[type=text].styled:FOCUS {
-    border-color: #3498db;
-    color: white;
-}
-
-input[type=text].number {
-    width: 5em;
-}
-
-input[type=text].error,
-input[type=password].error,
-input[type=file].error,
-select.error {
-    background-image: url(../img/validation-error.svg) !important;
-    background-position: center right;
-    background-repeat: no-repeat;
-}
-
-input[type=text].time {
-    width: 3.5em;
-}
-
-input[readonly] {
-    border: 1px solid #555 !important;
-}
-
-
-    /* combo box */
-
-select.styled {
-    -webkit-appearance: none;
-    appearance: none;
-    width: 90%;
-    border: 1px solid #317CAD;
-    border-radius: 2px;
-    background-color: transparent;
-    padding: 1px 1.25em 1px 1px;
-    color: #dddddd;
-    font-family: inherit;
-    font-size: 0.8em;
-    margin: 2px;
-    background-image: url(../img/combo-box-arrow.svg);
-    background-repeat: no-repeat;
-    background-position: right center;
-    cursor: pointer;
-    vertical-align: middle;
-}
-
-select.styled:FOCUS {
-    background-color: #414141;
-    appearance: auto;
-}
-
-select.styled:HOVER,
-select.styled:FOCUS {
-    border-color: #3498db;
-    color: white;
-}
-
-.ff select.styled {
-    background-image: none;
-    padding-right: 1px;
-}
-
-
-    /* slider */
-
-input[type=text].range {
-    display: none;
-}
-
-div.slider {
-    width: 82%;
-    height: 1.7em;
-    position: relative;
-    padding: 0.2em 0px;
-    margin-left: 5%;
-}
-
-div.slider-labels {
-    position: relative;
-    width: 100%;
-    height: 0.5em;
-}
-
-span.slider-label {
-    display: inline-block;
-    width: 20%;
-    text-align: center;
-    overflow: visible;
-    position: absolute;
-    font-size: 0.5em;
-}
-
-div.slider-bar {
-    position: relative;
-    top: 7px;
-    left: -5px;
-    width: 100%;
-}
-
-div.slider-bar-inside {
-    border: 1px solid #317CAD;
-    height: 3px;
-    position: relative;
-    top: 3px;
-    left: 7px;
-    transition: all 0.1s ease;
-}
-
-div.slider:HOVER div.slider-bar-inside,
-div.slider:FOCUS div.slider-bar-inside {
-    border-color: #3498db;
-}
-
-div.slider:FOCUS div.slider-bar-inside {
-    background-color: #414141;
-}
-
-div.slider-cursor {
-    background-image: url(../img/slider-arrow.svg);
-    width: 11px;
-    height: 18px;
-    position: absolute;
-    left: 0px;
-    top: -5px;
-    cursor: pointer;
-}
-
-div.slider-cursor-label {
-    font-size: 0.6em;
-    margin-left: 1.5em;
-    margin-top: 0.15em;
-    background: rgba(30, 30, 30, 0.8);
-    vertical-align: top;
-    padding: 1px 2px;
-    border-radius: 3px;
-    display: none;
-}
-
-
-    /* progress bar */
-
-div.progress-bar-container {
-    position: relative;
-    height: 1em;
-    border: 1px solid #555;
-    vertical-align: middle;
-    margin: 0px 0.2em;
-    text-align: center;
-    line-height: 1em;
-}
-
-div.progress-bar-fill {
-    position: absolute;
-    left: 0px;
-    top: 0px;
-    bottom: 0px;
-    width: 0%;
-    background-color: #555;
-    transition: width 0.1s ease;
-}
-
-span.progress-bar-text {
-    vertical-align: middle;
-    font-size: 0.8em;
-    position: relative;
-}
-
-
-    /* modal dialogs */
-    
-div.modal-glass {
-    display: none;
-    position: fixed;
-    z-index: 10000;
-    top: 0px;
-    right: 0px;
-    bottom: 0px;
-    left: 0px;
-    background-color: black;
-    opacity: 0;
-}
-
-div.modal-container {
-    position: fixed;
-    display: none;
-    z-index: 10001;
-    background-color: #313131;
-    border-radius: 3px;
-    opacity: 0;
-    padding: 5px;
-    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
-}
-
-div.modal-title-bar {
-    min-height: 1.5em;
-    line-height: 1.5em;
-    text-align: center;
-    position: relative;
-}
-
-span.modal-title {
-    color: white;
-    font-size: 1.2em;
-    line-height: 1.2em;
-}
-
-div.modal-close-button {
-    position: absolute;
-    top: 0.2em;
-    right: 0.3em;
-    width: 1.1em;
-    height: 1.1em;
-    background-image: url(../img/top-bar-buttons.svg);
-    background-size: cover;
-    cursor: pointer;
-}
-
-table.modal-buttons-container {
-    width: 100%;
-    margin: auto;
-    text-align: center;
-    table-layout: fixed;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    user-select: none;
-}
-
-table.modal-buttons-container td:not(:FIRST-CHILD) {
-    padding-left: 5px;
-}
-
-table.modal-buttons-container div.button.dialog {
-    display: block;
-}
-
-div.modal-progress {
-    border-radius: 10px;
-    background-image: url(../img/modal-progress.gif);
-    width: 64px;
-    height: 64px;
-    margin: auto;
-}
-
-td.dialog-item-label {
-    text-align: right;
-    padding-right: 5px;
-}
-
-td.dialog-item-value {
-    text-align: left;
-    padding-left: 5px;
-}
-
-span.dialog-item-label {
-    font-size: 0.9em;    
-}
-
-div.dialog-item-separator {
-    height: 1px;
-    border-top: 1px solid #414141;
-    margin: 0.5em 1em;
-}
-
-   
-    /* popup message */
-
-div.popup-message-container {
-    position: fixed;
-    display: none;
-    z-index: 10002;
-    background-color: #313131;
-    border-radius: 3px;
-    opacity: 0;
-    padding: 5px;
-    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
-    top: 60px;
-}
-
-span.popup-message {
-    
-}
-
-span.popup-message.info {
-    color: white;
-}
-
-span.popup-message.error {
-    color: #FF6D55;
-}
-
-
-    /* media queries */
-
-@media all and (max-width: 400px) {
-    span.modal-title {
-        font-size: 1.5em;
-        
-    }
-    
-    div.modal-title-bar {
-        min-height: 2em;
-        line-height: 2em;
-    }
-    
-    div.modal-close-button {
-        background-size: cover;
-        width: 24px;
-        height: 24px;
-    }
-}
diff --git a/static/favicon.ico b/static/favicon.ico
deleted file mode 100644 (file)
index 6cb844d..0000000
Binary files a/static/favicon.ico and /dev/null differ
diff --git a/static/fnt/mavenpro-black-webfont.eot b/static/fnt/mavenpro-black-webfont.eot
deleted file mode 100644 (file)
index 7ee4811..0000000
Binary files a/static/fnt/mavenpro-black-webfont.eot and /dev/null differ
diff --git a/static/fnt/mavenpro-black-webfont.svg b/static/fnt/mavenpro-black-webfont.svg
deleted file mode 100644 (file)
index b888155..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
-<svg xmlns="http://www.w3.org/2000/svg">
-<metadata></metadata>
-<defs>
-<font id="maven_problack" horiz-adv-x="1159" >
-<font-face units-per-em="2048" ascent="1638" descent="-410" />
-<missing-glyph horiz-adv-x="692" />
-<glyph horiz-adv-x="2048" />
-<glyph horiz-adv-x="2048" />
-<glyph unicode="&#xd;" horiz-adv-x="682" />
-<glyph unicode=" "  horiz-adv-x="692" />
-<glyph unicode="&#x09;" horiz-adv-x="692" />
-<glyph unicode="&#xa0;" horiz-adv-x="692" />
-<glyph unicode="!" horiz-adv-x="882" d="M279 1366h354l-45 -975h-264zM297 0v262h317v-262h-317z" />
-<glyph unicode="&#x22;" horiz-adv-x="899" d="M166 1384h254l-35 -411h-182zM477 1384h254l-35 -411h-182z" />
-<glyph unicode="#" horiz-adv-x="1288" d="M152 248v186h176q4 27 19 110l24 132h-164v184h194l37 209h197l-37 -209h217q33 170 37 209h197l-39 -209h125v-184h-154q-4 -27 -20.5 -111t-22.5 -131h143v-186h-174q-6 -37 -19.5 -111t-17.5 -98h-196l37 209h-219q-8 -43 -35 -209h-197l39 209h-147zM524 434h221 l39 242h-217q-6 -47 -22.5 -131t-20.5 -111z" />
-<glyph unicode="$" horiz-adv-x="1212" d="M147 952q0 193 123 299.5t316 120.5v217h125v-215q168 -14 311 -92v-307q-162 96 -311 98v-244q88 -25 148.5 -50t129 -73.5t104 -129t35.5 -189.5q0 -164 -118.5 -272.5t-298.5 -128.5v-207h-125v203q-250 12 -422 133v329q189 -157 419 -157h3v243q-94 23 -154.5 45.5 t-134.5 68.5t-112 123t-38 185zM457 958q0 -57 129 -96v203q-129 -23 -129 -107zM711 299q120 27 120 101q0 60 -120 100v-201z" />
-<glyph unicode="%" horiz-adv-x="1890" d="M147 983q0 377 289 377q291 0 291 -377t-291 -377q-289 0 -289 377zM354 983q0 -145 82 -145q84 0 84 145q0 88 -22.5 116.5t-61.5 28.5q-82 0 -82 -145zM387 0l858 1366h234l-858 -1366h-234zM1141 383q0 377 289 377q291 0 291 -376q-1 -378 -291 -378q-289 0 -289 377 zM1346 383q0 -145 84 -145q82 0 82 143q0 147 -82 147q-84 0 -84 -145z" />
-<glyph unicode="&#x26;" horiz-adv-x="1337" d="M113 397q0 113 45 207t192 189q0 2 -9 13l-15 20q-7 8 -18 24t-19 30.5t-17.5 36t-15.5 41t-10 43t-4 46.5q0 137 86 232t247 95q160 0 249 -91t89 -239q0 -111 -47 -179t-161 -140l22 -27q14 -18 59 -70l107 -126l31 131h235q-32 -195 -104 -328l254 -305h-308l-92 117 q-147 -133 -352 -133q-115 0 -204 41t-139 105.5t-75.5 133t-25.5 133.5zM342 391q0 -23 9 -51.5t32 -60t69 -53t109 -21.5q119 0 207 84l-268 323q-57 -31 -92 -63.5t-48.5 -64t-15.5 -48t-2 -45.5zM469 1049q0 -41 82 -144q82 47 108.5 80t26.5 72q0 43 -31.5 73.5 t-77 30.5t-77 -32.5t-31.5 -79.5z" />
-<glyph unicode="'" horiz-adv-x="583" d="M166 1384h254l-35 -411h-182z" />
-<glyph unicode="(" horiz-adv-x="796" d="M190 627q0 440 211 856h258q-210 -414 -210 -852q0 -440 210 -862h-258q-211 422 -211 858z" />
-<glyph unicode=")" horiz-adv-x="796" d="M133 -231q211 422 211 858q0 440 -211 856h258q211 -416 211 -856q0 -436 -211 -858h-258z" />
-<glyph unicode="*" horiz-adv-x="845" d="M172 1133l158 57l-158 57l78 139l110 -79l-12 131h160l-10 -131l108 79l78 -139l-156 -57l156 -57l-78 -140l-108 80l10 -131h-160l12 131l-110 -80z" />
-<glyph unicode="+" horiz-adv-x="1128" d="M190 498v229h259v258h229v-258h256v-229h-256v-256h-229v256h-259z" />
-<glyph unicode="," horiz-adv-x="751" d="M152 -229l151 401h285l-197 -401h-239z" />
-<glyph unicode="-" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
-<glyph unicode="." horiz-adv-x="811" d="M260 0v283h295v-283h-295z" />
-<glyph unicode="/" horiz-adv-x="948" d="M-29 -201l752 1749h256l-754 -1749h-254z" />
-<glyph unicode="0" horiz-adv-x="1345" d="M119 684q0 700 553 700t553 -700t-553 -700t-553 700zM473 684q0 -229 55.5 -312t143.5 -83q90 0 146 83t56 312q0 399 -202 399q-199 0 -199 -399z" />
-<glyph unicode="1" horiz-adv-x="1040" d="M211 840v346l285 180h288v-1366h-352v975z" />
-<glyph unicode="2" horiz-adv-x="1269" d="M158 0v279q579 433 579 642q0 62 -45 100t-133 38q-180 0 -391 -168v356q223 137 424 137q254 0 373.5 -128t119.5 -308q0 -158 -116.5 -325.5t-272.5 -305.5h398v-317h-936z" />
-<glyph unicode="3" horiz-adv-x="1228" d="M139 98v338q229 -137 402 -137q88 0 144 41t56 102.5t-47 105.5t-159 44h-224v254h228q72 0 107.5 32.5t35.5 79.5q0 49 -55.5 76t-126.5 27h-28h-30q-8 0 -28.5 -1t-32.5 -3t-33.5 -5t-39.5 -8l-42 -13q-24 -7 -50 -17t-52 -23v297q182 96 375 96q88 0 170 -18 t158.5 -58t122.5 -117t46 -181q0 -96 -65.5 -174.5t-147.5 -94.5q121 -35 195 -131t74 -227q0 -117 -49.5 -199t-133.5 -124t-170 -59t-184 -17q-190 0 -416 114z" />
-<glyph unicode="4" horiz-adv-x="1265" d="M80 348v270l598 748h307v-719h164v-299h-164v-348h-350v348h-555zM455 647h180v240z" />
-<glyph unicode="5" horiz-adv-x="1280" d="M162 707l74 661h806v-315h-499l-11 -119q31 4 99 4q229 0 356 -125t127 -385q0 -127 -41 -218t-112.5 -138t-151.5 -67.5t-174 -20.5q-213 0 -442 114v334q213 -135 395 -135q88 0 132 49t44 113q0 82 -45 124t-117 42h-17q-136 0 -229 -60z" />
-<glyph unicode="6" horiz-adv-x="1234" d="M111 630q0 196 49 341t117.5 221t163 122t165 58t142.5 12q176 0 278 -49v-297q-145 45 -250 45q-115 0 -211 -51t-110 -158q67 27 190 27q96 0 170 -15.5t148.5 -55t115.5 -129t41 -220.5q0 -90 -27.5 -175t-85 -158.5t-158.5 -118.5t-233 -45q-505 0 -505 646zM461 629 q0 -160 43 -247q46 -95 112 -95q61 0 98.5 45t44.5 83t7 68q0 78 -38 137.5t-116 59.5q-98 0 -151 -23v-28z" />
-<glyph unicode="7" horiz-adv-x="1226" d="M135 1016v350h1004v-258l-537 -1108h-407l495 1016h-555z" />
-<glyph unicode="8" horiz-adv-x="1261" d="M119 397q0 123 78.5 215.5t193.5 122.5q-88 25 -153.5 98.5t-65.5 174.5q0 376 459 376h2q459 0 459 -376q0 -100 -65 -175t-153 -98q113 -31 193 -123t80 -215q0 -205 -135 -309t-379 -104t-379 104.5t-135 308.5zM473 442q0 -74 40 -122t120 -48q78 0 118 48.5 t40 121.5q0 51 -41 93.5t-117 42.5q-78 0 -119 -42t-41 -94zM514 979q0 -53 37 -83t82 -30q43 0 80 30t37 83q0 43 -31 80t-86 37q-57 0 -88 -37t-31 -80z" />
-<glyph unicode="9" horiz-adv-x="1259" d="M111 887q0 90 27.5 175t85 158.5t158.5 118.5t232 45q506 0 506 -649q0 -193 -49 -338t-117.5 -221t-163 -122t-165 -58t-142.5 -12q-176 0 -278 49v297q145 -45 250 -45q115 0 211 51t110 158q-67 -27 -190 -27q-96 0 -170 15.5t-148.5 55.5t-115.5 129t-41 220z M465 885q0 -78 38 -137.5t115 -59.5q98 0 152 23v28q0 159 -43 247q-47 95 -113 95q-61 0 -98 -45t-44 -83t-7 -68z" />
-<glyph unicode=":" horiz-adv-x="868" d="M281 72v282h294v-282h-294zM281 670v282h294v-282h-294z" />
-<glyph unicode=";" horiz-adv-x="913" d="M166 -229l151 401h285l-196 -401h-240zM309 670v282h295v-282h-295z" />
-<glyph unicode="&#x3c;" horiz-adv-x="1368" d="M246 455v186l862 383v-279l-504 -196l504 -199v-278z" />
-<glyph unicode="=" horiz-adv-x="1306" d="M250 285v239h805v-239h-805zM250 698v240h805v-240h-805z" />
-<glyph unicode="&#x3e;" horiz-adv-x="1351" d="M238 72v278l503 199l-503 196v279l862 -383v-186z" />
-<glyph unicode="?" horiz-adv-x="1075" d="M164 1024v262q59 41 155.5 68.5t196.5 27.5q115 0 200 -33.5t132 -92t69.5 -126t22.5 -145.5q0 -70 -22.5 -126t-56.5 -92l-73 -74l-72 -73q-34 -34 -56.5 -87.5t-22.5 -118.5v-21h-281v27q0 104 31 184t75 127t89 86t75.5 81t30.5 89q0 45 -15 76t-46 43t-58.5 16 t-66.5 4q-180 0 -307 -102zM338 0v262h317v-262h-317z" />
-<glyph unicode="@" horiz-adv-x="1869" d="M166 373q0 182 79 346t203.5 274.5t280 175t309.5 64.5q106 0 220 -38t220.5 -112.5t175 -211t68.5 -308.5q0 -207 -92 -362.5t-268 -155.5q-68 0 -114 41t-64 90q-31 -57 -103.5 -94t-175.5 -37q-156 0 -246 70.5t-90 193.5q0 53 17.5 100.5t63.5 95.5t143.5 76.5 t238.5 28.5q35 0 105 -4q-2 66 -35 99.5t-129 33.5q-162 0 -285 -45l29 178q168 53 274 54q340 0 340 -308q0 -49 -15.5 -151t-15.5 -160q0 -123 82 -123q70 0 139.5 99.5t69.5 279.5q0 162 -88 288t-213 186.5t-258 60.5q-287 0 -509 -218t-222 -513q0 -125 44 -226.5 t107.5 -158t142.5 -94.5t138.5 -51t102.5 -13q258 0 469 104l-23 -149q-184 -86 -444 -86q-106 0 -217 38t-216.5 113.5t-172 213t-66.5 315.5zM766 313q0 -71 139 -71q111 0 154 46t53 158q-27 2 -80 3q-266 0 -266 -136z" />
-<glyph unicode="A" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM612 588h277l-137 424z" />
-<glyph unicode="B" horiz-adv-x="1349" d="M178 0v1366h463q541 0 541 -383q0 -66 -21.5 -116t-59.5 -79.5t-64.5 -43t-61.5 -25.5q274 -78 274 -342q0 -377 -567 -377h-504zM530 289h201h7h4q67 0 114 35q51 38 51 114t-57 111t-125 35h-195v-295zM530 831h181q49 0 89 31t40 84q0 70 -46 101.5t-104 31.5h-160 v-248z" />
-<glyph unicode="C" horiz-adv-x="1314" d="M125 684q0 207 57.5 351.5t155.5 216t201.5 101t226.5 29.5q197 0 438 -96v-305q-205 98 -418 98q-141 0 -225 -86t-84 -309t84 -309t225 -86q213 0 418 98v-305q-242 -96 -438 -96q-123 0 -226.5 29.5t-201.5 101t-155.5 216t-57.5 351.5z" />
-<glyph unicode="D" horiz-adv-x="1443" d="M178 0v1366h492q317 0 483 -174t166 -510q0 -334 -166 -508t-483 -174h-492zM530 289h140q297 0 297 393q0 397 -297 397h-140v-790z" />
-<glyph unicode="E" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952z" />
-<glyph unicode="F" horiz-adv-x="1187" d="M178 0v1366h928v-287h-576v-276h498v-289h-498v-514h-352z" />
-<glyph unicode="G" horiz-adv-x="1427" d="M125 682q0 207 57.5 351.5t155.5 216t201.5 101t226.5 29.5q244 0 471 -102v-283q-207 84 -418 84q-344 0 -344 -395q0 -223 90 -309t242 -86q119 0 188 30v226h-243v262h524v-690q-223 -131 -510 -131q-96 0 -180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293z" />
-<glyph unicode="H" horiz-adv-x="1503" d="M180 0v1366h352v-539h439v539h352v-1366h-352v539h-439v-539h-352z" />
-<glyph unicode="I" horiz-adv-x="708" d="M180 0v1366h352v-1366h-352z" />
-<glyph unicode="J" horiz-adv-x="1060" d="M59 92v326q141 -129 293 -129q72 0 117 41t45 125v911h352v-889q0 -123 -32.5 -215t-79.5 -143t-114 -83t-122 -41t-115 -9q-176 0 -344 106z" />
-<glyph unicode="K" horiz-adv-x="1505" d="M178 0v1366h352v-481l375 481h475l-536 -594l575 -772h-444l-348 532l-97 -108v-424h-352z" />
-<glyph unicode="L" horiz-adv-x="1179" d="M178 0v1366h352v-1049h607v-317h-959z" />
-<glyph unicode="M" horiz-adv-x="1695" d="M180 0v1366h346l322 -465l321 465h347v-1366h-353v846l-315 -453l-316 453v-846h-352z" />
-<glyph unicode="N" horiz-adv-x="1619" d="M178 0v1366h352l555 -819v819h353v-1366h-348l-560 813v-813h-352z" />
-<glyph unicode="O" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM477 684q0 -223 79 -309t210 -86 t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309z" />
-<glyph unicode="P" horiz-adv-x="1337" d="M176 0v1366h553q248 0 385 -126t137 -353q0 -229 -137 -355t-385 -126h-201v-406h-352zM528 696h168q106 0 155.5 56.5t49.5 134.5q0 76 -49 134t-156 58h-168v-383z" />
-<glyph unicode="Q" horiz-adv-x="1529" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293q0 -604 -494 -684q8 -162 213 -162v-227q-500 0 -505 387q-496 80 -496 686zM477 684q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86 t-79 -309z" />
-<glyph unicode="R" horiz-adv-x="1396" d="M178 0v1366h539q250 0 400.5 -116.5t150.5 -327.5q0 -162 -75 -266.5t-200 -145.5l330 -510h-393l-254 457h-146v-457h-352zM530 721h209q72 0 124 50t52 128q0 80 -52 130t-124 50h-209v-358z" />
-<glyph unicode="S" horiz-adv-x="1232" d="M127 954q0 211 145.5 317.5t360.5 106.5q195 0 368 -94v-307q-166 98 -317 98q-80 0 -142.5 -26.5t-62.5 -87.5q0 -39 46 -65t117 -39t151.5 -43t151.5 -73t117 -133t46 -219q0 -180 -143.5 -292.5t-348.5 -112.5q-281 0 -473 133v329q189 -157 419 -157h3 q78 0 134.5 29.5t56.5 84.5q0 37 -35 63t-91 41l-122 31q-66 16 -133.5 45t-122.5 71t-90 119t-35 181z" />
-<glyph unicode="T" horiz-adv-x="1191" d="M25 1047v319h1142v-319h-397v-1047h-352v1047h-393z" />
-<glyph unicode="U" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5z" />
-<glyph unicode="V" horiz-adv-x="1413" d="M25 1366h376l306 -967l305 967h377l-521 -1366h-319z" />
-<glyph unicode="W" horiz-adv-x="2045" d="M27 1366h368l234 -862l243 862h304l243 -862l234 862h368l-440 -1366h-321l-236 782l-236 -782h-321z" />
-<glyph unicode="X" horiz-adv-x="1443" d="M25 0l485 692l-475 674h430l258 -369l260 369h403l-462 -653l503 -713h-430l-288 410l-289 -410h-395z" />
-<glyph unicode="Y" horiz-adv-x="1492" d="M25 1366h428l290 -520l291 520h428l-542 -791v-575h-353v575z" />
-<glyph unicode="Z" horiz-adv-x="1337" d="M133 0v291l617 762h-611v313h1047v-285l-598 -772h598v-309h-1053z" />
-<glyph unicode="[" horiz-adv-x="849" d="M244 -238v1663h436v-247h-209v-1168h209v-248h-436z" />
-<glyph unicode="\" horiz-adv-x="880" d="M-76 1548h256l752 -1749h-254z" />
-<glyph unicode="]" horiz-adv-x="849" d="M172 10h209v1168h-209v247h436v-1663h-436v248z" />
-<glyph unicode="^" horiz-adv-x="1210" d="M246 958l282 455h148l289 -455h-230l-131 244l-131 -244h-227z" />
-<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-209h-1118v209z" />
-<glyph unicode="`" horiz-adv-x="571" d="M92 1393h240l127 -240h-176z" />
-<glyph unicode="a" horiz-adv-x="1142" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86 q-49 4 -68 4q-135 0 -135 -103z" />
-<glyph unicode="b" horiz-adv-x="1230" d="M160 348v1065h350v-383q29 4 133 4q223 0 344 -133t121 -383q0 -534 -502 -534h-2q-137 0 -229 30.5t-136 87t-61.5 114.5t-17.5 132zM510 287q0 -29 26.5 -51.5t81.5 -22.5q72 0 105 90t33 227q0 133 -45 209t-133 76q-18 0 -68 -4v-524z" />
-<glyph unicode="c" horiz-adv-x="993" d="M119 510q0 254 126 390t361 136q178 0 322 -69v-262q-139 63 -258 63q-90 0 -144.5 -64.5t-54.5 -193.5t54.5 -193.5t144.5 -64.5q119 0 258 63v-262q-141 -69 -317 -69h-5q-236 0 -361.5 136t-125.5 390z" />
-<glyph unicode="d" horiz-adv-x="1222" d="M129 518q0 250 121 383t344 133q104 0 133 -4v383h350v-1065q0 -74 -17.5 -132t-61.5 -114.5t-136 -87t-229 -30.5q-504 0 -504 534zM481 530q0 -137 32.5 -227t104.5 -90q55 0 82 22.5t27 51.5v524q-49 4 -68 4q-88 0 -133 -75.5t-45 -209.5z" />
-<glyph unicode="e" horiz-adv-x="1169" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM485 641h230q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z " />
-<glyph unicode="f" horiz-adv-x="776" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141z" />
-<glyph unicode="g" horiz-adv-x="1153" d="M127 516q0 520 491 520q117 0 202 -30.5t128 -84t62.5 -112.5t19.5 -131v-637q0 -51 -10 -102.5t-42 -115t-82 -110.5t-139 -79.5t-204 -32.5q-156 0 -322 53v250q115 -66 252 -66q102 0 145.5 48t49.5 130q-53 -4 -98 -4q-211 0 -332 131t-121 373zM479 510 q0 -127 40 -209t110 -82q24 0 51 6v490q0 80 -80 80q-57 0 -89 -81t-32 -204z" />
-<glyph unicode="h" horiz-adv-x="1316" d="M207 0v1411h352v-387q74 12 174 12q213 0 333 -117.5t120 -316.5v-602h-352v657q0 113 -181 113q-55 0 -94 -14v-756h-352z" />
-<glyph unicode="i" horiz-adv-x="735" d="M193 1139v268h360v-268h-360zM197 0v1022h352v-1022h-352z" />
-<glyph unicode="j" horiz-adv-x="667" d="M6 -141q168 0 168 125v1038h352v-1008q0 -119 -40 -201.5t-114.5 -126.5t-162.5 -62.5t-203 -18.5v254zM170 1139v268h360v-268h-360z" />
-<glyph unicode="k" horiz-adv-x="1318" d="M178 0v1405h352v-631l215 248h451l-416 -451l461 -571h-426l-250 330l-35 -37v-293h-352z" />
-<glyph unicode="l" horiz-adv-x="761" d="M207 0v1411h352v-1411h-352z" />
-<glyph unicode="m" horiz-adv-x="1939" d="M203 0v930l14 6l42 16q26 10 60.5 20.5t78.5 22t92.5 20.5t105.5 15t113 6q147 0 249 -59q175 59 355 59q215 0 337 -110.5t122 -323.5v-602h-353v616q0 158 -141 158q-80 0 -141 -20q26 -77 26 -147v-5v-602h-352v621q0 149 -131 149h-2q-55 0 -123 -18v-752h-352z" />
-<glyph unicode="n" horiz-adv-x="1325" d="M178 0v913q246 123 508 123q217 0 344 -115.5t127 -318.5v-602h-352v657q0 31 -8.5 51.5t-48 41t-115.5 20.5q-55 0 -103 -12v-758h-352z" />
-<glyph unicode="o" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
-<glyph unicode="p" horiz-adv-x="1232" d="M170 -379v1051q0 74 17.5 132t61.5 114.5t136 87t229 30.5q504 0 504 -534q0 -250 -121 -383t-344 -133q-104 0 -133 4v-369h-350zM520 209q49 -4 68 -4q88 0 133 75.5t45 208.5q0 137 -32.5 227.5t-104.5 90.5q-55 0 -82 -22.5t-27 -51.5v-524z" />
-<glyph unicode="q" horiz-adv-x="1228" d="M115 502q0 534 501 534h2q137 0 229.5 -30.5t136.5 -87t61.5 -115t17.5 -131.5v-1051h-350v369q-29 -4 -133 -4q-223 0 -344 133t-121 383zM467 489q0 -133 45 -208.5t133 -75.5q18 0 68 4v524q0 29 -27 51.5t-82 22.5q-72 0 -104.5 -90t-32.5 -228z" />
-<glyph unicode="r" horiz-adv-x="813" d="M164 0v922q264 109 584 116v-295q-127 -2 -232 -18v-725h-352z" />
-<glyph unicode="s" horiz-adv-x="962" d="M90 694q0 166 115 254t293 88q156 0 293 -84v-233q-133 74 -236 74q-111 0 -111 -70q0 -20 45.5 -40.5t110 -46.5t128 -61.5t108.5 -105.5t45 -164q0 -141 -113 -230t-293 -89q-190 0 -346 100v272q160 -125 309 -125q92 0 92 56q0 27 -45 48t-109.5 45t-130 57.5 t-110.5 99t-45 155.5z" />
-<glyph unicode="t" horiz-adv-x="839" d="M49 786v236h139v303h353v-303h213v-236h-213v-495q0 -23 18.5 -39t44.5 -16q63 0 150 51v-252q-88 -49 -205 -49q-178 0 -269.5 94t-91.5 264v442h-139z" />
-<glyph unicode="u" horiz-adv-x="1280" d="M172 461v561h352v-623q0 -150 122 -150l1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475z" />
-<glyph unicode="v" horiz-adv-x="1224" d="M66 1022h374l170 -629l172 629h377l-391 -1022h-311z" />
-<glyph unicode="w" horiz-adv-x="1763" d="M82 1022h373l133 -616l162 616h262l162 -616l133 616h372l-362 -1022h-309l-127 459l-127 -459h-310z" />
-<glyph unicode="x" horiz-adv-x="1249" d="M53 0l363 518l-353 504h414l148 -207l145 207h389l-350 -479l387 -543h-416l-172 252l-166 -252h-389z" />
-<glyph unicode="y" horiz-adv-x="1169" d="M63 1022h365l156 -588l153 588h363l-373 -1002q-90 -231 -177 -327t-212 -96q-133 0 -223 43v256q70 -25 135 -25q47 0 92 56.5t78 170.5z" />
-<glyph unicode="z" horiz-adv-x="1030" d="M94 0v289l424 461h-414v272h805v-285l-381 -444h381v-293h-815z" />
-<glyph unicode="{" horiz-adv-x="804" d="M186 485v238q4 0 11.5 2t25 10t30.5 19.5t24.5 33t11.5 48.5v270q0 145 81 235.5t242 90.5h62v-236h-45q-45 0 -68.5 -22.5t-27 -45t-3.5 -67.5v-238q0 -94 -56 -147t-138 -72q82 -18 138 -71.5t56 -147.5v-238q0 -45 3.5 -67.5t27 -45t68.5 -22.5h45v-235h-62 q-162 0 -242.5 90t-80.5 235v271q0 41 -25.5 69.5t-50.5 36.5z" />
-<glyph unicode="|" horiz-adv-x="1005" d="M379 -203v1569h270v-1569h-270z" />
-<glyph unicode="}" horiz-adv-x="804" d="M156 12h45q45 0 68.5 22.5t26.5 45t3 67.5v238q0 94 56.5 147.5t138.5 71.5q-82 18 -138.5 71.5t-56.5 147.5v238q0 45 -3 67.5t-26.5 45t-68.5 22.5h-45v236h61q162 0 243 -90.5t81 -235.5v-270q0 -41 25.5 -70t51.5 -37l25 -6v-238q-4 0 -11 -2t-24.5 -10t-31 -19.5 t-24.5 -33t-11 -47.5v-271q0 -145 -81 -235t-243 -90h-61v235z" />
-<glyph unicode="~" horiz-adv-x="1071" d="M201 457v186q57 68 129 74q11 1 23 1q59 0 116 -26l135 -60q57 -25 115 -25q12 0 23 1q71 6 128 74v-186q-57 -68 -128 -74q-11 -1 -23 -1q-58 0 -115 25l-135 60q-56 26 -115 26q-12 0 -24 -2q-72 -6 -129 -73z" />
-<glyph unicode="&#xa1;" horiz-adv-x="882" d="M270 -344l45 975h265l45 -975h-355zM289 760v262h317v-262h-317z" />
-<glyph unicode="&#xa2;" horiz-adv-x="1007" d="M117 682q0 254 126 390t361 136h29v158h131v-174q90 -16 162 -53v-262q-90 43 -162 55v-500q70 14 162 55v-262q-74 -35 -162 -53v-172h-131v156h-29q-236 0 -361.5 136t-125.5 390zM469 682q0 -115 45 -178.5t119 -75.5v510q-164 -29 -164 -256z" />
-<glyph unicode="&#xa3;" horiz-adv-x="1153" d="M100 -31v307q80 55 158 74v203h-158v268h158v168q0 162 105.5 277.5t328.5 115.5q162 0 346 -106v-281q-141 119 -299 119q-90 0 -144 -35t-54 -104v-154h340v-268h-340v-211q117 -53 227 -53q141 0 270 86v-297q-82 -76 -181 -90q-35 -6 -70 -6q-62 0 -123 17l-187 51 q-60 17 -122 17q-36 0 -73 -5q-100 -15 -182 -93z" />
-<glyph unicode="&#xa5;" horiz-adv-x="1492" d="M37 1366h428l291 -520l291 520h428l-408 -594h137v-201h-272v-155h272v-201h-272v-215h-352v215h-273v201h273v155h-273v201h137z" />
-<glyph unicode="&#xa7;" horiz-adv-x="1247" d="M127 707q0 98 91 145t192 49q-43 4 -95.5 60.5t-52.5 121.5q0 86 39 148.5t106.5 94.5t141.5 45t160 13q180 0 362 -104v-248q-160 92 -311 92q-170 0 -170 -80q0 -31 84 -63.5t184 -62t184 -100.5t84 -171q0 -61 -48 -100t-94 -50.5t-93 -13.5q49 -27 74.5 -87 t25.5 -122q0 -137 -119.5 -213.5t-328.5 -76.5q-213 0 -410 131v276q171 -153 361 -153h2q66 0 120 17t54 58q0 27 -56.5 49.5t-136.5 46t-158.5 56.5t-135 95.5t-56.5 146.5zM453 715q0 -18 58 -43t210 -76q78 14 78 45q0 18 -49.5 38.5t-123 43t-89.5 29.5 q-84 -10 -84 -37z" />
-<glyph unicode="&#xa8;" horiz-adv-x="899" d="M141 1139v223h246v-223h-246zM489 1139v223h246v-223h-246z" />
-<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -205 -493.5t-495 -204.5q-289 0 -494 204.5t-205 493.5zM227 682q0 -236 167 -402.5t402.5 -166.5t403.5 166.5t168 402.5q0 238 -167 404.5t-404 166.5q-236 0 -403 -166.5t-167 -404.5z M430 684q0 121 34 206t91 126t117.5 58.5t132.5 17.5q117 0 258 -56v-178q-123 57 -246 57q-180 0 -180 -231t180 -231q123 0 246 57v-178q-139 -57 -258 -58q-72 0 -132.5 17.5t-117.5 59.5t-91 127t-34 206z" />
-<glyph unicode="&#xaa;" horiz-adv-x="833" d="M154 970q0 24 8 48t31.5 55t79 50.5t137.5 19.5h51q0 86 -78 86q-80 0 -174 -35v147q102 41 188 41q125 0 195 -64.5t70 -182.5v-136q0 -106 -57.5 -155t-205.5 -49q-245 0 -245 175zM350 1006q0 -61 58 -62q27 0 42 22.5t15 45.5v49q-8 0 -20.5 1t-18.5 1q-76 0 -76 -57 z" />
-<glyph unicode="&#xab;" horiz-adv-x="1347" d="M70 522l346 389h338l-373 -389l373 -391h-338zM565 522l346 389h338l-372 -389l372 -391h-338z" />
-<glyph unicode="&#xac;" horiz-adv-x="1300" d="M199 465v219h903v-389h-211v170h-692z" />
-<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
-<glyph unicode="&#xae;" horiz-adv-x="1593" d="M100 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -494 204.5t-205 493.5zM229 682q0 -236 167 -402.5t402.5 -166.5t403.5 166.5t168 402.5q0 238 -167 404.5t-404 166.5q-236 0 -403 -166.5t-167 -404.5 zM498 285v796h315q145 0 233.5 -67.5t88.5 -190.5q0 -94 -44 -155t-118 -84l192 -299h-229l-148 266h-83v-266h-207zM705 707h120q41 0 72 28.5t31 73.5q0 47 -31 76.5t-72 29.5h-120v-208z" />
-<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1139v180h479v-180h-479z" />
-<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1143q0 111 57.5 169t166 58t165.5 -58.5t57 -168.5q0 -109 -57 -167t-165.5 -58t-166 58t-57.5 167zM293 1145q0 -57 23.5 -83t74.5 -26t73.5 25.5t22.5 83t-22.5 82t-73.5 24.5q-98 0 -98 -106z" />
-<glyph unicode="&#xb1;" horiz-adv-x="1198" d="M236 12v215h743v-215h-743zM236 608v230h258v258h229v-258h256v-230h-256v-256h-229v256h-258z" />
-<glyph unicode="&#xb2;" horiz-adv-x="696" d="M102 948v150q311 231 312 346q0 74 -97 74q-98 0 -211 -91v193q121 74 230 74q137 0 201.5 -69t64.5 -167q0 -154 -211 -340h215v-170h-504z" />
-<glyph unicode="&#xb3;" horiz-adv-x="686" d="M100 999v183q121 -74 215 -74q49 0 79 21.5t30 54.5q0 82 -111 82h-120v135h122q76 0 76 61q0 27 -29.5 41.5t-66.5 14.5q-98 0 -180 -39v159q100 53 200 54q106 0 186.5 -47.5t80.5 -155.5q0 -51 -36 -93t-79 -50q66 -18 105.5 -70.5t39.5 -122.5q0 -119 -86 -167 t-202 -48q-96 0 -224 61z" />
-<glyph unicode="&#xb4;" horiz-adv-x="559" d="M121 1139l127 239h239l-190 -239h-176z" />
-<glyph unicode="&#xb5;" horiz-adv-x="1325" d="M203 -430v1452h352v-657q0 -31 8 -51.5t48 -41t116 -20.5q55 0 102 12v758h353v-913q-246 -123 -508 -123q-57 0 -119 10v-426h-352z" />
-<glyph unicode="&#xb6;" horiz-adv-x="1083" d="M80 870q0 240 143.5 368t413.5 128h354v-1366h-274v371h-80q-270 0 -413.5 129t-143.5 370z" />
-<glyph unicode="&#xb7;" horiz-adv-x="741" d="M215 420v282h295v-282h-295z" />
-<glyph unicode="&#xb8;" horiz-adv-x="585" d="M106 -250l191 240h176l-127 -240h-240z" />
-<glyph unicode="&#xb9;" horiz-adv-x="512" d="M92 1401v186l154 96h155v-735h-190v525z" />
-<glyph unicode="&#xba;" horiz-adv-x="868" d="M152 1090q0 143 71.5 218.5t204.5 75.5q131 0 202.5 -75.5t71.5 -218.5q0 -141 -71.5 -217t-202.5 -76q-133 0 -204.5 75.5t-71.5 217.5zM348 1090q0 -150 80 -150q78 0 78 150q0 151 -77 151h-1q-80 0 -80 -151z" />
-<glyph unicode="&#xbb;" horiz-adv-x="1374" d="M106 131l373 391l-373 389h338l347 -389l-347 -391h-338zM602 131l373 391l-373 389h338l346 -389l-346 -391h-338z" />
-<glyph unicode="&#xbc;" horiz-adv-x="1476" d="M92 1098v186l154 96h155v-735h-190v524zM170 -45l895 1448h192l-895 -1448h-192zM840 186v146l321 403h166v-387h88v-162h-88v-186h-188v186h-299zM1040 348h99v129z" />
-<glyph unicode="&#xbd;" horiz-adv-x="1550" d="M92 1098v186l154 96h155v-735h-190v524zM170 -45l895 1448h192l-895 -1448h-192zM946 0v150q311 231 311 346q0 73 -93 73h-3q-98 0 -211 -90v193q119 73 227 73h3q137 0 201.5 -68.5t64.5 -166.5q0 -154 -211 -340h215v-170h-504z" />
-<glyph unicode="&#xbe;" horiz-adv-x="1757" d="M137 696v183q121 -74 215 -74q49 0 79 21.5t30 54.5q0 82 -111 82h-121v135h123q76 0 76 61q0 27 -29.5 41t-66.5 14q-98 0 -180 -38v159q100 53 200 54q106 0 186 -47.5t80 -155.5q0 -51 -35.5 -93.5t-78.5 -50.5q66 -18 105.5 -70t39.5 -122q0 -119 -86 -167t-203 -48 q-96 0 -223 61zM387 -45l895 1448h193l-895 -1448h-193zM1067 186v146l322 403h165v-387h88v-162h-88v-186h-188v186h-299zM1268 348h98v129z" />
-<glyph unicode="&#xbf;" horiz-adv-x="1075" d="M164 37q0 70 22.5 126t56 92t72.5 74l73 73q34 34 56.5 87.5t22.5 118.5v21h281v-27q0 -104 -31 -184t-75 -127t-89 -86t-76 -81t-31 -89q0 -45 15.5 -76t46.5 -43t58.5 -16t66.5 -4q180 0 307 102v-262q-59 -41 -155.5 -68.5t-196.5 -27.5q-115 0 -200 33.5t-132 92 t-69.5 126t-22.5 145.5zM449 760v262h317v-262h-317z" />
-<glyph unicode="&#xc0;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM477 1722h240l127 -239h-176zM612 588h277l-137 424z" />
-<glyph unicode="&#xc1;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM612 588h277l-137 424zM659 1483l127 239h240l-190 -239h-177z" />
-<glyph unicode="&#xc2;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM457 1483l245 250h99l246 -250h-209l-86 90l-86 -90h-209zM612 588h277l-137 424z" />
-<glyph unicode="&#xc3;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM440 1497v174q57 68 124 74q11 1 22 1q55 0 106 -26l121 -60q51 -25 105 -25q11 0 22 1q66 6 123 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-121 60q-51 26 -106 26q-11 0 -22 -1 q-66 -6 -124 -74zM612 588h277l-137 424z" />
-<glyph unicode="&#xc4;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM455 1483v223h245v-223h-245zM612 588h277l-137 424zM803 1483v223h246v-223h-246z" />
-<glyph unicode="&#xc5;" horiz-adv-x="1503" d="M43 0l543 1366h332l542 -1366h-379l-106 328h-447l-106 -328h-379zM573 1618q0 84 47.5 130t131.5 46q86 0 132 -46t46 -130t-46 -130t-132 -46q-84 0 -131.5 46t-47.5 130zM612 588h277l-137 424zM700 1618q0 -37 11.5 -50.5t40 -13.5t40 13.5t11.5 50.5q0 63 -51.5 63 t-51.5 -63z" />
-<glyph unicode="&#xc6;" horiz-adv-x="2217" d="M51 0l961 1366h1065v-287h-600v-252h520v-288h-520v-250h600v-289h-953v246h-469l-168 -246h-436zM831 504h293v428z" />
-<glyph unicode="&#xc7;" horiz-adv-x="1314" d="M125 684q0 207 57.5 351.5t155.5 216t201.5 101t226.5 29.5q197 0 438 -96v-305q-205 98 -418 98q-141 0 -225 -86t-84 -309t84 -309t225 -86q213 0 418 98v-305q-195 -78 -356 -92l-125 -240h-240l189 240q-109 10 -200 48t-173 112.5t-128 211t-46 322.5z" />
-<glyph unicode="&#xc8;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM424 1722h240l127 -239h-177z" />
-<glyph unicode="&#xc9;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM561 1483l127 239h240l-191 -239h-176z" />
-<glyph unicode="&#xca;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM375 1483l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
-<glyph unicode="&#xcb;" horiz-adv-x="1247" d="M178 0v1366h952v-287h-600v-252h521v-288h-521v-250h600v-289h-952zM383 1483v223h246v-223h-246zM731 1483v223h246v-223h-246z" />
-<glyph unicode="&#xcc;" horiz-adv-x="708" d="M76 1722h239l127 -239h-176zM180 0h352v1366h-352v-1366z" />
-<glyph unicode="&#xcd;" horiz-adv-x="708" d="M180 0h352v1366h-352v-1366zM270 1483l127 239h240l-191 -239h-176z" />
-<glyph unicode="&#xce;" horiz-adv-x="708" d="M61 1483l246 250h99l245 -250h-209l-86 90l-86 -90h-209zM180 0h352v1366h-352v-1366z" />
-<glyph unicode="&#xcf;" horiz-adv-x="708" d="M59 1483v223h246v-223h-246zM180 0h352v1366h-352v-1366zM408 1483v223h245v-223h-245z" />
-<glyph unicode="&#xd0;" horiz-adv-x="1527" d="M106 582v200h166v584h492q317 0 483 -174t166 -510q0 -334 -166 -508t-483 -174h-492v582h-166zM625 289h139q297 0 297 393q0 397 -297 397h-139v-297h217v-200h-217v-293z" />
-<glyph unicode="&#xd1;" horiz-adv-x="1619" d="M178 0v1366h352l555 -819v819h353v-1366h-348l-560 813v-813h-352zM510 1497v174q57 68 124 74q10 1 21 1q55 0 105 -26l122 -60q52 -25 106 -25q11 0 22 1q65 6 123 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -106 25l-122 60q-50 26 -105 26q-11 0 -21 -1 q-67 -6 -124 -74z" />
-<glyph unicode="&#xd2;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM477 684q0 -223 79 -309t210 -86 t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309zM500 1722h239l127 -239h-176z" />
-<glyph unicode="&#xd3;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM477 684q0 -223 79 -309t210 -86 t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309zM666 1483l127 239h239l-190 -239h-176z" />
-<glyph unicode="&#xd4;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM471 1483l246 250h98l246 -250h-209 l-86 90l-86 -90h-209zM477 684q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309z" />
-<glyph unicode="&#xd5;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM459 1497v174q57 68 123 74q11 1 22 1 q55 0 106 -26l122 -60q51 -25 105 -25q11 0 21 1q66 6 123 74v-174q-57 -68 -123 -74q-10 -1 -21 -1q-54 0 -105 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -123 -74zM477 684q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309z" />
-<glyph unicode="&#xd6;" horiz-adv-x="1538" d="M125 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16t180 -16t171 -63.5t149.5 -122t101.5 -201.5t39 -293t-39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16t-180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM469 1483v223h246v-223h-246zM477 684 q0 -223 79 -309t210 -86t210 86t79 309t-79 309t-210 86t-210 -86t-79 -309zM817 1483v223h246v-223h-246z" />
-<glyph unicode="&#xd7;" horiz-adv-x="1093" d="M147 154l287 370l-284 371h225l174 -223l176 223h223l-284 -371l284 -370h-223l-176 223l-174 -223h-228z" />
-<glyph unicode="&#xd8;" horiz-adv-x="1529" d="M14 0l211 246q-100 168 -100 436q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16q215 0 360 -84l62 72h328l-211 -246q102 -168 102 -438q0 -166 -39 -293t-101.5 -201.5t-149.5 -122t-171 -63.5t-180 -16q-217 0 -363 86l-61 -72h-328zM477 682q0 -82 8 -133 l426 495q-57 33 -145 33q-131 0 -210 -86t-79 -309zM618 319q55 -32 143 -32h5q131 0 210 86t79 309q0 70 -11 135z" />
-<glyph unicode="&#xd9;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM506 1722h239l127 -239h-176z" />
-<glyph unicode="&#xda;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM651 1483l127 239h240l-191 -239h-176z" />
-<glyph unicode="&#xdb;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM467 1483l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
-<glyph unicode="&#xdc;" horiz-adv-x="1533" d="M178 588v778h352v-819q0 -131 56.5 -194.5t175.5 -63.5t175 63.5t56 194.5v819h353v-778q0 -287 -154 -444.5t-430.5 -157.5t-430 157.5t-153.5 444.5zM465 1483v223h246v-223h-246zM813 1483v223h246v-223h-246z" />
-<glyph unicode="&#xdd;" horiz-adv-x="1492" d="M25 1366h428l290 -520l291 520h428l-542 -791v-575h-353v575zM635 1483l127 239h239l-190 -239h-176z" />
-<glyph unicode="&#xde;" horiz-adv-x="1337" d="M176 0v1366h352v-203h177q248 0 385 -126t137 -353q0 -229 -137.5 -355t-384.5 -126h-177v-203h-352zM528 494h144q106 0 155.5 56t49.5 134q0 76 -49.5 134.5t-155.5 58.5h-144v-383z" />
-<glyph unicode="&#xdf;" horiz-adv-x="1282" d="M180 0v846q0 160 41 271.5t110.5 166.5t143.5 77.5t162 22.5q236 0 357.5 -109.5t121.5 -266.5q0 -106 -60 -187.5t-130 -101.5q250 -74 250 -313q0 -221 -167 -318q-149 -87 -391 -87q-30 0 -61 1v256h12q124 0 217 38q97 40 97 134q0 51 -32 84t-91.5 47t-100.5 19.5 t-102 7.5v233q43 0 84 6.5t88 21.5t74.5 48t27.5 82q0 162 -167 162q-190 0 -191 -267v-874h-293z" />
-<glyph unicode="&#xe0;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM326 1378h239l127 -239h-176zM459 362 q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
-<glyph unicode="&#xe1;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM442 1139l127 239h240l-191 -239h-176zM459 362 q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
-<glyph unicode="&#xe2;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM268 1139l246 250h98l246 -250h-209l-86 90l-86 -90 h-209zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
-<glyph unicode="&#xe3;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM252 1153v174q57 68 124 74q10 1 21 1q55 0 105 -26 l122 -60q52 -25 106 -25q11 0 22 1q65 6 122 74v-174q-57 -68 -122 -74q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103z" />
-<glyph unicode="&#xe4;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM266 1139v223h246v-223h-246zM459 362 q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103zM614 1139v223h246v-223h-246z" />
-<glyph unicode="&#xe5;" horiz-adv-x="1128" d="M106 301q0 31 7.5 63.5t35 78.5t72.5 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -311 -64v267q178 71 336 71q225 0 348 -114.5t123 -329.5v-242q0 -190 -102.5 -277t-366.5 -87q-438 0 -439 315zM385 1272q0 84 47 130t131 46q86 0 132 -46t46 -130 t-46 -130t-132 -46q-84 0 -131 46t-47 130zM459 362q0 -108 98 -108h2q49 0 76 39t27 82v86q-49 4 -68 4q-135 0 -135 -103zM512 1272q0 -37 11.5 -50.5t40 -13.5t39.5 13.5t11 50.5q0 63 -51 63t-51 -63z" />
-<glyph unicode="&#xe6;" horiz-adv-x="1792" d="M127 297q0 31 7 63.5t35 78.5t73 81t134 59.5t208 24.5q63 0 92 -2q0 84 -41 120t-96 36q-154 0 -312 -64v267q176 71 332 71h4q92 0 175 -44t124 -91q35 51 113 94t170 43q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248 q-166 -106 -389 -106q-96 0 -175 48t-112 109q-89 -157 -319 -157h-3q-438 0 -438 315zM479 358q0 -108 99 -108h2q49 0 75.5 39t26.5 82v86q-49 4 -68 4q-135 0 -135 -103zM1030 637h230q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
-<glyph unicode="&#xe7;" horiz-adv-x="993" d="M119 510q0 254 126 390t361 136q178 0 322 -69v-262q-139 63 -258 63q-90 0 -144.5 -64.5t-54.5 -193.5t54.5 -193.5t144.5 -64.5q119 0 258 63v-262q-106 -51 -238 -63l-125 -240h-239l188 240q-193 25 -294 158t-101 362z" />
-<glyph unicode="&#xe8;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM338 1393h240l127 -240h-177zM485 641h230 q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
-<glyph unicode="&#xe9;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM475 1139l127 239h240l-191 -239h-176zM485 641h230 q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
-<glyph unicode="&#xea;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM301 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209z M485 641h230q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121z" />
-<glyph unicode="&#xeb;" d="M131 502q0 244 113.5 390t355.5 146q229 0 347 -144t118 -353q0 -61 -6 -111h-582q0 -72 67.5 -121t151.5 -49q188 0 322 80v-248q-166 -106 -402 -106q-68 0 -129 11t-128.5 46t-115.5 89t-80 149.5t-32 220.5zM299 1139v223h246v-223h-246zM485 641h230 q-4 59 -35 116.5t-80 57.5t-80 -53t-35 -121zM647 1139v223h246v-223h-246z" />
-<glyph unicode="&#xec;" horiz-adv-x="735" d="M92 1393h240l127 -240h-176zM197 0h352v1022h-352v-1022z" />
-<glyph unicode="&#xed;" horiz-adv-x="735" d="M197 0h352v1022h-352v-1022zM287 1139l127 239h239l-190 -239h-176z" />
-<glyph unicode="&#xee;" horiz-adv-x="735" d="M78 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209zM197 0h352v1022h-352v-1022z" />
-<glyph unicode="&#xef;" horiz-adv-x="735" d="M76 1139v223h246v-223h-246zM197 0h352v1022h-352v-1022zM424 1139v223h246v-223h-246z" />
-<glyph unicode="&#xf0;" horiz-adv-x="1271" d="M117 481q0 131 41 220.5t115.5 129t148.5 55t170 15.5q123 0 190 -27q-14 109 -123 164l-151 -118l-92 114l61 49h-16q-90 0 -199 -30v301q91 30 219 30h8q162 0 291 -59l107 84l92 -117l-68 -53q215 -193 215 -606q0 -649 -505 -649q-131 0 -232.5 45t-159 118.5 t-85 158.5t-27.5 175zM471 483q0 -31 7 -68.5t44.5 -82.5t98.5 -45q66 0 113 95q42 87 42 247v28q-53 23 -151 23q-78 0 -116 -59.5t-38 -137.5z" />
-<glyph unicode="&#xf1;" horiz-adv-x="1308" d="M178 0v913q246 123 508 123q217 0 344 -115.5t127 -318.5v-602h-352v657q0 31 -8.5 51.5t-48 41t-115.5 20.5q-55 0 -103 -12v-758h-352zM342 1153v174q57 68 124 74q10 1 21 1q55 0 105 -26l122 -60q52 -25 106 -25q11 0 22 1q65 6 123 74v-174q-57 -68 -123 -74 q-11 -1 -22 -1q-54 0 -106 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74z" />
-<glyph unicode="&#xf2;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM332 1378h239l127 -239h-176zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
-<glyph unicode="&#xf3;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270zM469 1139l127 239h240l-191 -239h-176z" />
-<glyph unicode="&#xf4;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM289 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
-<glyph unicode="&#xf5;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM276 1153v174q57 68 124 74q11 1 22 1q55 0 106 -26l122 -60q51 -25 105 -25q11 0 21 1q66 6 123 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60 q-51 26 -106 26q-11 0 -22 -1q-66 -6 -124 -74zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270z" />
-<glyph unicode="&#xf6;" d="M92 510q0 254 128 389t363.5 135t363.5 -135t128 -389t-128 -389t-363.5 -135t-363.5 135t-128 389zM287 1139v223h245v-223h-245zM444 510q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270zM635 1139v223h246v-223h-246z" />
-<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M188 418v231h916v-231h-916zM520 47v240h252v-240h-252zM520 782v240h252v-240h-252z" />
-<glyph unicode="&#xf8;" horiz-adv-x="1255" d="M23 0l178 209q-68 119 -68 303q0 254 128 389t364 135q141 0 243 -49l31 35h330l-178 -207q65 -119 65 -298v-5q0 -254 -128 -389t-363 -135q-139 0 -244 49l-31 -37h-327zM487 541l193 227q-28 14 -55 14q-131 0 -138 -241zM569 256q23 -14 56 -14q131 0 139 239z" />
-<glyph unicode="&#xf9;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM391 1378h240l127 -239h-176z" />
-<glyph unicode="&#xfa;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM537 1139l127 239h239l-190 -239h-176z" />
-<glyph unicode="&#xfb;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM352 1139l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
-<glyph unicode="&#xfc;" horiz-adv-x="1269" d="M172 461v561h352v-623q0 -150 122 -150q1 0 1 1q123 0 123 149v623h352v-561q0 -475 -475 -475t-475 475zM350 1139v223h246v-223h-246zM698 1139v223h246v-223h-246z" />
-<glyph unicode="&#xfd;" horiz-adv-x="1169" d="M63 1022h373l148 -588l145 588h371l-373 -1002q-90 -231 -177 -327t-212 -96q-133 0 -223 43v256q70 -25 135 -25q47 0 92 56.5t78 170.5zM485 1139l127 239h240l-190 -239h-177z" />
-<glyph unicode="&#xfe;" horiz-adv-x="1218" d="M172 -375v1778h350v-379q43 6 133 6q223 0 344 -133t121 -383t-120.5 -383t-344.5 -133q-90 0 -133 6v-379h-350zM522 221q50 -4 68 -4q178 12 178 297t-178 297q-18 0 -68 -4v-586z" />
-<glyph unicode="&#xff;" horiz-adv-x="1169" d="M63 1022h373l148 -588l145 588h371l-373 -1002q-90 -231 -177 -327t-212 -96q-133 0 -223 43v256q70 -25 135 -25q47 0 92 56.5t78 170.5zM287 1139v223h245v-223h-245zM635 1139v223h246v-223h-246z" />
-<glyph unicode="&#x152;" horiz-adv-x="2160" d="M123 684q0 166 39 293t101.5 201.5t149.5 122t171 63.5t180 16q168 0 293 -51v37h952v-287h-600v-252h520v-288h-520v-250h600v-289h-952v39q-125 -51 -293 -51q-96 0 -180 16t-171 63.5t-149.5 122t-101.5 201.5t-39 293zM475 684q0 -223 79 -309t210 -86t210 86t79 309 t-79 309t-210 86t-210 -86t-79 -309z" />
-<glyph unicode="&#x153;" horiz-adv-x="1853" d="M125 512q0 254 128 389t363 135q190 0 308 -86q117 88 297 88q229 0 347 -144t118 -353q0 -61 -7 -111h-581q0 -72 67.5 -121t151.5 -49q188 0 321 80v-248q-166 -106 -401 -106q-193 0 -311 88q-119 -86 -310 -86q-236 0 -363.5 135t-127.5 389zM477 512 q0 -270 139.5 -270t139.5 270t-139.5 270t-139.5 -270zM1106 641h229q-4 59 -34.5 116.5t-80 57.5t-80 -53t-34.5 -121z" />
-<glyph unicode="&#x178;" horiz-adv-x="1492" d="M25 1366h428l290 -520l291 520h428l-542 -791v-575h-353v575zM449 1483v223h245v-223h-245zM797 1483v223h245v-223h-245z" />
-<glyph unicode="&#x2c6;" horiz-adv-x="874" d="M137 1155l246 250h98l246 -250h-209l-86 90l-86 -90h-209z" />
-<glyph unicode="&#x2dc;" horiz-adv-x="1058" d="M215 1153v174q57 68 124 74q11 1 22 1q55 0 105 -26l122 -60q51 -25 105 -25q11 0 21 1q66 6 124 74v-174q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74z" />
-<glyph unicode="&#x2000;" horiz-adv-x="897" />
-<glyph unicode="&#x2001;" horiz-adv-x="1794" />
-<glyph unicode="&#x2002;" horiz-adv-x="897" />
-<glyph unicode="&#x2003;" horiz-adv-x="1794" />
-<glyph unicode="&#x2004;" horiz-adv-x="598" />
-<glyph unicode="&#x2005;" horiz-adv-x="448" />
-<glyph unicode="&#x2006;" horiz-adv-x="299" />
-<glyph unicode="&#x2007;" horiz-adv-x="299" />
-<glyph unicode="&#x2008;" horiz-adv-x="224" />
-<glyph unicode="&#x2009;" horiz-adv-x="358" />
-<glyph unicode="&#x200a;" horiz-adv-x="99" />
-<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
-<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
-<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 514v240h624v-240h-624z" />
-<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 475v240h860v-240h-860z" />
-<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 475v240h1501v-240h-1501z" />
-<glyph unicode="&#x2018;" horiz-adv-x="616" d="M96 1384h285l151 -401h-239z" />
-<glyph unicode="&#x2019;" horiz-adv-x="616" d="M96 983l152 401h284l-196 -401h-240z" />
-<glyph unicode="&#x201a;" horiz-adv-x="698" d="M147 -229l152 401h285l-197 -401h-240z" />
-<glyph unicode="&#x201c;" horiz-adv-x="980" d="M98 1384h285l152 -401h-240zM455 1384h284l152 -401h-240z" />
-<glyph unicode="&#x201d;" horiz-adv-x="980" d="M98 983l152 401h285l-197 -401h-240zM455 983l151 401h285l-197 -401h-239z" />
-<glyph unicode="&#x201e;" horiz-adv-x="1056" d="M147 -229l152 401h285l-197 -401h-240zM504 -229l151 401h285l-197 -401h-239z" />
-<glyph unicode="&#x2022;" horiz-adv-x="1085" d="M172 665.5q0 153.5 109.5 262t263 108.5t261 -107.5t107.5 -263t-107.5 -263t-261 -107.5t-263 108.5t-109.5 262z" />
-<glyph unicode="&#x2026;" horiz-adv-x="1849" d="M219 0v283h295v-283h-295zM780 0v283h295v-283h-295zM1343 0v283h295v-283h-295z" />
-<glyph unicode="&#x202f;" horiz-adv-x="358" />
-<glyph unicode="&#x2039;" horiz-adv-x="819" d="M63 522l347 389h338l-373 -389l373 -391h-338z" />
-<glyph unicode="&#x203a;" horiz-adv-x="843" d="M80 131l373 391l-373 389h338l346 -389l-346 -391h-338z" />
-<glyph unicode="&#x205f;" horiz-adv-x="448" />
-<glyph unicode="&#x20ac;" horiz-adv-x="1466" d="M98 408v190h166q-2 27 -2 86t2 88h-166v191h197q117 419 606 419h2q199 0 438 -96v-305q-204 98 -417 98q-174 0 -248 -116h397v-191h-455q-4 -53 -4 -88q0 -33 4 -86h455v-190h-397q74 -119 248 -119q213 0 417 98v-305q-240 -96 -438 -96q-492 0 -608 422h-197z" />
-<glyph unicode="&#x2122;" horiz-adv-x="1581" d="M100 1206v160h572v-160h-199v-522h-176v522h-197zM772 684v682h174l160 -233l160 233h174v-682h-176v422l-158 -227l-158 227v-422h-176z" />
-<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
-<glyph unicode="&#xfb01;" horiz-adv-x="1462" d="M84 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-206v-774h-353v774h-141zM981 1139h360v268h-360v-268zM985 0h352v1022h-352v-1022z" />
-<glyph unicode="&#xfb02;" horiz-adv-x="1462" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141zM983 0h352v1411h-352v-1411z" />
-<glyph unicode="&#xfb03;" horiz-adv-x="2287" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141zM858 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141z M1745 1139v268h360v-268h-360zM1749 0v1022h352v-1022h-352z" />
-<glyph unicode="&#xfb04;" horiz-adv-x="2313" d="M82 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141zM858 774v248h145q1 424 404 424q84 0 151 -23v-211q-29 8 -55 9q-72 0 -109.5 -63.5t-37.5 -135.5h202v-248h-207v-774h-352v774h-141z M1759 0v1411h352v-1411h-352z" />
-</font>
-</defs></svg> 
\ No newline at end of file
diff --git a/static/fnt/mavenpro-black-webfont.ttf b/static/fnt/mavenpro-black-webfont.ttf
deleted file mode 100644 (file)
index 38d7d3f..0000000
Binary files a/static/fnt/mavenpro-black-webfont.ttf and /dev/null differ
diff --git a/static/fnt/mavenpro-black-webfont.woff b/static/fnt/mavenpro-black-webfont.woff
deleted file mode 100644 (file)
index 6ddfe8c..0000000
Binary files a/static/fnt/mavenpro-black-webfont.woff and /dev/null differ
diff --git a/static/fnt/mavenpro-bold-webfont.eot b/static/fnt/mavenpro-bold-webfont.eot
deleted file mode 100644 (file)
index 030d7cb..0000000
Binary files a/static/fnt/mavenpro-bold-webfont.eot and /dev/null differ
diff --git a/static/fnt/mavenpro-bold-webfont.svg b/static/fnt/mavenpro-bold-webfont.svg
deleted file mode 100644 (file)
index 43eb15b..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
-<svg xmlns="http://www.w3.org/2000/svg">
-<metadata></metadata>
-<defs>
-<font id="maven_probold" horiz-adv-x="1441" >
-<font-face units-per-em="2048" ascent="1638" descent="-410" />
-<missing-glyph horiz-adv-x="692" />
-<glyph horiz-adv-x="2048" />
-<glyph horiz-adv-x="2048" />
-<glyph unicode="&#xd;" horiz-adv-x="682" />
-<glyph unicode=" "  horiz-adv-x="692" />
-<glyph unicode="&#x09;" horiz-adv-x="692" />
-<glyph unicode="&#xa0;" horiz-adv-x="692" />
-<glyph unicode="!" horiz-adv-x="825" d="M283 1366h280l-45 -973h-190zM303 0v227h240v-227h-240z" />
-<glyph unicode="&#x22;" horiz-adv-x="825" d="M166 1384h205l-35 -411h-133zM461 1384h205l-35 -411h-133z" />
-<glyph unicode="#" horiz-adv-x="1216" d="M152 256v152h180l51 292h-174v152h201l36 217h162l-39 -217h232l39 217h159l-38 -217h133v-152h-158l-51 -292h153v-152h-178l-39 -217h-162l39 217h-231l-37 -217h-162l39 217h-155zM494 408h231l49 292h-229z" />
-<glyph unicode="$" horiz-adv-x="1181" d="M150 987q0 174 111.5 273.5t289.5 119.5v209h119v-207q168 -6 334 -92v-239q-160 102 -334 104v-340q74 -23 120 -39t109.5 -53t100 -81t63.5 -116t27 -164q0 -154 -121 -257t-299 -117v-209h-119v209q-219 14 -383 114v250q174 -129 383 -131v379q-76 23 -117 37 t-104.5 47t-96 70t-58 97t-25.5 136zM395 993q0 -53 39 -84.5t117 -58.5v293q-156 -33 -156 -150zM670 229q174 27 174 150q0 121 -174 184v-334z" />
-<glyph unicode="%" horiz-adv-x="1777" d="M145 983q0 377 289 377q291 0 291 -377t-291 -377q-289 0 -289 377zM305 983q0 -133 36 -179t93.5 -46t93 46t35.5 179q0 135 -35.5 180t-93 45t-93.5 -45t-36 -180zM381 0l858 1366h174l-858 -1366h-174zM1065 383q0 377 289 377q291 0 291 -377t-291 -377 q-289 0 -289 377zM1225 383q0 -133 35.5 -179t93 -46t93.5 46t36 179q0 135 -36 180t-93.5 45t-93 -45t-35.5 -180z" />
-<glyph unicode="&#x26;" horiz-adv-x="1310" d="M113 389q0 53 7 94t31.5 96.5t79 108.5t138.5 103q-125 131 -125 249q0 137 85 235.5t240 98.5q139 0 232.5 -92t93.5 -227q0 -104 -50 -174t-173 -152l229 -274q29 72 43 155h201q-29 -180 -109 -319l244 -291h-262l-105 129q-152 -145 -364 -145q-113 0 -201 41 t-137 103t-73.5 130t-24.5 131zM307 383q0 -39 17.5 -83t77 -87t151.5 -43q135 0 242 106l-301 361q-66 -35 -108 -72t-57.5 -72.5t-18.5 -56t-3 -53.5zM438 1059q0 -14 5.5 -31.5t10.5 -29t18 -32t19.5 -27.5t25 -29.5t22.5 -26.5q96 57 129.5 97t33.5 87q0 53 -37.5 90 t-93 37t-94.5 -39t-39 -96z" />
-<glyph unicode="'" horiz-adv-x="534" d="M166 1384h205l-35 -411h-133z" />
-<glyph unicode="(" horiz-adv-x="751" d="M190 627q0 440 211 856h213q-211 -416 -211 -856q0 -436 211 -858h-213q-211 422 -211 858z" />
-<glyph unicode=")" horiz-adv-x="751" d="M133 -231q211 412 211 858q0 449 -211 856h213q211 -416 211 -856q0 -436 -211 -858h-213z" />
-<glyph unicode="*" horiz-adv-x="845" d="M172 1133l166 57l-166 57l78 139l123 -104l-25 156h160l-25 -156l123 104l78 -139l-166 -57l166 -57l-78 -140l-123 105l25 -156h-160l25 156l-123 -105z" />
-<glyph unicode="+" horiz-adv-x="1128" d="M199 520v182h272v275h182v-275h273v-182h-273v-270h-182v270h-272z" />
-<glyph unicode="," horiz-adv-x="677" d="M152 -229l151 401h215l-182 -401h-184z" />
-<glyph unicode="-" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
-<glyph unicode="." horiz-adv-x="759" d="M260 0v227h240v-227h-240z" />
-<glyph unicode="/" horiz-adv-x="897" d="M-29 -201l752 1749h205l-754 -1749h-203z" />
-<glyph unicode="0" horiz-adv-x="1345" d="M133 677.5q0 700.5 538.5 700.5t538.5 -700.5t-538.5 -700.5t-538.5 700.5zM379 678q0 -156 24.5 -260.5t71.5 -152.5t92 -64.5t104.5 -16.5t104.5 16.5t92 64.5t72 152.5t25 260.5q0 125 -17.5 218t-45.5 144.5t-68.5 82t-77.5 39.5t-84 9t-84 -9t-78 -39.5t-68.5 -82 t-45 -144.5t-17.5 -218z" />
-<glyph unicode="1" horiz-adv-x="993" d="M211 936v262l266 168h223v-1366h-245v1085z" />
-<glyph unicode="2" horiz-adv-x="1226" d="M158 0v201q94 70 192.5 154.5t205 193t174 222.5t67.5 204q0 70 -56.5 117t-185.5 47q-100 0 -205.5 -34t-179.5 -85v244q195 120 392 120h3q242 0 358.5 -120.5t116.5 -288.5q0 -182 -137 -373.5t-336 -357.5h494v-244h-903z" />
-<glyph unicode="3" horiz-adv-x="1228" d="M166 84v250q180 -106 373 -107q290 0 290 188q0 91 -68.5 149.5t-230.5 58.5h-174v202h181q115 0 171 47.5t56 116.5q0 82 -73 116t-169 34q-178 0 -327 -90v251q178 84 342 84q205 0 339 -92t134 -295q0 -168 -168 -260q104 -37 167.5 -121t63.5 -204q0 -96 -32.5 -172 t-85 -123t-123 -78t-138 -43t-139.5 -12q-203 0 -389 100z" />
-<glyph unicode="4" horiz-adv-x="1234" d="M78 385v203l622 778h234v-762h178v-219h-178v-385h-246v385h-610zM365 604h323v420z" />
-<glyph unicode="5" horiz-adv-x="1245" d="M170 709l74 657h751v-244h-542q-4 -51 -12.5 -126.5t-12.5 -123.5q96 18 184 18q2 1 4 1q86 0 158 -19q74 -20 146.5 -67t113.5 -141.5t41 -227.5q0 -135 -41 -229t-110.5 -140.5t-143 -65.5t-162.5 -19q-227 0 -415 96v248q190 -121 401 -121q227 0 227 211v20 q-10 219 -223 223h-9q-174 0 -292 -73z" />
-<glyph unicode="6" horiz-adv-x="1222" d="M131 629q0 195 49 340t117 222t162 123t162.5 58t140.5 12q100 0 235 -38v-218q-82 27 -221 27q-47 0 -98 -12.5t-113.5 -42t-112 -96t-65.5 -160.5q90 69 250 69q221 0 342 -120.5t121 -331.5q0 -86 -26.5 -167t-81 -152.5t-152 -114.5t-224.5 -43q-485 0 -485 645z M378 585q0 -103 20 -179q22 -91 64 -132t78.5 -57t75.5 -16q76 0 128.5 31.5t73 80.5t27.5 84t7 66q0 104 -61.5 166.5t-174.5 62.5q-133 0 -237 -69q-1 -19 -1 -38z" />
-<glyph unicode="7" horiz-adv-x="1202" d="M141 1120v246h965v-195l-563 -1171h-291l545 1120h-656z" />
-<glyph unicode="8" horiz-adv-x="1236" d="M129 408q0 74 25.5 134t61.5 94t73 57.5t63 31.5l25 8q-8 2 -21.5 7t-48.5 26.5t-61.5 49.5t-48 81t-21.5 119q0 172 119 270t325.5 98t325.5 -98t119 -270q0 -188 -158 -266l-41 -17q10 -2 26.5 -8t58.5 -30.5t75 -58.5t59.5 -94.5t26.5 -133.5q0 -199 -129 -311.5 t-362.5 -112.5t-362.5 112.5t-129 311.5zM375 416q0 -111 61.5 -175.5t184 -64.5t184 63.5t61.5 176.5q0 82 -63.5 141t-182 59t-182 -59t-63.5 -141zM422 1010q0 -98 59.5 -140.5t139 -42.5t139 42t59.5 141q0 74 -51 124t-147.5 50t-147.5 -50.5t-51 -123.5z" />
-<glyph unicode="9" horiz-adv-x="1234" d="M117 907q0 86 26.5 167t81 152.5t151.5 114.5t224 43q485 0 485 -645q0 -195 -49 -340t-116.5 -222t-161.5 -123t-163 -58t-140 -12q-100 0 -236 39v217q82 -27 221 -27q47 0 98.5 12.5t114 42t111.5 96t65 160.5q-89 -69 -249 -69q-221 0 -342 120.5t-121 331.5z M365 905q0 -104 61 -166.5t174 -62.5q133 0 238 69q1 19 1 38q0 103 -19 179q-23 91 -65 132t-79 57t-76 16q-76 0 -128 -31.5t-72.5 -80.5t-27.5 -84t-7 -66z" />
-<glyph unicode=":" horiz-adv-x="813" d="M293 78v223h235v-223h-235zM293 723v223h235v-223h-235z" />
-<glyph unicode=";" horiz-adv-x="858" d="M182 -229l152 401h215l-182 -401h-185zM322 723v223h235v-223h-235z" />
-<glyph unicode="&#x3c;" horiz-adv-x="1351" d="M246 477v148l850 379v-211l-617 -242l617 -242v-211z" />
-<glyph unicode="=" horiz-adv-x="1306" d="M260 348v176h789v-176h-789zM260 698v176h789v-176h-789z" />
-<glyph unicode="&#x3e;" horiz-adv-x="1329" d="M270 98v211l617 242l-617 242v211l850 -379v-148z" />
-<glyph unicode="?" horiz-adv-x="1042" d="M164 1059v227q150 96 321 96q115 0 200 -33.5t132 -92t69.5 -126t22.5 -145.5q0 -70 -22.5 -126t-56 -92t-72.5 -74l-73 -73q-34 -34 -56.5 -87.5t-22.5 -118.5v-21h-217v27q0 104 31 184t76 127t89 86t74.5 81t30.5 89q0 182 -221 182q-180 0 -305 -110zM379 0v227h239 v-227h-239z" />
-<glyph unicode="@" horiz-adv-x="1869" d="M170 371q0 184 79 349t205 276.5t282.5 176t310.5 64.5q84 0 174 -22.5t183 -75t165.5 -127t119 -191t46.5 -258.5q0 -207 -92.5 -362.5t-266.5 -155.5q-82 0 -127 54.5t-51 97.5q-27 -66 -101.5 -109t-185.5 -43q-154 0 -242.5 69.5t-88.5 190.5q0 53 17 99.5t62.5 94.5 t142.5 76.5t238 28.5q45 0 115 -4q0 76 -34 114t-140 38q-166 0 -279 -43q4 29 13.5 82t13.5 81q158 51 270 52q166 0 252 -77t86 -226q0 -51 -16 -156t-16 -162q0 -133 92 -133q74 0 147.5 103.5t73.5 289.5q0 127 -53.5 234.5t-137.5 175t-185 105.5t-202 38 q-295 0 -522 -224t-227 -527q0 -129 46 -233.5t111.5 -163t145.5 -97.5t141.5 -52.5t106.5 -13.5q262 0 465 99l-21 -131q-188 -84 -442 -84q-106 0 -217 37.5t-216.5 113.5t-173 214.5t-67.5 316.5zM762 309q-1 -80 149 -80q119 0 165 50.5t54 175.5q-27 2 -90 2 q-279 0 -278 -148z" />
-<glyph unicode="A" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM516 551h408l-203 536z" />
-<glyph unicode="B" horiz-adv-x="1323" d="M178 0v1366h492q94 0 174 -19.5t149.5 -61.5t109.5 -120t40 -184q0 -180 -162 -264q238 -86 238 -316q0 -55 -9.5 -103t-42 -105.5t-85 -97.5t-145.5 -67.5t-218 -27.5h-541zM424 215h283q266 0 266 199q0 178 -275 178h-274v-377zM424 809h260q213 0 213 164 q0 180 -231 180h-242v-344z" />
-<glyph unicode="C" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q209 0 414 -81v-230q-195 90 -392 90q-92 0 -157.5 -16.5t-128 -63.5t-95 -147t-32.5 -252t32.5 -252t95 -147t128 -63.5t157.5 -16.5q197 0 392 90v-229q-205 -82 -414 -82q-96 0 -179 16t-170 63.5 t-148.5 123t-100.5 202.5t-39 295z" />
-<glyph unicode="D" horiz-adv-x="1419" d="M178 0v1366h471q317 0 481 -174t164 -510q0 -334 -163.5 -508t-481.5 -174h-471zM424 223h225q213 0 306.5 107.5t93.5 351.5q0 246 -93.5 353.5t-306.5 107.5h-225v-920z" />
-<glyph unicode="E" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909z" />
-<glyph unicode="F" horiz-adv-x="1142" d="M178 0v1366h883v-221h-637v-346h551v-232h-551v-567h-246z" />
-<glyph unicode="G" horiz-adv-x="1406" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q217 0 450 -92v-225q-221 98 -426 98q-72 0 -127 -9t-111 -39t-93 -81t-60.5 -140t-23.5 -210q0 -123 23.5 -212t60.5 -140t93 -81t111.5 -39t126.5 -9q113 0 242 35v327h-321v217h548v-682 q-238 -116 -489 -116h-4q-96 0 -179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294z" />
-<glyph unicode="H" horiz-adv-x="1429" d="M178 0v1366h246v-567h580v567h245v-1366h-245v567h-580v-567h-246z" />
-<glyph unicode="I" horiz-adv-x="600" d="M178 0v1366h246v-1366h-246z" />
-<glyph unicode="J" horiz-adv-x="1003" d="M59 72v243q125 -102 281 -102q115 0 169 48t54 188v917h246v-909q0 -145 -44 -245.5t-117 -147t-139.5 -63.5t-139.5 -17q-168 0 -310 88z" />
-<glyph unicode="K" horiz-adv-x="1415" d="M178 0v1366h246v-578l516 578h330l-551 -600l608 -766h-307l-459 592l-137 -150v-442h-246z" />
-<glyph unicode="L" horiz-adv-x="1124" d="M178 0v1366h246v-1133h657v-233h-903z" />
-<glyph unicode="M" horiz-adv-x="1695" d="M178 0v1366h273l395 -571l395 571h272v-1366h-245v1010l-422 -604l-422 604v-1010h-246z" />
-<glyph unicode="N" horiz-adv-x="1587" d="M178 0v1366h279l702 -1012v1012h246v-1366h-275l-706 1020v-1020h-246z" />
-<glyph unicode="O" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212z" />
-<glyph unicode="P" horiz-adv-x="1277" d="M176 0v1366h510q242 0 374 -120t132 -339t-132 -338.5t-374 -119.5h-264v-449h-246zM422 672h246q162 0 221 59.5t59 175.5q0 115 -59.5 173.5t-220.5 58.5h-246v-467z" />
-<glyph unicode="Q" horiz-adv-x="1529" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t171 63.5t180 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -152 -32 -269.5t-84 -193.5t-126 -127t-148.5 -74.5t-160.5 -31.5q0 -94 69.5 -136.5t165.5 -42.5v-188q-438 0 -438 369q-106 14 -193 53 t-165 115.5t-122 211t-44 314.5zM371 682q0 -154 30.5 -254t91 -148.5t122 -64.5t149.5 -16q86 0 148.5 16t123 64.5t91 148.5t30.5 254t-30.5 254t-91 148.5t-123 64.5t-148.5 16q-88 0 -149.5 -16t-122 -64.5t-91 -148.5t-30.5 -254z" />
-<glyph unicode="R" horiz-adv-x="1337" d="M178 0v1366h524q240 0 373 -113.5t133 -324.5q0 -314 -299 -406l359 -522h-297l-326 481h-221v-481h-246zM424 707h272q125 0 196 45t71 170q0 127 -71 171t-196 44h-272v-430z" />
-<glyph unicode="S" horiz-adv-x="1189" d="M125 985q0 197 139 297t348 100q184 0 367 -94v-239q-158 104 -338 104q-113 0 -191.5 -39t-78.5 -123q0 -68 71.5 -106.5t173 -65t204 -66.5t174 -138.5t71.5 -254.5q0 -166 -137 -271t-334 -105q-260 0 -451 116v250q176 -131 394 -131q282 0 282 157q0 71 -51 118 t-129 69.5t-167 53t-167 66.5t-129 112.5t-51 189.5z" />
-<glyph unicode="T" horiz-adv-x="1169" d="M25 1120v246h1120v-246h-438v-1120h-246v1120h-436z" />
-<glyph unicode="U" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5z" />
-<glyph unicode="V" horiz-adv-x="1347" d="M25 1366h262l387 -1061l387 1061h262l-522 -1366h-252z" />
-<glyph unicode="W" horiz-adv-x="2045" d="M25 1366h258l307 -1010l325 1010h215l326 -1010l309 1010h256l-438 -1366h-254l-305 952l-307 -952h-254z" />
-<glyph unicode="X" horiz-adv-x="1329" d="M25 0l485 690l-473 676h299l326 -461l323 461h289l-469 -666l491 -700h-299l-344 487l-344 -487h-284z" />
-<glyph unicode="Y" horiz-adv-x="1368" d="M25 1366h297l362 -555l360 555h297l-534 -780v-586h-246v586z" />
-<glyph unicode="Z" horiz-adv-x="1271" d="M131 0v225l684 895h-676v246h981v-219l-688 -901h688v-246h-989z" />
-<glyph unicode="[" horiz-adv-x="808" d="M244 -238v1663h395v-190h-207v-1282h207v-191h-395z" />
-<glyph unicode="\" horiz-adv-x="829" d="M-76 1548h205l752 -1749h-203z" />
-<glyph unicode="]" horiz-adv-x="808" d="M172 -47h207v1282h-207v190h395v-1663h-395v191z" />
-<glyph unicode="^" horiz-adv-x="1146" d="M264 969l223 444h113l225 -444h-170l-110 260l-111 -260h-170z" />
-<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-141h-1118v141z" />
-<glyph unicode="`" horiz-adv-x="522" d="M92 1395h201l127 -240h-137z" />
-<glyph unicode="a" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM358 365q0 -164 193 -164h2 q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
-<glyph unicode="b" horiz-adv-x="1181" d="M170 328v1085h246v-391q88 16 168 16q229 0 352 -133t123 -383q0 -137 -36 -241.5t-85 -158.5t-117.5 -88t-119 -42t-101.5 -8q-219 0 -324.5 101t-105.5 243zM416 305q0 -110 184 -110q117 0 164 79.5t47 260.5q0 172 -60.5 240.5t-175.5 68.5q-90 0 -159 -17v-522z" />
-<glyph unicode="c" horiz-adv-x="966" d="M119 510q0 256 125 392t358 136q150 0 295 -57v-205q-121 49 -254 49q-141 0 -209.5 -64.5t-68.5 -250.5q0 -184 68.5 -249.5t209.5 -65.5q127 0 254 51v-205q-145 -57 -295 -57q-233 0 -358 136t-125 390z" />
-<glyph unicode="d" horiz-adv-x="1167" d="M129 522q0 250 122 383t351 133q82 0 170 -16v391h246v-1085q0 -141 -106.5 -242.5t-325.5 -101.5q-51 0 -101.5 8t-118 42t-117.5 88t-85 158.5t-35 241.5zM375 535q0 -180 47 -260t164 -80q186 0 186 110v522q-72 16 -162 17q-115 0 -175 -68.5t-60 -240.5z" />
-<glyph unicode="e" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM381 588h412q-10 106 -62.5 179t-144.5 73 t-143.5 -59.5t-61.5 -192.5z" />
-<glyph unicode="f" horiz-adv-x="731" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174z" />
-<glyph unicode="g" horiz-adv-x="1122" d="M123 514q0 135 35 235.5t83 153.5t113.5 86t114.5 41t98 8q215 0 316.5 -98t101.5 -238v-727q0 -49 -9 -93t-38 -98t-77 -92t-135 -64.5t-202 -26.5q-162 0 -295 41v219q123 -41 267 -41q86 0 140 13t73.5 41t24.5 47.5t5 53.5v56q-82 -16 -155 -17q-223 0 -342 129 t-119 371zM369 510q0 -172 55 -233.5t168 -61.5q75 0 147 14v476q0 114 -169 114h-3q-98 0 -148 -71.5t-50 -237.5z" />
-<glyph unicode="h" horiz-adv-x="1228" d="M193 0v1411h245v-407q88 34 205 34q213 0 333 -111.5t120 -312.5v-614h-244v635q0 41 -7 68.5t-30.5 57.5t-77 45t-135.5 15q-96 0 -164 -30v-791h-245z" />
-<glyph unicode="i" horiz-adv-x="638" d="M193 1161v225h251v-225h-251zM197 0v1022h245v-1022h-245z" />
-<glyph unicode="j" horiz-adv-x="593" d="M6 -205q70 4 112 17.5t57 42t18.5 49t3.5 65.5v1053h245v-1036q0 -66 -7 -113t-33.5 -99.5t-72.5 -85t-128 -56t-195 -27.5v190zM193 1161v225h251v-225h-251z" />
-<glyph unicode="k" horiz-adv-x="1226" d="M188 0v1405h246v-758l344 375h326l-445 -455l492 -567h-311l-338 403l-68 -69v-334h-246z" />
-<glyph unicode="l" horiz-adv-x="659" d="M207 0v1411h246v-1411h-246z" />
-<glyph unicode="m" horiz-adv-x="1908" d="M193 0v924q227 114 446 114h4q166 0 277 -75q190 75 368 75q207 0 322.5 -112.5t115.5 -311.5v-614h-245v631q0 98 -49.5 148t-184.5 50q-106 0 -209 -24q43 -49 43 -191v-614h-243v637q0 94 -48.5 141t-181.5 47q-88 0 -170 -16v-809h-245z" />
-<glyph unicode="n" horiz-adv-x="1273" d="M168 0v926q225 112 469 112q213 0 333 -111.5t120 -312.5v-614h-244v635q0 41 -7.5 68.5t-30 57.5t-73.5 45t-131 15q-90 0 -190 -18v-803h-246z" />
-<glyph unicode="o" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5z" />
-<glyph unicode="p" horiz-adv-x="1175" d="M168 -379v1071q0 141 105.5 242.5t324.5 101.5q51 0 101.5 -8t119 -42t117.5 -88t85 -158.5t36 -241.5q0 -250 -123 -384t-352 -134q-86 0 -168 18v-377h-246zM414 182q85 -12 151 -12q119 0 181.5 69.5t62.5 245.5q0 180 -47 260t-164 80q-184 0 -184 -110v-533z" />
-<glyph unicode="q" horiz-adv-x="1171" d="M115 498q0 137 34.5 241.5t85 158.5t118 88t117.5 42t101 8q219 0 326 -101t107 -243v-1071h-246v377q-82 -18 -170 -18q-229 0 -351 134t-122 384zM360 485q0 -176 62.5 -245.5t181.5 -69.5q68 0 154 12v533q0 110 -184 110h-3q-117 0 -164 -80t-47 -260z" />
-<glyph unicode="r" horiz-adv-x="741" d="M166 0v930q25 12 69 28.5t177 47t264 32.5v-213q-154 -2 -264 -34v-791h-246z" />
-<glyph unicode="s" horiz-adv-x="923" d="M100 719q0 154 108.5 236.5t270.5 82.5q152 0 279 -73v-187q-104 49 -236 49q-176 0 -176 -86q0 -43 50 -67.5t122 -43t143.5 -50t122 -106.5t50.5 -191q0 -129 -104.5 -214t-276.5 -85q-176 0 -324 84v219q135 -90 289 -90q106 0 138 24.5t32 73.5q0 45 -50.5 72.5 t-122 48t-143 48.5t-122 92.5t-50.5 162.5z" />
-<glyph unicode="t" horiz-adv-x="794" d="M49 811v211h172v303h246v-303h242v-211h-242v-504q0 -66 27.5 -89t97.5 -23q66 0 117 22v-203q-72 -30 -157 -30h-5q-152 0 -239 90t-87 248v489h-172z" />
-<glyph unicode="u" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352z" />
-<glyph unicode="v" horiz-adv-x="1169" d="M66 1022h262l256 -772l256 772h264l-391 -1022h-256z" />
-<glyph unicode="w" horiz-adv-x="1710" d="M78 1022h260l217 -737l195 737h211l194 -737l217 737h260l-362 -1022h-246l-168 588l-170 -588h-246z" />
-<glyph unicode="x" horiz-adv-x="1142" d="M57 0l363 516l-354 506h301l204 -291l205 291h273l-342 -483l378 -539h-301l-229 326l-231 -326h-267z" />
-<glyph unicode="y" horiz-adv-x="1107" d="M63 1022h261l229 -713l225 713h260l-389 -1047q-82 -223 -178 -300.5t-199 -77.5q-76 0 -155 28v205q59 -25 116 -25q106 0 197 248z" />
-<glyph unicode="z" horiz-adv-x="999" d="M92 0v229l471 570h-461v223h777v-227l-478 -572h478v-223h-787z" />
-<glyph unicode="{" horiz-adv-x="784" d="M186 506v194q12 2 28.5 7.5t45.5 34t29 71.5v293q0 145 81 235.5t242 90.5h41v-185h-24q-43 0 -70 -11t-37 -34.5t-13 -41t-3 -48.5v-289q0 -86 -52.5 -142t-117.5 -79q66 -20 118 -76.5t52 -142.5v-289q0 -31 3 -48t13 -40.5t37 -35t70 -11.5h24v-184h-41 q-162 0 -242.5 90t-80.5 235v293q0 41 -25.5 70t-50.5 37z" />
-<glyph unicode="|" horiz-adv-x="954" d="M379 -203v1569h219v-1569h-219z" />
-<glyph unicode="}" horiz-adv-x="784" d="M156 -41h22q43 0 69.5 11.5t37 35t13.5 40.5t3 48v289q0 86 52.5 142.5t117.5 76.5q-66 23 -118 79t-52 142v289q0 31 -3 48.5t-13.5 41t-37 34.5t-69.5 11h-22v185h39q164 0 244.5 -89.5t80.5 -236.5v-293q0 -43 25.5 -70.5t52.5 -33.5l25 -9v-194q-4 0 -11.5 -2 t-25 -10.5t-30.5 -19.5t-24.5 -32.5t-11.5 -48.5v-293q0 -147 -81 -236t-244 -89h-39v184z" />
-<glyph unicode="~" horiz-adv-x="1071" d="M201 457v168q57 68 129 74q11 1 23 1q59 0 116 -26l135 -60q57 -25 115 -25q12 0 23 1q71 6 128 74v-168q-57 -68 -128 -74q-11 -1 -23 -1q-58 0 -115 25l-135 60q-56 26 -115 26q-12 0 -24 -2q-72 -6 -129 -73z" />
-<glyph unicode="&#xa1;" horiz-adv-x="825" d="M283 -344l45 973h190l45 -973h-280zM303 795v227h240v-227h-240z" />
-<glyph unicode="&#xa2;" horiz-adv-x="976" d="M117 682q0 246 115.5 382t334.5 146v156h119v-160q98 -10 209 -53v-205q-98 41 -209 47v-626q104 6 209 49v-205q-111 -43 -209 -53v-160h-119v156q-219 10 -334.5 145t-115.5 381zM362 682q0 -158 51.5 -228.5t153.5 -82.5v622q-102 -12 -153.5 -81.5t-51.5 -229.5z" />
-<glyph unicode="&#xa3;" horiz-adv-x="1153" d="M100 -31v209q51 39 174 68v319h-174v195h174v235q0 160 105.5 273.5t327.5 113.5q168 0 331 -94v-211q-149 113 -313 113q-117 0 -178.5 -56.5t-61.5 -152.5v-221h377v-195h-377v-325q6 -2 75 -20.5t85.5 -21.5t74.5 -14q49 -10 76 -10h8q27 2 74.5 4t85 15.5t74.5 35.5 v-215q-86 -44 -182 -44h-4q-98 1 -186 25.5t-178 46.5q-54 13 -112 13q-38 0 -78 -6q-101 -14 -198 -80z" />
-<glyph unicode="&#xa5;" horiz-adv-x="1396" d="M37 1366h297l362 -555l361 555h297l-410 -598h162v-184h-287v-160h287v-184h-287v-240h-246v240h-288v184h288v160h-288v184h161z" />
-<glyph unicode="&#xa7;" horiz-adv-x="1247" d="M137 709q0 98 79 152t175 59q-47 20 -78.5 69t-31.5 103q0 106 67.5 174.5t158.5 93t204 24.5q184 0 344 -92v-211q-164 78 -314 78q-74 0 -135 -21.5t-61 -72.5q0 -27 42 -50.5t105.5 -41t136 -46t136 -61t105.5 -90t42 -131.5q0 -86 -65.5 -137t-161.5 -62 q84 -51 84 -167q0 -133 -117 -214t-315 -81q-211 0 -394 120v230q164 -133 369 -133q82 0 141.5 22.5t59.5 69.5q0 35 -59.5 60.5t-143.5 50t-169 56.5t-144.5 95t-59.5 154zM401 719q0 -43 68 -75t254 -93q61 6 93 33.5t32 54.5q0 37 -55.5 66.5t-147.5 57.5t-106 34 q-45 -4 -91.5 -24.5t-46.5 -53.5z" />
-<glyph unicode="&#xa8;" horiz-adv-x="856" d="M162 1155v195h207v-195h-207zM489 1155v195h207v-195h-207z" />
-<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -205 -493.5t-495 -204.5q-289 0 -494 204.5t-205 493.5zM203 682q0 -246 174 -420t420 -174q248 0 422 174t174 420q0 248 -174.5 422t-421.5 174q-246 0 -420 -174t-174 -422zM440 682 q0 121 33 206t90.5 127t116.5 59.5t131 17.5q123 0 242 -48v-135q-117 53 -229 53q-1 1 -2 1q-112 0 -174 -57q-64 -56 -64 -224t63 -223.5t176 -55.5q119 0 230 54v-135q-119 -47 -242 -48q-72 0 -131 17.5t-116.5 58.5t-90.5 126t-33 206z" />
-<glyph unicode="&#xaa;" horiz-adv-x="808" d="M154 989q0 25 7 48.5t28.5 57t74.5 54t133 20.5q39 0 101 -8q0 106 -121 107q-98 0 -166 -29v111q78 32 174 32q121 0 186.5 -62.5t65.5 -178.5v-154q0 -80 -58.5 -136t-181.5 -56q-125 0 -184 57t-59 137zM289 1008q0 -92 106 -93q105 1 105 89v71q-74 6 -82 6 q-129 0 -129 -73z" />
-<glyph unicode="&#xab;" horiz-adv-x="1247" d="M66 522l346 389h286l-372 -389l372 -391h-286zM522 522l346 389h287l-373 -389l373 -391h-287z" />
-<glyph unicode="&#xac;" horiz-adv-x="1273" d="M199 492v192h876v-389h-176v197h-700z" />
-<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
-<glyph unicode="&#xae;" horiz-adv-x="1593" d="M100 682q0 291 205 495.5t494 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -494 204.5t-205 493.5zM205 682q0 -246 174 -420t420 -174q248 0 422 174t174 420q0 248 -174 422t-422 174q-246 0 -420 -174t-174 -422zM528 285v796 h306q139 0 216.5 -66.5t77.5 -189.5q0 -184 -174 -235l209 -305h-174l-188 280h-129v-280h-144zM672 696h157q72 0 114 26t42 99.5t-42 100t-114 26.5h-157v-252z" />
-<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1180v145h479v-145h-479z" />
-<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1143q0 111 57.5 169t166 58t165.5 -58.5t57 -168.5q0 -109 -57 -167t-165.5 -58t-166 58t-57.5 167zM276 1145q0 -68 28 -97.5t87.5 -29.5t86 29.5t26.5 97.5q0 66 -26.5 95.5t-86 29.5t-87.5 -30t-28 -95z" />
-<glyph unicode="&#xb1;" horiz-adv-x="1198" d="M242 61v166h727v-166h-727zM242 604v182h272v275h182v-275h273v-182h-273v-270h-182v270h-272z" />
-<glyph unicode="&#xb2;" horiz-adv-x="696" d="M109 948v109q125 92 234.5 209.5t109.5 208.5q0 88 -129 88q-119 0 -209 -66v133q106 63 213 64q131 0 193.5 -64.5t62.5 -154.5q0 -180 -254 -396h266v-131h-487z" />
-<glyph unicode="&#xb3;" horiz-adv-x="686" d="M111 993v135q96 -57 200 -57q158 0 158 100q0 113 -162 113h-94v109h98q61 0 91 25.5t30 62.5q0 80 -129 80q-100 0 -176 -48v136q98 45 184 45q109 0 181.5 -49.5t72.5 -159.5q0 -90 -90 -139q125 -43 125 -177q0 -121 -85 -175t-196 -54q-109 0 -208 53z" />
-<glyph unicode="&#xb4;" horiz-adv-x="528" d="M121 1155l127 240h201l-191 -240h-137z" />
-<glyph unicode="&#xb5;" horiz-adv-x="1323" d="M203 -430v1452h243v-635q0 -41 7.5 -68.5t30 -57.5t73.5 -45t131 -15q90 0 191 18v803h245v-926q-223 -112 -465 -112h-4q-127 0 -209 51v-465h-243z" />
-<glyph unicode="&#xb6;" horiz-adv-x="911" d="M78 946q0 203 121 311.5t350 108.5h299v-1366h-232v524h-67q-229 0 -350 108.5t-121 313.5z" />
-<glyph unicode="&#xb7;" horiz-adv-x="663" d="M211 444v228h240v-228h-240z" />
-<glyph unicode="&#xb8;" horiz-adv-x="563" d="M119 -256l178 246h160l-127 -246h-211z" />
-<glyph unicode="&#xb9;" horiz-adv-x="512" d="M109 1452v141l143 90h121v-735h-133v586z" />
-<glyph unicode="&#xba;" horiz-adv-x="854" d="M152 1092q0 141 69.5 216.5t200.5 75.5t200.5 -75.5t69.5 -216.5q0 -143 -69.5 -219t-200.5 -76t-200.5 75.5t-69.5 219.5zM289 1092q0 -106 32.5 -144.5t100.5 -38.5q66 0 99.5 38t33.5 145q0 104 -34 142t-99 38q-68 0 -100.5 -38t-32.5 -142z" />
-<glyph unicode="&#xbb;" horiz-adv-x="1277" d="M106 131l373 389l-373 391h287l346 -391l-346 -389h-287zM563 131l373 389l-373 391h287l346 -391l-346 -389h-287z" />
-<glyph unicode="&#xbc;" horiz-adv-x="1476" d="M109 1149v141l143 90h121v-735h-133v586zM168 -45l895 1448h164l-895 -1448h-164zM825 209v108l336 418h125v-409h96v-117h-96v-209h-131v209h-330zM981 326h174v227z" />
-<glyph unicode="&#xbd;" horiz-adv-x="1527" d="M109 1149v141l143 90h121v-735h-133v586zM168 -45l895 1448h164l-895 -1448h-164zM936 0v109q125 92 234.5 209.5t109.5 207.5q0 88 -129 88q-119 0 -209 -65v133q106 63 213 63q131 0 193.5 -64.5t62.5 -154.5q0 -180 -254 -395h266v-131h-487z" />
-<glyph unicode="&#xbe;" horiz-adv-x="1691" d="M135 688v135q96 -57 201 -57q158 0 158 100q0 113 -162 113h-94v108h98q61 0 91 26t30 63q0 79 -126 79h-3q-100 0 -176 -47v135q98 45 184 46q109 0 181.5 -49.5t72.5 -159.5q0 -91 -90 -140q125 -43 125 -176q0 -121 -85 -175t-196 -54q-109 0 -209 53zM369 -45 l895 1448h163l-895 -1448h-163zM1032 209v108l336 418h125v-409h96v-117h-96v-209h-131v209h-330zM1188 326h174v227z" />
-<glyph unicode="&#xbf;" horiz-adv-x="1042" d="M156 37q0 70 22.5 126t56 92t72.5 74l73 73q34 34 56.5 87.5t22.5 118.5v21h217v-27q0 -104 -31 -184t-76 -127t-89 -86t-74.5 -81t-30.5 -89q0 -182 221 -182q180 0 305 110v-227q-150 -96 -321 -96q-115 0 -200 33.5t-132 92t-69.5 126t-22.5 145.5zM446 795v227h240 v-227h-240z" />
-<glyph unicode="&#xc0;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM459 1722h200l127 -239h-137zM516 551h408l-203 536z" />
-<glyph unicode="&#xc1;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM516 551h408l-203 536zM655 1483l127 239h201l-190 -239h-138z" />
-<glyph unicode="&#xc2;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM485 1483l187 250h98l188 -250h-163l-74 114l-74 -114h-162zM516 551h408l-203 536z" />
-<glyph unicode="&#xc3;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM410 1481v155q57 68 124 74q10 1 21 1q55 0 105 -25l122 -61q51 -25 104 -25q12 0 24 2q65 6 122 73v-155q-57 -68 -122 -74q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -104 26q-11 0 -22 -2 q-67 -6 -124 -73zM516 551h408l-203 536z" />
-<glyph unicode="&#xc4;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM455 1483v194h207v-194h-207zM516 551h408l-203 536zM782 1483v194h207v-194h-207z" />
-<glyph unicode="&#xc5;" d="M43 0l543 1366h270l543 -1366h-264l-125 328h-578l-125 -328h-264zM516 551h408l-203 536zM543 1599.5q0 83.5 47 130t131 46.5q86 0 132 -46.5t46 -130t-46 -130t-132 -46.5q-84 0 -131 46.5t-47 130zM651 1599.5q0 -49.5 15.5 -69t54.5 -19.5t54.5 19.5t15.5 69 t-15.5 69t-54.5 19.5t-54.5 -19.5t-15.5 -69z" />
-<glyph unicode="&#xc6;" horiz-adv-x="2039" d="M51 0l864 1366h1002v-221h-664v-346h576v-232h-576v-346h664v-221h-909v307h-478l-188 -307h-291zM655 512h353v578z" />
-<glyph unicode="&#xc7;" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q211 0 412 -81v-230q-191 90 -390 90q-92 0 -157.5 -16.5t-128 -63.5t-95 -147t-32.5 -252t32.5 -252t95 -147t128 -63.5t157.5 -16.5q199 0 390 90v-229q-170 -72 -349 -80l-123 -242h-213l177 244 q-109 12 -198 50t-171 113.5t-127 212t-45 320.5z" />
-<glyph unicode="&#xc8;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM397 1722h201l127 -239h-137z" />
-<glyph unicode="&#xc9;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM555 1483l127 239h201l-191 -239h-137z" />
-<glyph unicode="&#xca;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM414 1483l186 250h98l189 -250h-164l-74 114l-74 -114h-161z" />
-<glyph unicode="&#xcb;" horiz-adv-x="1204" d="M178 0v1366h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909zM389 1483v194h207v-194h-207zM717 1483v194h207v-194h-207z" />
-<glyph unicode="&#xcc;" horiz-adv-x="600" d="M35 1722h201l126 -239h-137zM178 0h246v1366h-246v-1366z" />
-<glyph unicode="&#xcd;" horiz-adv-x="600" d="M178 0h246v1366h-246v-1366zM240 1483l127 239h200l-190 -239h-137z" />
-<glyph unicode="&#xce;" horiz-adv-x="600" d="M63 1483l187 250h98l189 -250h-164l-74 114l-74 -114h-162zM178 0h246v1366h-246v-1366z" />
-<glyph unicode="&#xcf;" horiz-adv-x="600" d="M33 1483v194h207v-194h-207zM178 0h246v1366h-246v-1366zM360 1483v194h207v-194h-207z" />
-<glyph unicode="&#xd0;" horiz-adv-x="1509" d="M106 598v168h162v600h471q317 0 481 -174t164 -510q0 -334 -163.5 -508t-481.5 -174h-471v598h-162zM514 223h225q213 0 306.5 107.5t93.5 351.5q0 246 -93.5 353.5t-306.5 107.5h-225v-377h242v-168h-242v-375z" />
-<glyph unicode="&#xd1;" horiz-adv-x="1587" d="M178 0v1366h279l702 -1012v1012h246v-1366h-275l-706 1020v-1020h-246zM481 1481v155q57 68 124 74q11 1 22 1q55 0 105 -25l122 -61q50 -25 104 -25q11 0 23 2q66 6 123 73v-155q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -104 26q-11 0 -22 -2 q-67 -6 -125 -73z" />
-<glyph unicode="&#xd2;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM518 1722h201l127 -239h-137z" />
-<glyph unicode="&#xd3;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM680 1483l127 239h201l-191 -239h-137z" />
-<glyph unicode="&#xd4;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM526 1483l187 250h98l188 -250h-163l-74 114l-74 -114h-162z" />
-<glyph unicode="&#xd5;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM451 1481v155q57 68 123 74q11 1 22 1q55 0 106 -25l122 -61q50 -25 103 -25 q12 0 23 2q66 6 123 73v-155q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2q-66 -6 -123 -73z" />
-<glyph unicode="&#xd6;" horiz-adv-x="1538" d="M125 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16t179 -16t170 -63.5t149.5 -123t101.5 -202.5t39 -295q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16t-179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM371 684q0 -154 30.5 -255 t90 -148.5t122 -64.5t148.5 -17q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 152 -30.5 253t-91 148.5t-122 63.5t-149.5 16q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM496 1483v194h206v-194h-206zM823 1483v194h207v-194h-207z" />
-<glyph unicode="&#xd7;" horiz-adv-x="1062" d="M156 160l282 364l-282 365h184l190 -246l193 246h182l-280 -365l280 -364h-182l-193 243l-190 -243h-184z" />
-<glyph unicode="&#xd8;" horiz-adv-x="1529" d="M68 0l178 211q-121 174 -121 471q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q233 0 385 -102l74 86h237l-180 -213q123 -178 123 -471q0 -166 -39 -294t-101.5 -202.5t-149.5 -122t-170 -63.5t-179 -16q-233 0 -387 100l-70 -84h-237zM371 684q0 -168 39 -278 l594 702q-84 57 -242 57q-68 0 -119 -9t-105.5 -39t-88 -81t-56 -140t-22.5 -212zM520 256q84 -57 242 -57q70 0 121 9t105 38.5t88 82t56.5 142.5t22.5 213q0 162 -41 274z" />
-<glyph unicode="&#xd9;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM506 1722h201l127 -239h-138z" />
-<glyph unicode="&#xda;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM668 1483l127 239h200l-190 -239h-137z" />
-<glyph unicode="&#xdb;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM514 1483l186 250h99l188 -250h-164l-73 114l-74 -114h-162z" />
-<glyph unicode="&#xdc;" horiz-adv-x="1511" d="M178 578v788h246v-809q0 -180 78 -264t248 -84t248.5 84t78.5 264v809h246v-788q0 -283 -150.5 -438.5t-423 -155.5t-422 155.5t-149.5 438.5zM483 1483v194h207v-194h-207zM811 1483v194h207v-194h-207z" />
-<glyph unicode="&#xdd;" horiz-adv-x="1368" d="M25 1366h297l362 -555l360 555h297l-534 -780v-586h-246v586zM594 1483l127 239h201l-191 -239h-137z" />
-<glyph unicode="&#xde;" horiz-adv-x="1253" d="M176 0v1366h246v-172h209q242 0 374 -120t132 -339t-132.5 -339t-373.5 -120h-209v-276h-246zM422 500h190q162 0 221.5 59t59.5 176q0 115 -59.5 173.5t-221.5 58.5h-190v-467z" />
-<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M180 0v856q0 156 40 265.5t108.5 162.5t140.5 75.5t156 22.5q223 0 344 -113t121 -265q0 -109 -63.5 -187t-133.5 -104q37 -10 70.5 -27.5t82 -51.5t77 -93.5t28.5 -134.5q0 -201 -160 -310q-144 -97 -364 -97q-24 0 -49 1v199h12q334 0 334 215q0 193 -346 204v181 q66 0 124 11t113 58t55 127q0 213 -219 213q-51 0 -89 -13t-75.5 -48t-58 -109.5t-20.5 -187.5v-850h-228z" />
-<glyph unicode="&#xe0;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM313 1395h201l127 -240h-137z M358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
-<glyph unicode="&#xe1;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM358 365q0 -164 193 -164h2 q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM459 1155l127 240h200l-190 -240h-137z" />
-<glyph unicode="&#xe2;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM338 1155l186 250h99l188 -250h-164 l-74 115l-73 -115h-162zM358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
-<glyph unicode="&#xe3;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM258 1153v156q57 68 124 74 q11 1 22 1q55 0 105 -26l121 -60q52 -25 105 -25q11 0 23 1q65 6 123 74v-156q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -106 25l-121 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74zM358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133z" />
-<glyph unicode="&#xe4;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM287 1155v195h207v-195h-207z M358 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM614 1155v195h207v-195h-207z" />
-<glyph unicode="&#xe5;" horiz-adv-x="1110" d="M115 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q219 0 335 -111.5t116 -322.5v-276q0 -141 -105.5 -242.5t-324.5 -101.5q-225 0 -331.5 102t-106.5 248zM358 365q0 -164 193 -164h2 q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM389 1272q0 84 47 130t131 46q86 0 132 -46t46 -130t-46 -130t-132 -46q-84 0 -131 46t-47 130zM498 1272q0 -49 15 -68.5t54 -19.5t54.5 19.5t15.5 68.5t-15.5 68.5t-54.5 19.5t-54 -19.5t-15 -68.5z" />
-<glyph unicode="&#xe6;" horiz-adv-x="1792" d="M109 334q0 31 6 63.5t31.5 81.5t69.5 86t130 63.5t203 26.5q84 0 180 -14q0 193 -217 193q-172 0 -299 -52v199q141 57 313 57q236 0 338 -143q35 57 121 101t199 44q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198 q-170 -92 -385 -92q-104 0 -193 43t-135 102q-106 -145 -327 -145q-225 0 -331.5 102t-106.5 248zM352 365q0 -164 193 -164h2q184 0 184 155v129q-86 12 -147 13q-231 0 -232 -133zM979 588h412q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
-<glyph unicode="&#xe7;" horiz-adv-x="966" d="M119 510q0 256 125 392t358 136q150 0 295 -57v-205q-121 49 -254 49q-141 0 -209.5 -64.5t-68.5 -250.5q0 -184 68.5 -249.5t209.5 -65.5q127 0 254 51v-205q-106 -45 -227 -53l-123 -244h-213l176 246q-190 25 -290.5 158t-100.5 362z" />
-<glyph unicode="&#xe8;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM334 1395h201l127 -240h-138zM381 588h412 q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
-<glyph unicode="&#xe9;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM381 588h412q-10 106 -62.5 179t-144.5 73 t-143.5 -59.5t-61.5 -192.5zM496 1155l127 240h200l-190 -240h-137z" />
-<glyph unicode="&#xea;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM346 1155l186 250h99l188 -250h-164l-73 115l-74 -115 h-162zM381 588h412q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
-<glyph unicode="&#xeb;" horiz-adv-x="1144" d="M129 500q0 246 109.5 393t347.5 147q225 0 340.5 -143t115.5 -352q0 -86 -4 -127h-657q10 -133 79.5 -178t180.5 -45q180 0 348 79v-198q-170 -92 -385 -92q-68 0 -128 11t-125.5 46t-113 89t-78 149.5t-30.5 220.5zM309 1155v195h207v-195h-207zM381 588h412 q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5zM637 1155v195h207v-195h-207z" />
-<glyph unicode="&#xec;" horiz-adv-x="638" d="M63 1395h201l127 -240h-137zM197 0h245v1022h-245v-1022z" />
-<glyph unicode="&#xed;" horiz-adv-x="638" d="M197 0h245v1022h-245v-1022zM248 1155l127 240h200l-190 -240h-137z" />
-<glyph unicode="&#xee;" horiz-adv-x="638" d="M84 1155l186 250h99l188 -250h-164l-74 115l-73 -115h-162zM197 0h245v1022h-245v-1022z" />
-<glyph unicode="&#xef;" horiz-adv-x="638" d="M53 1155v195h207v-195h-207zM197 0h245v1022h-245v-1022zM381 1155v195h207v-195h-207z" />
-<glyph unicode="&#xf0;" horiz-adv-x="1232" d="M117 461q0 211 120.5 331.5t342.5 120.5q160 0 249 -69q-27 152 -145 237l-180 -123l-78 113l104 74q-41 10 -90 10q-82 0 -145 -10v219q92 20 160 20q178 0 315 -75l162 112l78 -114l-123 -84q199 -195 198 -594q0 -645 -485 -645q-127 0 -224 43t-151.5 114.5 t-81 152.5t-26.5 167zM365 463q0 -31 7 -66t27.5 -84t72.5 -80.5t128 -31.5q39 0 76 16t79 57t65 132q19 76 19 179q0 19 -1 38q-103 69 -234 69h-4q-113 0 -174 -62.5t-61 -166.5z" />
-<glyph unicode="&#xf1;" horiz-adv-x="1273" d="M168 0v926q225 112 469 112q213 0 333 -111.5t120 -312.5v-614h-244v635q0 41 -7.5 68.5t-30 57.5t-73.5 45t-131 15q-90 0 -190 -18v-803h-246zM326 1153v156q57 68 124 74q10 1 21 1q55 0 105 -26l122 -60q52 -25 105 -25q11 0 23 1q65 6 122 74v-156q-57 -68 -122 -74 q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74z" />
-<glyph unicode="&#xf2;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM326 1395h200l127 -240h-137zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5z" />
-<glyph unicode="&#xf3;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5zM514 1155l127 240h201l-191 -240h-137z" />
-<glyph unicode="&#xf4;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5zM348 1155l187 250h98l188 -250h-164l-73 115l-74 -115h-162 z" />
-<glyph unicode="&#xf5;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM272 1153v156q57 68 124 74q11 1 22 1q55 0 106 -26l122 -60q51 -25 104 -25q11 0 22 1q66 6 123 74v-156q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60 q-51 26 -106 26q-11 0 -22 -1q-66 -6 -124 -74zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5z" />
-<glyph unicode="&#xf6;" horiz-adv-x="1159" d="M100 510q0 254 125 390t358.5 136t358.5 -136t125 -390t-125 -390t-358.5 -136t-358.5 136t-125 390zM317 1155v195h207v-195h-207zM346 509.5q0 -190.5 58.5 -257t179.5 -66.5t179 66.5t58 257t-58 257.5t-179 67t-179.5 -67t-58.5 -257.5zM645 1155v195h207v-195h-207z " />
-<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M211 446v164h866v-164h-866zM541 129v193h207v-193h-207zM541 737v193h207v-193h-207z" />
-<glyph unicode="&#xf8;" horiz-adv-x="1175" d="M39 0l147 176q-82 133 -82 334q0 254 125 390t359 136q164 0 270 -63l43 49h238l-150 -176q82 -127 82 -336q0 -254 -125 -390t-358 -136q-158 0 -271 65l-41 -49h-237zM350 510q0 -68 10 -129l361 426q-47 27 -133 27q-121 0 -179.5 -67t-58.5 -257zM457 215 q51 -29 131 -29q121 0 179 67t58 257q0 82 -8 131z" />
-<glyph unicode="&#xf9;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM381 1395h201l127 -240h-138z" />
-<glyph unicode="&#xfa;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM551 1155l127 240h201l-191 -240h-137z" />
-<glyph unicode="&#xfb;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM393 1155l187 250h98l188 -250h-164l-73 115l-74 -115h-162z" />
-<glyph unicode="&#xfc;" horiz-adv-x="1259" d="M168 463v559h248v-592q0 -123 50 -177t163 -54q115 0 165 54t50 177v592h248v-559q0 -225 -123 -352t-340 -127t-339 127t-122 352zM362 1155v195h207v-195h-207zM690 1155v195h207v-195h-207z" />
-<glyph unicode="&#xfd;" horiz-adv-x="1107" d="M63 1022h261l229 -713l225 713h260l-389 -1047q-82 -223 -178 -300.5t-199 -77.5q-76 0 -155 28v205q59 -25 116 -25q106 0 197 248zM473 1155l127 240h201l-191 -240h-137z" />
-<glyph unicode="&#xfe;" horiz-adv-x="1157" d="M170 -377v1778h246v-389q88 16 168 16q229 0 352 -133t123 -383t-123 -383t-352 -133q-80 0 -168 16v-389h-246zM416 207q69 -17 159 -17q115 0 175.5 71t60.5 251t-60.5 251t-175.5 71q-90 0 -159 -17v-610z" />
-<glyph unicode="&#xff;" horiz-adv-x="1107" d="M63 1022h261l229 -713l225 713h260l-389 -1047q-82 -223 -178 -300.5t-199 -77.5q-76 0 -155 28v205q59 -25 116 -25q106 0 197 248zM285 1155v195h207v-195h-207zM612 1155v195h207v-195h-207z" />
-<glyph unicode="&#x152;" horiz-adv-x="2074" d="M123 682q0 168 39 295t100.5 202.5t148.5 123t170 63.5t179 16q147 0 270 -43v27h909v-221h-663v-346h575v-232h-575v-346h663v-221h-909v27q-115 -43 -270 -43q-96 0 -179 16t-170 63.5t-148.5 122t-100.5 202.5t-39 294zM369 684q0 -154 33.5 -255t98 -148.5 t131.5 -64.5t161 -17q145 0 237 37v884q-111 45 -237 45q-94 0 -161 -16t-131.5 -63.5t-98 -148.5t-33.5 -253z" />
-<glyph unicode="&#x153;" horiz-adv-x="1853" d="M98 512q0 254 125 390t359 136q229 0 356 -133q119 135 338 135q225 0 341 -143t116 -352q0 -86 -4 -127h-658q10 -133 80 -178t180 -45q180 0 348 79v-198q-170 -92 -385 -92q-233 0 -356 135q-123 -133 -356.5 -133t-358.5 136t-125 390zM344 512q0 -190 58.5 -257 t179 -67t179 67t58.5 257t-58.5 257t-179 67t-179 -67t-58.5 -257zM1071 588h412q-10 106 -62.5 179t-144.5 73t-143.5 -59.5t-61.5 -192.5z" />
-<glyph unicode="&#x178;" horiz-adv-x="1368" d="M25 1366h297l362 -555l360 555h297l-534 -780v-586h-246v586zM418 1483v194h207v-194h-207zM745 1483v194h207v-194h-207z" />
-<glyph unicode="&#x2c6;" horiz-adv-x="874" d="M201 1155l186 250h98l189 -250h-164l-74 115l-74 -115h-161z" />
-<glyph unicode="&#x2dc;" horiz-adv-x="1058" d="M215 1153v156q57 68 124 74q11 1 22 1q55 0 105 -26l122 -60q51 -25 104 -25q11 0 22 1q66 6 124 74v-156q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74z" />
-<glyph unicode="&#x2000;" horiz-adv-x="888" />
-<glyph unicode="&#x2001;" horiz-adv-x="1776" />
-<glyph unicode="&#x2002;" horiz-adv-x="888" />
-<glyph unicode="&#x2003;" horiz-adv-x="1776" />
-<glyph unicode="&#x2004;" horiz-adv-x="592" />
-<glyph unicode="&#x2005;" horiz-adv-x="444" />
-<glyph unicode="&#x2006;" horiz-adv-x="296" />
-<glyph unicode="&#x2007;" horiz-adv-x="296" />
-<glyph unicode="&#x2008;" horiz-adv-x="222" />
-<glyph unicode="&#x2009;" horiz-adv-x="355" />
-<glyph unicode="&#x200a;" horiz-adv-x="98" />
-<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
-<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
-<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 514v193h624v-193h-624z" />
-<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 500v192h860v-192h-860z" />
-<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 500v192h1501v-192h-1501z" />
-<glyph unicode="&#x2018;" horiz-adv-x="552" d="M96 1384h215l152 -401h-184z" />
-<glyph unicode="&#x2019;" horiz-adv-x="552" d="M96 983l152 401h215l-182 -401h-185z" />
-<glyph unicode="&#x201a;" horiz-adv-x="651" d="M147 -229l152 401h215l-182 -401h-185z" />
-<glyph unicode="&#x201c;" horiz-adv-x="858" d="M96 1384h215l152 -401h-184zM408 1384h215l151 -401h-184z" />
-<glyph unicode="&#x201d;" horiz-adv-x="858" d="M96 983l152 401h215l-182 -401h-185zM408 983l151 401h215l-182 -401h-184z" />
-<glyph unicode="&#x201e;" horiz-adv-x="948" d="M147 -229l152 401h215l-182 -401h-185zM459 -229l151 401h215l-182 -401h-184z" />
-<glyph unicode="&#x2022;" horiz-adv-x="890" d="M172 661.5q0 112.5 80 192.5t194 80q113 0 192 -80t79 -192.5t-79 -192.5t-192 -80q-115 0 -194.5 80t-79.5 192.5z" />
-<glyph unicode="&#x2026;" horiz-adv-x="1726" d="M219 0v227h240v-227h-240zM743 0v227h240v-227h-240zM1266 0v227h239v-227h-239z" />
-<glyph unicode="&#x202f;" horiz-adv-x="355" />
-<glyph unicode="&#x2039;" horiz-adv-x="770" d="M63 522l347 389h286l-372 -389l372 -391h-286z" />
-<glyph unicode="&#x203a;" horiz-adv-x="815" d="M111 131l372 389l-372 391h286l346 -391l-346 -389h-286z" />
-<glyph unicode="&#x205f;" horiz-adv-x="444" />
-<glyph unicode="&#x20ac;" d="M102 412v178h162q-4 57 -4 94t4 94h-162v178h191q115 428 604 428q209 0 414 -81v-230q-194 90 -391 90q-137 0 -231.5 -43t-141.5 -164h530v-178h-567q-4 -57 -4 -94t4 -94h567v-178h-530q47 -121 141 -164t232 -43q197 0 391 90v-229q-205 -82 -414 -82 q-492 0 -604 428h-191z" />
-<glyph unicode="&#x2122;" horiz-adv-x="1572" d="M100 1245v121h559v-121h-219v-561h-121v561h-219zM762 684v682h137l197 -285l196 285h138v-682h-123v506l-211 -303l-211 303v-506h-123z" />
-<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
-<glyph unicode="&#xfb01;" horiz-adv-x="1333" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM950 1161h252v225h-252v-225zM954 0h246v1022h-246v-1022z" />
-<glyph unicode="&#xfb02;" horiz-adv-x="1333" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM958 0h246v1411h-246v-1411z" />
-<glyph unicode="&#xfb03;" horiz-adv-x="2100" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM813 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM1655 1161v225 h251v-225h-251zM1659 0v1022h245v-1022h-245z" />
-<glyph unicode="&#xfb04;" horiz-adv-x="2121" d="M82 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM813 811v211h174q2 424 381 424q49 0 100 -10v-193q-37 4 -55 4q-96 0 -138 -46t-42 -179h235v-211h-235v-811h-246v811h-174zM1669 0v1411h246 v-1411h-246z" />
-</font>
-</defs></svg> 
\ No newline at end of file
diff --git a/static/fnt/mavenpro-bold-webfont.ttf b/static/fnt/mavenpro-bold-webfont.ttf
deleted file mode 100644 (file)
index 2120446..0000000
Binary files a/static/fnt/mavenpro-bold-webfont.ttf and /dev/null differ
diff --git a/static/fnt/mavenpro-bold-webfont.woff b/static/fnt/mavenpro-bold-webfont.woff
deleted file mode 100644 (file)
index 7f73d1e..0000000
Binary files a/static/fnt/mavenpro-bold-webfont.woff and /dev/null differ
diff --git a/static/fnt/mavenpro-medium-webfont.eot b/static/fnt/mavenpro-medium-webfont.eot
deleted file mode 100644 (file)
index efb30f5..0000000
Binary files a/static/fnt/mavenpro-medium-webfont.eot and /dev/null differ
diff --git a/static/fnt/mavenpro-medium-webfont.svg b/static/fnt/mavenpro-medium-webfont.svg
deleted file mode 100644 (file)
index 8e8503b..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
-<svg xmlns="http://www.w3.org/2000/svg">
-<metadata></metadata>
-<defs>
-<font id="maven_promedium" horiz-adv-x="1529" >
-<font-face units-per-em="2048" ascent="1638" descent="-410" />
-<missing-glyph horiz-adv-x="692" />
-<glyph horiz-adv-x="2048" />
-<glyph horiz-adv-x="2048" />
-<glyph unicode="&#xd;" horiz-adv-x="682" />
-<glyph unicode=" "  horiz-adv-x="692" />
-<glyph unicode="&#x09;" horiz-adv-x="692" />
-<glyph unicode="&#xa0;" horiz-adv-x="692" />
-<glyph unicode="!" horiz-adv-x="800" d="M299 1366h195l-17 -1032h-162zM303 0v174h186v-174h-186z" />
-<glyph unicode="&#x22;" horiz-adv-x="821" d="M166 1384h190l-34 -411h-119zM461 1384h190l-37 -411h-118z" />
-<glyph unicode="#" horiz-adv-x="1210" d="M145 260v135h183l55 318h-180v135h203l38 221h144l-39 -221h256l39 221h143l-39 -221h137v-135h-159l-56 -318h158v-135h-180l-39 -221h-143l39 221h-259l-36 -221h-144l39 221h-160zM471 395h256l55 318h-256z" />
-<glyph unicode="$" horiz-adv-x="1198" d="M158 993q0 145 96 256t299 131v209h127v-207q160 -10 307 -86v-188q-141 86 -307 90v-418q86 -31 145.5 -60.5t122 -78.5t95 -120t32.5 -163q0 -156 -103.5 -252t-291.5 -116v-211h-127v207q-193 6 -377 98v197q180 -109 377 -115v467q-76 23 -118 38t-102.5 46t-93 64.5 t-57 89t-24.5 122.5zM348 993q0 -57 51.5 -94t153.5 -74v367q-205 -35 -205 -199zM680 174q205 35 205 184q0 86 -53.5 138.5t-151.5 91.5v-414z" />
-<glyph unicode="%" horiz-adv-x="1814" d="M145 981q0 178 75 278.5t222.5 100.5t222 -100.5t74.5 -278.5t-74.5 -277.5t-222 -99.5t-222.5 99.5t-75 277.5zM276 981q0 -133 38 -197.5t128 -64.5t128 64.5t38 197.5q0 135 -38 198.5t-128 63.5t-128 -63.5t-38 -198.5zM410 0l858 1366h151l-858 -1366h-151z M1087 385q0 176 75 276.5t222.5 100.5t222 -100.5t74.5 -276.5q0 -178 -74.5 -278.5t-222 -100.5t-222.5 100.5t-75 278.5zM1219 385q0 -135 37.5 -198.5t128 -63.5t128 63.5t37.5 198.5q0 133 -37.5 197.5t-128 64.5t-128 -64.5t-37.5 -197.5z" />
-<glyph unicode="&#x26;" horiz-adv-x="1325" d="M111 385q0 115 54 216t210 192q-127 158 -127 272q0 131 93 225t226 94q135 0 227.5 -90t92.5 -221q0 -109 -56.5 -183.5t-183.5 -148.5l266 -319q37 84 54 176h176q-27 -168 -115 -313l238 -285h-228l-116 141q-157 -157 -374 -157h-3q-113 0 -200 39.5t-136 103 t-73.5 129.5t-24.5 129zM281 379q0 -31 11 -65.5t38.5 -75.5t84 -68t134.5 -27q156 0 270 123l-327 396q-74 -39 -121 -79t-64.5 -81t-21.5 -64.5t-4 -58.5zM416 1077.5q0 -65.5 114 -203.5q119 68 155 111t36 100q0 63 -43 105.5t-109 42.5q-63 0 -108 -45t-45 -110.5z" />
-<glyph unicode="'" horiz-adv-x="524" d="M166 1384h190l-34 -411h-119z" />
-<glyph unicode="(" horiz-adv-x="722" d="M190 627q0 440 211 856h185q-211 -416 -211 -856q0 -436 211 -858h-185q-211 422 -211 858z" />
-<glyph unicode=")" horiz-adv-x="722" d="M133 -231q211 412 211 858q0 449 -211 856h184q211 -416 211 -856q0 -436 -211 -858h-184z" />
-<glyph unicode="*" horiz-adv-x="841" d="M172 1118l162 70l-162 72l64 108l139 -100l-19 168h125l-14 -168l139 100l64 -108l-160 -72l160 -70l-64 -112l-139 102l14 -166h-125l19 166l-139 -102z" />
-<glyph unicode="+" horiz-adv-x="1150" d="M199 545v141h297v297h141v-297h295v-141h-295v-295h-141v295h-297z" />
-<glyph unicode="," horiz-adv-x="667" d="M152 -229l151 401h193l-172 -401h-172z" />
-<glyph unicode="-" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
-<glyph unicode="." horiz-adv-x="712" d="M260 0v182h195v-182h-195z" />
-<glyph unicode="/" horiz-adv-x="870" d="M-29 -201l752 1749h178l-754 -1749h-176z" />
-<glyph unicode="0" horiz-adv-x="1351" d="M133 684q0 166 34 294t86 202.5t126 122t145.5 63.5t153.5 16q104 0 191 -29.5t171 -101t132.5 -216t48.5 -351.5q0 -168 -34 -295t-86 -202.5t-126 -123t-144.5 -63.5t-152.5 -16t-153.5 16t-145.5 63.5t-126 123t-86 202.5t-34 295zM324 684q0 -135 21.5 -234.5 t54 -154.5t82.5 -88t94.5 -42t101.5 -9q55 0 100 9t94.5 42t82 88t54 154.5t21.5 234.5t-21.5 233.5t-54 153.5t-82 88t-94.5 42t-100 9q-57 0 -101.5 -9t-94.5 -42t-82.5 -88t-54 -153.5t-21.5 -233.5z" />
-<glyph unicode="1" horiz-adv-x="925" d="M211 1040v199l223 127h207v-1366h-190v1180z" />
-<glyph unicode="2" horiz-adv-x="1189" d="M158 0v152q111 84 214 177t210.5 208.5t172 234.5t64.5 215q0 219 -303 219q-184 0 -336 -102v180q170 98 361 98q238 0 351.5 -115.5t113.5 -277.5q0 -346 -519 -799h533v-190h-862z" />
-<glyph unicode="3" horiz-adv-x="1193" d="M166 63v187q160 -92 334 -92q338 0 338 239.5t-359 239.5h-119v168h115q295 0 295 199q0 200 -292 200h-3q-152 0 -278 -69v182q139 65 296 65h5q238 0 350.5 -110.5t112.5 -267.5q0 -177 -162 -273q229 -102 229 -332q0 -80 -27.5 -150.5t-86 -132t-161 -97 t-237.5 -35.5q-178 0 -350 79z" />
-<glyph unicode="4" horiz-adv-x="1210" d="M78 430v170l600 766h213v-766h194v-170h-194v-430h-191v430h-622zM303 600h397v494z" />
-<glyph unicode="5" horiz-adv-x="1228" d="M178 739l70 627h756v-176h-590l-33 -348q96 26 199 26h8q238 0 361.5 -112.5t123.5 -329.5t-124 -329.5t-361.5 -112.5t-401.5 102v205q133 -125 385 -125q180 0 246 65.5t66 194.5q0 131 -69 196.5t-263 65.5q-150 0 -277 -49z" />
-<glyph unicode="6" horiz-adv-x="1202" d="M133 627q0 141 25.5 256.5t65.5 191.5t96.5 134.5t111.5 90t119.5 51t112 25.5t94.5 6q106 0 211 -28v-172q-81 24 -175 24h-8q-180 0 -295.5 -100t-154.5 -287q111 72 285 72q240 0 348 -128t108 -310q0 -193 -120.5 -331t-350.5 -138q-473 0 -473 643zM324 616 q2 -131 24.5 -224t50 -138t72.5 -68.5t70.5 -26.5t64.5 -3q88 0 149.5 32.5t87 85t35 94.5t9.5 85q0 264 -281 264q-176 0 -282 -101z" />
-<glyph unicode="7" horiz-adv-x="1189" d="M143 1178v188h951v-180l-572 -1186h-211l580 1178h-748z" />
-<glyph unicode="8" horiz-adv-x="1196" d="M125 416q0 104 57.5 188t147.5 131q-154 98 -154 283q0 150 105.5 257t324.5 107t324.5 -107.5t105.5 -258.5q0 -186 -151 -279q200 -105 200 -319q0 -180 -122.5 -307t-356 -127t-357.5 128t-124 304zM315 414q0 -37 9.5 -74t37 -82t90 -72.5t154.5 -27.5t154.5 27.5 t90.5 72.5t37 82t9 74q0 225 -291 225t-291 -225zM365 1022q0 -31 8 -60.5t30.5 -67.5t74.5 -60.5t128 -22.5t128 22.5t73.5 60.5t30 67.5t8.5 60.5q0 186 -240 186q-241 0 -241 -186z" />
-<glyph unicode="9" horiz-adv-x="1212" d="M119 913q0 193 120.5 331t352.5 138q471 0 471 -643q0 -141 -25.5 -256.5t-65.5 -191.5t-96.5 -134.5t-111.5 -90t-119.5 -51t-112 -25.5t-94.5 -6q-106 0 -211 28v172q85 -24 176 -24h7q180 0 295.5 100t154.5 287q-111 -72 -285 -72q-240 0 -348 128t-108 310zM309 913 q0 -264 283 -264q176 0 282 101q-2 131 -24.5 224t-50 138t-72.5 68.5t-70.5 26.5t-64.5 3q-90 0 -151.5 -32.5t-87 -85t-35 -94.5t-9.5 -85z" />
-<glyph unicode=":" horiz-adv-x="813" d="M313 94v180h193v-180h-193zM313 741v181h193v-181h-193z" />
-<glyph unicode=";" horiz-adv-x="858" d="M184 -229l152 401h192l-172 -401h-172zM340 741v181h192v-181h-192z" />
-<glyph unicode="&#x3c;" horiz-adv-x="1357" d="M246 477v148l850 379v-181l-643 -272l643 -272v-181z" />
-<glyph unicode="=" horiz-adv-x="1306" d="M260 365v159h789v-159h-789zM260 698v160h789v-160h-789z" />
-<glyph unicode="&#x3e;" horiz-adv-x="1337" d="M270 98v181l641 272l-641 272v181l848 -379v-148z" />
-<glyph unicode="?" horiz-adv-x="1042" d="M164 1083v203q150 96 321 96q115 0 200 -33.5t132 -92t69.5 -126t22.5 -145.5q0 -82 -30.5 -146.5t-75.5 -106.5l-90 -83q-45 -41 -76 -100.5t-31 -134.5v-70h-180v76q0 104 30.5 184t76 127t89.5 86t74.5 81t30.5 89q0 219 -258 219q-166 0 -305 -123zM416 0v186h200 v-186h-200z" />
-<glyph unicode="@" horiz-adv-x="1869" d="M170 371q0 184 79 348t204 273.5t280 174t311 64.5q82 0 171.5 -21.5t180.5 -74t162.5 -126t117.5 -188t46 -254.5q0 -129 -33.5 -239.5t-113.5 -188.5t-193 -78q-137 0 -186 127q-90 -127 -293 -127q-143 0 -227 64.5t-84 175.5q0 51 16.5 96t59.5 95.5t138 80t232 29.5 q84 0 142 -8q0 4 1 15.5t1 17.5q0 35 -6.5 59.5t-26 50t-62.5 37.5t-110 12q-131 0 -252 -49l18 129q135 45 252 45q156 0 239 -72.5t83 -211.5q0 -53 -9 -129l-16 -125q-4 -37 -4 -74v-25q2 -49 26.5 -72.5t76.5 -23.5q88 0 151.5 97.5t63.5 287.5q0 143 -56.5 255 t-145.5 173.5t-183.5 92t-184.5 30.5q-184 0 -357 -98t-284.5 -273.5t-111.5 -377.5q0 -127 45 -229.5t109.5 -162t145.5 -99.5t144.5 -54.5t114.5 -14.5q250 0 442 103l-18 -133q-184 -82 -428 -82q-82 0 -169 22.5t-177.5 74.5t-161 128t-115.5 193.5t-45 259.5zM725 305 q0 -108 175 -108h3q141 0 188.5 51t61.5 153q4 49 8 74q-55 6 -123 6q-313 0 -313 -176z" />
-<glyph unicode="A" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM463 561h434l-217 567z" />
-<glyph unicode="B" horiz-adv-x="1280" d="M178 0v1366h473q139 0 234.5 -37t138.5 -98.5t59.5 -117.5t16.5 -120q0 -59 -16.5 -109t-40 -80t-48 -51.5t-41.5 -29.5l-16 -8l26 -11q16 -6 57.5 -32.5t73 -59t58.5 -92t27 -129.5q0 -68 -18.5 -127t-65.5 -123.5t-150.5 -102.5t-255.5 -38h-512zM369 168l309 2 q158 0 234.5 53t76.5 176q0 127 -84 173t-241 46h-295v-450zM369 788h280q127 0 193.5 42t66.5 157q0 117 -70.5 163t-207.5 46h-262v-408z" />
-<glyph unicode="C" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q213 0 418 -81v-177q-193 86 -396 86q-242 0 -351 -124.5t-109 -403.5q0 -276 109 -402t351 -126q203 0 396 86v-176q-205 -82 -418 -82q-94 0 -177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295z" />
-<glyph unicode="D" horiz-adv-x="1380" d="M178 0v1366h436q315 0 478 -174t163 -510t-162.5 -509t-478.5 -173h-436zM369 168h215q250 0 366.5 124t116.5 390q0 268 -116.5 392t-366.5 124h-215v-1030z" />
-<glyph unicode="E" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891z" />
-<glyph unicode="F" horiz-adv-x="1124" d="M178 0v1366h860v-178h-669v-416h589v-178h-589v-594h-191z" />
-<glyph unicode="G" horiz-adv-x="1386" d="M125 684q0 166 39 294t100.5 202.5t147.5 122t169 63.5t179 16q227 0 430 -79v-175q-199 82 -412 82q-74 0 -132 -9t-122.5 -42t-107.5 -88t-72 -153.5t-29 -233.5t29 -234.5t72 -154.5t108.5 -88t125 -42t132.5 -9q135 0 275 39v385h-279v178h463v-666 q-227 -108 -476 -108h-5q-96 0 -179 16t-169 63.5t-147.5 123t-100.5 202.5t-39 295z" />
-<glyph unicode="H" horiz-adv-x="1388" d="M178 0v1366h189v-594h655v594h188v-1366h-188v594h-655v-594h-189z" />
-<glyph unicode="I" horiz-adv-x="544" d="M178 0v1366h189v-1366h-189z" />
-<glyph unicode="J" horiz-adv-x="931" d="M61 66v198q119 -96 269 -96q117 0 174 49t57 186v963h189v-950q0 -133 -40 -225.5t-106.5 -134.5t-127 -58t-128.5 -16q-158 0 -287 84z" />
-<glyph unicode="K" horiz-adv-x="1380" d="M178 0v1366h189v-661l626 661h240l-580 -618l631 -748h-235l-521 614l-161 -172v-442h-189z" />
-<glyph unicode="L" horiz-adv-x="1089" d="M178 0v1366h189v-1188h680v-178h-869z" />
-<glyph unicode="M" horiz-adv-x="1665" d="M178 0v1366h205l448 -590l451 590h205v-1366h-189v1077l-467 -612l-464 612v-1077h-189z" />
-<glyph unicode="N" horiz-adv-x="1511" d="M178 0v1366h217l764 -1087v1087h191v-1366h-211l-770 1094v-1094h-191z" />
-<glyph unicode="O" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5z" />
-<glyph unicode="P" horiz-adv-x="1228" d="M176 0v1366h492q227 0 352 -112.5t125 -317.5t-125 -317.5t-352 -112.5h-301v-506h-191zM367 674h284q182 0 243.5 70.5t61.5 189.5t-61 189.5t-244 70.5h-284v-520z" />
-<glyph unicode="Q" d="M125 684q0 166 39 294t100.5 202.5t148.5 122t170 63.5t177 16q96 0 179 -16t169 -63.5t147.5 -122t100.5 -202.5t39 -294q0 -182 -45 -317.5t-124 -212t-167 -114.5t-195 -50q0 -82 54.5 -143.5t150.5 -61.5v-145q-170 0 -280.5 101t-110.5 247q-111 10 -202 46 t-174 111.5t-130 213t-47 325.5zM315 684q0 -135 28 -234.5t69 -154.5t103.5 -88t118.5 -42t125.5 -9t126 9t119 42t104.5 88t69.5 154.5t27.5 234.5t-27.5 233.5t-68.5 153.5t-104.5 88t-119 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -153.5t-28 -233.5z" />
-<glyph unicode="R" horiz-adv-x="1298" d="M178 0v1366h504q229 0 360.5 -107.5t131.5 -306.5q0 -168 -87.5 -267t-218.5 -136l363 -549h-225l-330 518h-307v-518h-191zM369 698h305q178 0 243.5 69t65.5 179q0 250 -317 250h-297v-498z" />
-<glyph unicode="S" horiz-adv-x="1155" d="M125 993q0 162 116.5 275.5t360.5 113.5q184 0 352 -86v-188q-150 90 -329 90q-310 0 -310 -205q0 -55 54.5 -96t134.5 -65.5t175 -63.5t175 -83t134 -129t54 -198q0 -176 -131 -275t-362 -99q-205 0 -406 100v197q190 -115 389 -115q319 0 320 192q0 82 -54 140t-134 84 l-176 59q-96 33 -175.5 67.5t-133.5 107.5t-54 177z" />
-<glyph unicode="T" horiz-adv-x="1163" d="M25 1188v178h1112v-178h-463v-1188h-189v1188h-460z" />
-<glyph unicode="U" horiz-adv-x="1474" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5z" />
-<glyph unicode="V" horiz-adv-x="1288" d="M25 1366h202l416 -1126l418 1126h203l-525 -1366h-190z" />
-<glyph unicode="W" horiz-adv-x="1961" d="M25 1366h198l338 -1090l336 1090h162l338 -1090l336 1090h198l-438 -1366h-195l-319 1026l-322 -1026h-194z" />
-<glyph unicode="X" horiz-adv-x="1253" d="M25 0l489 684l-477 682h231l359 -510l358 510h232l-484 -664l496 -702h-234l-368 526l-371 -526h-231z" />
-<glyph unicode="Y" horiz-adv-x="1275" d="M25 1366h229l383 -559l383 559h231l-520 -754v-612h-188v610z" />
-<glyph unicode="Z" horiz-adv-x="1247" d="M131 0v166l721 1010h-700v190h946v-170l-717 -1008h717v-188h-967z" />
-<glyph unicode="[" horiz-adv-x="784" d="M244 -238v1663h370v-151h-213v-1360h213v-152h-370z" />
-<glyph unicode="\" horiz-adv-x="802" d="M-76 1548h178l752 -1749h-176z" />
-<glyph unicode="]" horiz-adv-x="780" d="M172 -86h211v1360h-211v151h369v-1663h-369v152z" />
-<glyph unicode="^" horiz-adv-x="1146" d="M272 969l215 444h113l217 -444h-143l-129 303l-129 -303h-144z" />
-<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-115h-1118v115z" />
-<glyph unicode="`" horiz-adv-x="491" d="M90 1395h184l127 -240h-116z" />
-<glyph unicode="a" horiz-adv-x="1093" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q145 55 309 55q213 0 329 -110.5t116 -317.5v-295q0 -33 -6.5 -65.5t-31 -83.5t-66.5 -89t-126 -65.5t-194 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88z" />
-<glyph unicode="b" horiz-adv-x="1167" d="M170 315v1096h186v-403q106 30 219 30q88 0 164 -21.5t149 -74.5t114.5 -159.5t41.5 -258.5q0 -135 -31.5 -236.5t-77.5 -157.5t-110.5 -91t-119 -45t-113.5 -10q-111 0 -194 27.5t-125 65.5t-66.5 89t-30.5 84t-6 65zM356 303q0 -153 236 -153q53 0 94 13t83 49 t64.5 115t22.5 197q0 111 -22.5 183.5t-64.5 106.5t-87 46t-107 12q-115 0 -219 -43v-526z" />
-<glyph unicode="c" horiz-adv-x="966" d="M119 512q0 256 124 391t357 135q156 0 301 -57v-178q-139 67 -281 67h-4q-141 0 -221 -72.5t-80 -285.5q0 -113 23.5 -187.5t70 -110.5t94 -48t113.5 -12q147 0 285 65v-178q-145 -57 -301 -57q-233 0 -357 136t-124 392z" />
-<glyph unicode="d" horiz-adv-x="1150" d="M129 524q0 152 42 258.5t114.5 159.5t148.5 74.5t164 21.5q111 0 219 -30v403h187v-1096q0 -33 -6.5 -65.5t-31 -83.5t-66.5 -89t-125 -65.5t-193 -27.5q-59 0 -113.5 10t-119 45t-110.5 91t-78 157.5t-32 236.5zM315 524q0 -119 22.5 -197.5t64.5 -114.5t83 -49t97 -13 q236 0 235 153v526q-104 43 -219 43q-61 0 -106.5 -12t-88.5 -46t-65.5 -106.5t-22.5 -183.5z" />
-<glyph unicode="e" horiz-adv-x="1136" d="M129 498q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -142h-704q34 -260 288 -260q207 0 353 76v-170q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 573h525q-4 51 -17.5 97.5t-41 94.5t-79.5 76.5t-124 28.5 q-129 0 -189.5 -66.5t-73.5 -230.5z" />
-<glyph unicode="f" horiz-adv-x="675" d="M82 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-49h264v-164h-264v-858h-187v858h-143z" />
-<glyph unicode="g" horiz-adv-x="1101" d="M123 518q0 238 107.5 380t330.5 142q186 0 298 -84t112 -239v-760q0 -68 -19.5 -126t-65.5 -113.5t-138.5 -87t-225.5 -31.5q-154 0 -303 43v178q145 -45 287 -45q98 0 159.5 17.5t85 50t28.5 55t5 59.5v90q-104 -29 -206 -29q-219 0 -337 129t-118 371zM311 518 q0 -199 71 -264.5t196 -65.5q110 0 206 39v502q0 23 -4 39t-22.5 45t-68.5 44t-128 15q-51 0 -89 -12t-78 -46t-61.5 -108.5t-21.5 -187.5z" />
-<glyph unicode="h" horiz-adv-x="1187" d="M193 0v1411h188v-424q131 51 270 51q190 0 295 -113.5t105 -322.5v-602h-189v608q0 150 -63.5 200t-165.5 50q-125 0 -252 -49v-809h-188z" />
-<glyph unicode="i" horiz-adv-x="589" d="M195 1180v198h200v-198h-200zM201 0v1022h188v-1022h-188z" />
-<glyph unicode="j" horiz-adv-x="526" d="M6 -242q109 0 142.5 47.5t33.5 135.5v1081h187v-1081q0 -342 -363 -342v159zM176 1180v198h199v-198h-199z" />
-<glyph unicode="k" horiz-adv-x="1204" d="M188 0v1411h187v-817l442 428h254l-444 -426l503 -596h-249l-379 475l-127 -123v-352h-187z" />
-<glyph unicode="l" horiz-adv-x="602" d="M207 0v1411h188v-1411h-188z" />
-<glyph unicode="m" horiz-adv-x="1853" d="M193 0v938q205 102 425 102q166 0 273 -77q193 77 375 77q193 0 301 -101t108 -282v-657h-186v670q0 88 -43 142t-168 54q-135 0 -274 -57q24 -65 24 -146v-6v-657h-188v668q0 90 -47.5 142t-188.5 52q-109 0 -223 -31v-831h-188z" />
-<glyph unicode="n" horiz-adv-x="1198" d="M168 0v934q213 106 436 106q193 0 302.5 -101t109.5 -280v-659h-189v672q0 90 -46 142t-185 52q-125 0 -240 -37v-829h-188z" />
-<glyph unicode="o" horiz-adv-x="1161" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5z" />
-<glyph unicode="p" horiz-adv-x="1167" d="M168 -389v1098q0 33 6 65.5t30.5 82.5t66.5 88t126 65.5t195 27.5q57 0 111.5 -10t119 -45t111.5 -91t78.5 -157.5t31.5 -234.5q0 -152 -41.5 -259.5t-114.5 -159.5t-148.5 -74.5t-166.5 -22.5q-113 0 -217 30v-403h-188zM356 193q106 -41 217 -41q63 0 107.5 12t87.5 46 t65.5 106.5t22.5 183.5q0 119 -22.5 197.5t-64.5 114.5t-83 49t-94 13q-236 0 -236 -153v-528z" />
-<glyph unicode="q" horiz-adv-x="1157" d="M115 500q0 133 31.5 234.5t77.5 157.5t110.5 91t119 45t111.5 10q111 0 195 -27.5t126 -65.5t66.5 -88t30.5 -83t6 -65v-1098h-188v403q-104 -30 -217 -30q-90 0 -166 21.5t-147.5 74.5t-113.5 160.5t-42 259.5zM301 500q0 -111 22.5 -183.5t65.5 -106.5t87 -46t108 -12 q113 0 217 41v528q0 153 -236 153q-53 0 -94 -13t-83 -49t-64.5 -115t-22.5 -197z" />
-<glyph unicode="r" horiz-adv-x="702" d="M166 0v936q256 102 498 102v-172q-152 -2 -310 -43v-823h-188z" />
-<glyph unicode="s" horiz-adv-x="892" d="M102 733q0 127 90.5 216t276.5 89q139 0 274 -67v-162q-133 61 -251 61q-203 0 -203 -131q0 -43 54 -71.5t130 -50t152.5 -52t131 -102.5t54.5 -178q0 -123 -93 -212t-288 -89q-164 0 -295 71v170q133 -75 283 -75h4q98 0 149.5 35.5t51.5 90.5q0 63 -53.5 101.5 t-130.5 60t-153.5 48t-130 88t-53.5 159.5z" />
-<glyph unicode="t" horiz-adv-x="765" d="M49 864v158h178v305h187v-305h272v-158h-272v-594q0 -120 151 -120h2q70 0 119 30v-168q-70 -28 -140 -28h-5q-37 0 -79 9t-100.5 35.5t-96.5 92t-38 162.5v581h-178z" />
-<glyph unicode="u" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5z" />
-<glyph unicode="v" horiz-adv-x="1107" d="M66 1022h200l287 -797l289 797h200l-393 -1022h-192z" />
-<glyph unicode="w" horiz-adv-x="1656" d="M80 1022h207l229 -756l225 756h174l228 -756l227 756h207l-338 -1022h-186l-224 711l-225 -711h-186z" />
-<glyph unicode="x" horiz-adv-x="1073" d="M57 0l369 510l-360 512h229l242 -342l241 342h230l-361 -498l369 -524h-230l-249 354l-250 -354h-230z" />
-<glyph unicode="y" horiz-adv-x="1073" d="M63 1022h201l275 -766l270 766h201l-420 -1131q-109 -294 -317 -294h-1q-68 0 -143 16v168q61 -16 115 -17q127 0 205 236z" />
-<glyph unicode="z" horiz-adv-x="962" d="M92 0v172l520 676h-508v174h748v-172l-518 -674h518v-176h-760z" />
-<glyph unicode="{" horiz-adv-x="763" d="M186 535v137q109 31 109 127v313q0 156 82 238t237 82h17v-158q-84 0 -120 -32t-36 -124v-307q0 -123 -119 -207q119 -88 119 -209v-307q0 -92 36 -123t120 -33v-157h-17q-156 0 -237.5 83t-81.5 238v312q0 96 -109 127z" />
-<glyph unicode="|" horiz-adv-x="946" d="M379 -203v1569h188v-1569h-188z" />
-<glyph unicode="}" horiz-adv-x="759" d="M156 -68q84 2 119.5 33t35.5 123v307q0 121 119 209q-119 84 -119 207v307q0 94 -35.5 125t-119.5 31v158h18q154 0 237 -82t83 -238v-313q0 -94 106 -127v-137q-106 -33 -106 -127v-312q0 -156 -83 -238.5t-237 -82.5h-18v157z" />
-<glyph unicode="~" horiz-adv-x="1024" d="M201 473v135q57 68 123 74q11 1 22 1q55 0 106 -25l122 -61q50 -25 103 -25q12 0 23 2q66 6 123 73v-135q-57 -68 -123 -74q-10 -1 -21 -1q-54 0 -105 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -123 -74z" />
-<glyph unicode="&#xa1;" horiz-adv-x="788" d="M299 -344l16 1030h162l17 -1030h-195zM303 848v174h186v-174h-186z" />
-<glyph unicode="&#xa2;" horiz-adv-x="976" d="M117 684q0 238 108.5 373t313.5 151v158h123v-158q131 -12 237 -55v-178q-123 57 -237 65v-712q121 8 237 63v-178q-117 -47 -237 -55v-158h-123v158q-205 16 -313.5 152t-108.5 374zM313 684q0 -182 59.5 -260t166.5 -94v708q-106 -16 -166 -94t-60 -260z" />
-<glyph unicode="&#xa3;" horiz-adv-x="1155" d="M104 -53v182q82 66 181 82v360h-181v168h181v258q0 74 23.5 139.5t71.5 122t134 89t201 32.5q172 0 325 -90v-192q-143 108 -307 108q-264 0 -264 -209v-258h389v-168h-389v-366q45 -8 139 -33t150 -35q34 -6 75 -6q28 0 61 3q79 7 146 44v-182q-86 -47 -186 -47 t-191.5 25.5t-183.5 47.5q-56 14 -114 14q-38 0 -77 -6q-98 -14 -184 -83z" />
-<glyph unicode="&#xa5;" horiz-adv-x="1298" d="M37 1366h229l383 -559l383 559h232l-435 -629h215v-153h-301v-162h301v-154h-301v-268h-188v268h-301v154h301v162h-301v153h215z" />
-<glyph unicode="&#xa7;" horiz-adv-x="1198" d="M131 725q0 160 225 225q-72 55 -71 140q0 125 100 209.5t313 84.5q166 0 322 -77v-181q-145 80 -291 80q-248 0 -248 -125q0 -31 45 -54t112 -39.5t144.5 -43t144 -60.5t111.5 -96.5t45 -144.5q0 -90 -64.5 -145.5t-160.5 -79.5q72 -53 72 -131q0 -125 -101.5 -214 t-314.5 -89q-190 0 -375 106v191q174 -117 348 -117q246 0 246 123q0 31 -45 54.5t-111.5 39.5t-144.5 43t-144.5 60.5t-111.5 96t-45 144.5zM328 723q0 -33 19.5 -59.5t61.5 -46t79.5 -34t101 -34t100.5 -33.5q90 8 143.5 40t53.5 81q0 33 -19.5 58.5t-61.5 46t-80 35 t-101.5 33.5t-100.5 34q-90 -8 -143 -41t-53 -80z" />
-<glyph unicode="&#xa8;" horiz-adv-x="831" d="M162 1155v174h186v-174h-186zM465 1155v174h186v-174h-186z" />
-<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 680q0 289 204 493.5t492.5 204.5t493.5 -204.5t205 -493.5t-205 -492.5t-493.5 -203.5t-492.5 203.5t-204 492.5zM188 680q0 -250 178.5 -428t428.5 -178q252 0 430 178t178 428q0 252 -178 430t-430 178q-250 0 -428.5 -178t-178.5 -430zM432 680q0 121 33 206 t90 127t117.5 59.5t130.5 17.5q127 0 244 -48v-104q-113 51 -232 51q-141 0 -204.5 -72.5t-63.5 -236.5q0 -162 63.5 -234.5t204.5 -72.5q115 0 232 49v-103q-117 -47 -244 -47q-55 0 -103.5 9.5t-99.5 37t-87 71.5t-58.5 118t-22.5 172z" />
-<glyph unicode="&#xaa;" horiz-adv-x="813" d="M156 985q0 84 71.5 132t196.5 48q53 0 102 -6q-4 72 -39.5 103.5t-117.5 31.5q-70 0 -156 -28v88q86 30 172 30q119 0 182.5 -61t63.5 -176v-164q0 -25 -7.5 -49.5t-28 -58t-72.5 -55t-128 -21.5q-63 0 -110 15t-71 36.5t-38 50.5t-17 47.5t-3 36.5zM260 987 q0 -96 135 -96q131 0 131 92v92q-35 8 -98 8q-33 0 -66.5 -6t-67.5 -29.5t-34 -60.5z" />
-<glyph unicode="&#xab;" horiz-adv-x="1171" d="M63 522l347 389h223l-365 -389l365 -391h-223zM498 522l346 389h223l-365 -389l365 -391h-223z" />
-<glyph unicode="&#xac;" horiz-adv-x="1273" d="M199 528v156h876v-360h-151v204h-725z" />
-<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
-<glyph unicode="&#xae;" horiz-adv-x="1593" d="M98 684q0 289 205 494.5t496 205.5t495.5 -205.5t204.5 -494.5q0 -291 -204.5 -495.5t-495.5 -204.5t-496 204.5t-205 495.5zM188 684q0 -254 178.5 -432t432.5 -178q252 0 430 178t178 432q0 252 -178 430t-430 178q-254 0 -432.5 -178t-178.5 -430zM528 285v796h295 q133 0 210 -62.5t77 -178.5q0 -98 -51 -156t-127 -78l211 -321h-131l-193 303h-180v-303h-111zM639 692h178q180 0 180 145.5t-184 145.5h-174v-291z" />
-<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1180v123h477v-123h-477z" />
-<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1143q0 111 57.5 169t166 58t165.5 -58.5t57 -168.5q0 -109 -57 -167t-165.5 -58t-166 58t-57.5 167zM260 1143q0 -76 31 -109t100.5 -33t99 33t29.5 109t-29.5 108.5t-99 32.5t-100.5 -32.5t-31 -108.5z" />
-<glyph unicode="&#xb1;" horiz-adv-x="1214" d="M231 55v150h754v-150h-754zM231 612v150h304v301h149v-301h301v-150h-301v-303h-149v303h-304z" />
-<glyph unicode="&#xb2;" horiz-adv-x="671" d="M109 948v82q137 102 246.5 226t109.5 223q0 118 -161 118h-3q-104 0 -180 -55v96q88 53 194 54q127 0 188.5 -62.5t61.5 -148.5q0 -184 -278 -430h274v-103h-452z" />
-<glyph unicode="&#xb3;" horiz-adv-x="661" d="M109 983v100q86 -49 180 -49q182 0 182 127q0 129 -192 129h-62v92h59q158 0 158 106.5t-158 106.5q-86 0 -159 -41v99q74 39 172 39q127 0 187.5 -59.5t60.5 -143.5q0 -95 -86 -148q122 -53 122 -176q0 -94 -66.5 -159.5t-207.5 -65.5q-104 0 -190 43z" />
-<glyph unicode="&#xb4;" horiz-adv-x="518" d="M123 1155l127 240h184l-194 -240h-117z" />
-<glyph unicode="&#xb5;" horiz-adv-x="1253" d="M203 -430v1454h188v-672q0 -90 46 -142t186 -52q125 0 239 37v829h189v-934q-213 -106 -437 -106q-129 0 -223 47v-461h-188z" />
-<glyph unicode="&#xb6;" horiz-adv-x="940" d="M78 936q0 207 123.5 318.5t359.5 111.5h238v-1366h-170v504h-68q-236 0 -359.5 111.5t-123.5 320.5z" />
-<glyph unicode="&#xb7;" horiz-adv-x="663" d="M238 471v174h186v-174h-186z" />
-<glyph unicode="&#xb8;" horiz-adv-x="555" d="M115 -262l182 252h145l-141 -252h-186z" />
-<glyph unicode="&#xb9;" horiz-adv-x="491" d="M109 1509v107l118 67h113v-735h-102v635z" />
-<glyph unicode="&#xba;" horiz-adv-x="864" d="M156 1090q0 115 44 184t99 89.5t125 20.5q113 0 191.5 -64.5t78.5 -229.5q0 -164 -78.5 -229.5t-191.5 -65.5q-111 0 -189.5 65.5t-78.5 229.5zM260 1089.5q0 -120.5 43 -160.5t121 -40t121 40t43 160.5t-43 161.5t-121 41t-121 -41t-43 -161.5z" />
-<glyph unicode="&#xbb;" horiz-adv-x="1212" d="M137 131l365 391l-365 389h223l347 -389l-347 -391h-223zM571 131l365 391l-365 389h224l346 -389l-346 -391h-224z" />
-<glyph unicode="&#xbc;" horiz-adv-x="1447" d="M106 1206v107l119 67h111v-735h-103v635zM145 -45l893 1448h131l-895 -1448h-129zM799 231v93l323 413h115v-413h104v-93h-104v-229h-102v229h-336zM920 324h215v266z" />
-<glyph unicode="&#xbd;" horiz-adv-x="1482" d="M106 1206v107l119 67h111v-735h-103v635zM145 -45l893 1448h131l-895 -1448h-129zM895 2v82q135 102 244.5 226t109.5 222q0 119 -164 119q-92 0 -180 -55v96q88 53 195 53q127 0 188.5 -61t61.5 -149q0 -188 -279 -431h277v-102h-453z" />
-<glyph unicode="&#xbe;" horiz-adv-x="1662" d="M135 680v100q86 -49 180 -49q182 0 183 127q0 129 -193 129h-61v92h59q160 0 160 106.5t-160 106.5q-86 0 -160 -41v99q82 41 172 41q127 0 187.5 -60.5t60.5 -144.5q0 -95 -86 -148q123 -53 123 -178q0 -92 -66.5 -157.5t-207.5 -65.5q-100 0 -191 43zM369 -45l895 1448 h129l-893 -1448h-131zM1024 231v93l322 413h116v-413h105v-93h-105v-229h-102v229h-336zM1145 324h215v266z" />
-<glyph unicode="&#xbf;" horiz-adv-x="1052" d="M174 37q0 82 31 146.5t76 106.5l90 83q45 41 75.5 100t30.5 135v70h180v-76q0 -104 -30.5 -184t-75.5 -127t-89 -86t-75 -81t-31 -89q0 -219 258 -219q166 0 306 123v-203q-150 -96 -322 -96q-115 0 -200 33.5t-132 92t-69.5 126t-22.5 145.5zM467 836v186h201v-186h-201 z" />
-<glyph unicode="&#xc0;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM424 1722h182l129 -239h-117zM463 561h434l-217 567z" />
-<glyph unicode="&#xc1;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM463 561h434l-217 567zM625 1483l129 239h182l-195 -239h-116z" />
-<glyph unicode="&#xc2;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM451 1483l180 250h98l182 -250h-145l-86 133l-86 -133h-143zM463 561h434l-217 567z" />
-<glyph unicode="&#xc3;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM369 1481v135q57 68 123 74q11 1 22 1q55 0 106 -26l122 -61q51 -24 104 -24q11 0 22 1q66 6 123 74v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2 q-66 -6 -123 -73zM463 561h434l-217 567z" />
-<glyph unicode="&#xc4;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM434 1483v174h189v-174h-189zM463 561h434l-217 567zM737 1483v174h189v-174h-189z" />
-<glyph unicode="&#xc5;" horiz-adv-x="1359" d="M43 0l545 1366h186l543 -1366h-205l-147 385h-570l-147 -385h-205zM463 561h434l-217 567zM502 1599q0 82 47 128.5t131 46.5t131 -46.5t47 -128.5q0 -84 -47 -130t-131 -46t-131 46.5t-47 129.5zM600 1599.5q0 -51.5 17.5 -72t62.5 -20.5t62.5 20.5t17.5 72t-17.5 72 t-62.5 20.5t-62.5 -20.5t-17.5 -72z" />
-<glyph unicode="&#xc6;" horiz-adv-x="2031" d="M51 0l858 1366h983v-178h-700v-416h616v-178h-616v-416h700v-178h-891v414h-481l-258 -414h-211zM633 592h368v588z" />
-<glyph unicode="&#xc7;" horiz-adv-x="1286" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q213 0 418 -81v-177q-193 86 -396 86q-242 0 -351 -124.5t-109 -403.5q0 -276 109.5 -402t350.5 -126q203 0 396 86v-176q-176 -72 -361 -80l-137 -248h-188l184 250q-111 10 -201 47t-173 112.5t-130 213 t-47 323.5z" />
-<glyph unicode="&#xc8;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM412 1722h184l127 -239h-117z" />
-<glyph unicode="&#xc9;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM582 1483l127 239h184l-195 -239h-116z" />
-<glyph unicode="&#xca;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM416 1483l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
-<glyph unicode="&#xcb;" horiz-adv-x="1187" d="M178 0v1366h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891zM397 1483v174h187v-174h-187zM700 1483v174h187v-174h-187z" />
-<glyph unicode="&#xcc;" horiz-adv-x="544" d="M14 1722h185l127 -239h-117zM178 0h189v1366h-189v-1366z" />
-<glyph unicode="&#xcd;" horiz-adv-x="544" d="M178 0v1366h189v-1366h-189zM217 1483l127 239h184l-194 -239h-117z" />
-<glyph unicode="&#xce;" horiz-adv-x="544" d="M41 1483l180 250h98l183 -250h-146l-86 133l-86 -133h-143zM178 0h189v1366h-189v-1366z" />
-<glyph unicode="&#xcf;" horiz-adv-x="544" d="M27 1483v174h186v-174h-186zM178 0h189v1366h-189v-1366zM330 1483v174h186v-174h-186z" />
-<glyph unicode="&#xd0;" horiz-adv-x="1480" d="M109 606v154h163v606h437q315 0 479 -174t164 -510t-164 -509t-479 -173h-437v606h-163zM463 168h215q252 0 368.5 124t116.5 390q0 268 -116.5 392t-368.5 124h-215v-438h305v-154h-305v-438z" />
-<glyph unicode="&#xd1;" horiz-adv-x="1511" d="M178 0v1366h217l764 -1087v1087h191v-1366h-211l-770 1094v-1094h-191zM453 1481v135q57 68 123 74q11 1 22 1q55 0 106 -26l122 -61q51 -24 104 -24q11 0 22 1q66 6 123 74v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2 q-66 -6 -123 -73z" />
-<glyph unicode="&#xd2;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM524 1722h185l127 -239h-117z" />
-<glyph unicode="&#xd3;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM684 1483l127 239h184l-194 -239h-117z" />
-<glyph unicode="&#xd4;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM528 1483l181 250h98l182 -250h-145l-86 133l-86 -133h-144z" />
-<glyph unicode="&#xd5;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM449 1481v135q57 68 123 74q11 1 22 1q55 0 106 -26l122 -61q51 -24 104 -24 q11 0 22 1q66 6 123 74v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -105 26q-11 0 -23 -2q-66 -6 -123 -73z" />
-<glyph unicode="&#xd6;" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16t177 -16t170 -63.5t148.5 -123t100.5 -202.5t39 -295t-39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16t-177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5 t103.5 -88t117.5 -42t126.5 -9t127 9t118 42t103.5 88t68.5 154.5t27.5 234.5t-27.5 234.5t-68.5 154.5t-103.5 88t-118 42t-127 9t-126.5 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM514 1483v174h186v-174h-186zM817 1483v174h187v-174h-187z" />
-<glyph unicode="&#xd7;" horiz-adv-x="1050" d="M158 160l282 364l-282 365h164l200 -258l203 258h162l-281 -365l281 -364h-162l-203 258l-200 -258h-164z" />
-<glyph unicode="&#xd8;" d="M88 0l168 201q-131 174 -131 483q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q244 0 399 -112l82 94h191l-168 -199q131 -174 131 -483q0 -168 -39 -295t-100.5 -202.5t-148.5 -123t-170 -63.5t-177 -16q-244 0 -400 112l-81 -96h-191zM315 684q0 -209 64 -336 l665 789q-99 75 -279 75h-5q-72 0 -127 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5zM475 231q97 -75 280 -75h5q72 0 127 9t117.5 42t103.5 88t68.5 154.5t27.5 234.5q0 209 -63 336z" />
-<glyph unicode="&#xd9;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM498 1722h184l127 -239h-117z" />
-<glyph unicode="&#xda;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM666 1483l127 239h184l-195 -239h-116z" />
-<glyph unicode="&#xdb;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM506 1483l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
-<glyph unicode="&#xdc;" horiz-adv-x="1476" d="M178 563v803h191v-819q0 -201 90 -297t278.5 -96t278.5 96t90 297v819h190v-803q0 -276 -146 -428.5t-412.5 -152.5t-413 152.5t-146.5 428.5zM492 1483v174h186v-174h-186zM795 1483v174h186v-174h-186z" />
-<glyph unicode="&#xdd;" horiz-adv-x="1275" d="M25 1366h229l383 -559l383 559h231l-520 -754v-612h-188v610zM561 1483l127 239h184l-194 -239h-117z" />
-<glyph unicode="&#xde;" horiz-adv-x="1187" d="M176 0v1366h191v-205h231q227 0 352 -112.5t125 -317.5t-125 -317.5t-352 -112.5h-231v-301h-191zM367 469h215q182 0 243.5 70.5t61.5 189.5v2v2q0 119 -61.5 189.5t-243.5 70.5h-215v-524z" />
-<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M180 0v862q0 154 39 262.5t106.5 161.5t137 75.5t153.5 22.5q236 0 347.5 -114.5t111.5 -276.5q0 -188 -188 -280q254 -74 254 -316q0 -186 -150.5 -291.5t-400.5 -105.5v174q61 0 115.5 10.5t113 33t93 70.5t34.5 118q0 217 -356 231v143q293 4 293 219q0 219 -267 220 q-53 0 -92 -13.5t-75.5 -49.5t-56 -111.5t-19.5 -190.5v-854h-193z" />
-<glyph unicode="&#xe0;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM315 1395h185l127 -240h-117z" />
-<glyph unicode="&#xe1;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM461 1155l127 240h184l-194 -240h-117z" />
-<glyph unicode="&#xe2;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM338 1155l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
-<glyph unicode="&#xe3;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M250 1153v135q57 68 124 74q10 1 21 1q55 0 105 -25l122 -61q51 -25 104 -25q12 0 24 2q65 6 122 73v-135q-57 -68 -122 -74q-12 -1 -23 -1q-53 0 -105 25l-122 60q-50 26 -105 26q-11 0 -21 -1q-67 -6 -124 -74zM301 324q0 -174 246 -174h3q232 0 232 167v166 q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88z" />
-<glyph unicode="&#xe4;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM324 1155v174h186v-174h-186zM627 1155v174h186v-174h-186z" />
-<glyph unicode="&#xe5;" horiz-adv-x="1099" d="M115 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q159 55 309 55q213 0 328 -110.5t115 -317.5v-295q0 -33 -5.5 -65.5t-31 -83.5t-67.5 -89t-125 -65.5t-193 -27.5q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67z M301 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM379 1272q0 84 47 130t131 46q86 0 132 -46t46 -130t-46 -130t-132 -46q-84 0 -131 46t-47 130zM477 1272q0 -51 17.5 -71.5t62.5 -20.5t62.5 20.5t17.5 71.5 t-17.5 71.5t-62.5 20.5t-62.5 -20.5t-17.5 -71.5z" />
-<glyph unicode="&#xe6;" horiz-adv-x="1802" d="M111 322q0 152 128 237.5t353 85.5q96 0 186 -12q-6 129 -70.5 185t-211.5 56q-133 0 -283 -51v160q160 55 309 55q244 0 357 -143q113 143 348 143q205 0 327.5 -134t122.5 -359q0 -74 -6 -142h-704q35 -260 288 -260q197 0 353 76v-170q-158 -67 -357 -67h-6 q-229 0 -350 131q-106 -129 -352 -129q-113 0 -198 28.5t-128 66.5t-68.5 90t-31.5 86t-6 67zM297 324q0 -174 246 -174h3q232 0 232 167v166q-74 12 -178 13q-41 0 -86 -7.5t-97 -23.5t-86 -53t-34 -88zM965 573h524q-27 297 -262 297q-129 0 -189.5 -66.5t-72.5 -230.5z " />
-<glyph unicode="&#xe7;" horiz-adv-x="966" d="M119 512q0 256 124 391t357 135q156 0 301 -57v-178q-142 67 -285 67q-141 0 -221 -72.5t-80 -285.5q0 -113 23.5 -187.5t70 -110.5t94.5 -48t113 -12q147 0 285 65v-178q-104 -41 -223 -53l-139 -254h-189l182 254q-201 18 -307 152t-106 372z" />
-<glyph unicode="&#xe8;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM346 1395h184l127 -240h-116z" />
-<glyph unicode="&#xe9;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM506 1155l127 240h184l-194 -240h-117z" />
-<glyph unicode="&#xea;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM350 1155l180 250h99l182 -250h-145l-86 133l-86 -133h-144z" />
-<glyph unicode="&#xeb;" horiz-adv-x="1136" d="M129 500q0 540 451 540h2q205 0 327.5 -134t122.5 -359q0 -74 -6 -141h-704q35 -260 288 -261q201 0 353 74v-168q-158 -67 -357 -67h-6q-90 0 -166 22.5t-148.5 74.5t-114.5 159.5t-42 259.5zM319 575h525q-4 51 -17.5 97.5t-41 94.5t-80 76.5t-123.5 28.5 q-129 0 -190 -66.5t-73 -230.5zM336 1155v174h186v-174h-186zM639 1155v174h186v-174h-186z" />
-<glyph unicode="&#xec;" horiz-adv-x="589" d="M41 1395h184l127 -240h-116zM201 0h188v1022h-188v-1022z" />
-<glyph unicode="&#xed;" horiz-adv-x="589" d="M201 0v1022h188v-1022h-188zM236 1155l126 240h185l-195 -240h-116z" />
-<glyph unicode="&#xee;" horiz-adv-x="589" d="M63 1155l181 250h98l182 -250h-145l-86 133l-86 -133h-144zM201 0h188v1022h-188v-1022z" />
-<glyph unicode="&#xef;" horiz-adv-x="589" d="M49 1155v174h187v-174h-187zM201 0h188v1022h-188v-1022zM352 1155v174h187v-174h-187z" />
-<glyph unicode="&#xf0;" horiz-adv-x="1212" d="M119 453q0 182 108.5 310t347.5 128q170 0 285 -72q-41 193 -162 293l-217 -131l-67 113l151 90q-63 22 -149 22h-8q-74 0 -156 -18v172q81 22 178 22h8q184 0 322 -79l178 108l68 -113l-136 -81q193 -197 193 -590q0 -643 -473 -643q-229 0 -350 138t-121 331zM309 453 q0 -43 9.5 -85t35 -94.5t87 -85t149.5 -32.5q39 0 64.5 3t70.5 26.5t72.5 68.5t50 138t24.5 224q-106 100 -282 101q-281 0 -281 -264z" />
-<glyph unicode="&#xf1;" horiz-adv-x="1198" d="M168 0v934q213 106 436 106q193 0 302.5 -101t109.5 -280v-659h-189v672q0 90 -46 142t-185 52q-125 0 -240 -37v-829h-188zM281 1153v135q57 68 123 74q11 1 22 1q55 0 106 -25l122 -61q50 -25 103 -25q12 0 23 2q66 6 123 73v-135q-57 -68 -123 -74q-11 -1 -22 -1 q-53 0 -104 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -123 -74z" />
-<glyph unicode="&#xf2;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM319 1395h185l127 -240h-117z" />
-<glyph unicode="&#xf3;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM532 1155l127 240h185l-195 -240h-117z" />
-<glyph unicode="&#xf4;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM350 1155l180 250h99l182 -250h-145l-86 133l-86 -133h-144z" />
-<glyph unicode="&#xf5;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM270 1153v135q57 68 124 74q11 1 22 1q55 0 106 -25l122 -61 q50 -25 103 -25q12 0 23 2q66 6 123 73v-135q-57 -68 -123 -74q-11 -1 -22 -1q-53 0 -104 25l-122 60q-51 26 -106 26q-11 0 -22 -1q-66 -6 -124 -74zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5 t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5z" />
-<glyph unicode="&#xf6;" horiz-adv-x="1155" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170 22.5t170 -22.5t151.5 -76.5t117 -162.5t43 -264.5t-43 -264.5t-117 -162.5t-151.5 -76.5t-170 -22.5t-170 22.5t-151.5 76.5t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12 t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM336 1155v174h186v-174h-186zM639 1155v174h186v-174h-186z" />
-<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M227 453v151h832v-151h-832zM549 143v174h186v-174h-186zM549 739v174h186v-174h-186z" />
-<glyph unicode="&#xf8;" horiz-adv-x="1175" d="M55 0l135 160q-90 131 -90 350q0 156 43 264.5t117 162.5t151.5 76.5t170.5 22.5q170 0 284 -73l52 59h190l-135 -162q90 -131 90 -350q0 -156 -43 -264.5t-117 -162.5t-151.5 -76.5t-169.5 -22.5q-172 0 -287 73l-49 -57h-191zM289 510q0 -119 26 -199l441 520 q-61 39 -174 39q-66 0 -112 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM406 188q59 -38 170 -38h6q66 0 111.5 12t90.5 48t67.5 110.5t22.5 189.5q0 125 -28 201z" />
-<glyph unicode="&#xf9;" horiz-adv-x="1208" d="M168 430v592h188v-606q0 -139 58.5 -204t185.5 -65t186.5 64.5t59.5 204.5v606h186v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM358 1395h185l127 -240h-117z" />
-<glyph unicode="&#xfa;" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM528 1155l127 240h185l-195 -240h-117z" />
-<glyph unicode="&#xfb;" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM369 1155l180 250h98l182 -250h-145l-86 133l-86 -133h-143z" />
-<glyph unicode="&#xfc;" horiz-adv-x="1208" d="M168 430v592h186v-606q0 -139 59.5 -204t186.5 -65t185.5 64.5t58.5 204.5v606h188v-592q0 -213 -113.5 -330.5t-318.5 -117.5t-318.5 117.5t-113.5 330.5zM354 1155v174h187v-174h-187zM657 1155v174h187v-174h-187z" />
-<glyph unicode="&#xfd;" horiz-adv-x="1073" d="M63 1022h201l275 -766l270 766h201l-420 -1131q-109 -294 -317 -294h-1q-68 0 -143 16v168q61 -16 115 -17q127 0 205 236zM467 1155l127 240h184l-194 -240h-117z" />
-<glyph unicode="&#xfe;" horiz-adv-x="1142" d="M170 -377v1774h186v-404q106 31 219 31q88 0 164 -21.5t149 -75t114.5 -159.5t41.5 -258t-41.5 -258.5t-114.5 -159.5t-148.5 -74.5t-164.5 -21.5q-113 0 -219 31v-404h-186zM356 205q104 -43 219 -43q61 0 106.5 12t88.5 46t65.5 106.5t22.5 183.5t-22.5 183.5 t-65.5 106.5t-88 46t-107 12q-115 0 -219 -43v-610z" />
-<glyph unicode="&#xff;" horiz-adv-x="1069" d="M63 1022h201l275 -766l270 766h201l-420 -1131q-109 -294 -317 -294h-1q-68 0 -143 16v168q61 -16 115 -17q127 0 205 236zM291 1155v174h186v-174h-186zM594 1155v174h186v-174h-186z" />
-<glyph unicode="&#x152;" horiz-adv-x="2056" d="M125 684q0 168 39 295t100.5 202.5t148.5 123t170 63.5t177 16q152 0 272 -45v27h891v-178h-700v-416h616v-178h-616v-416h700v-178h-891v29q-121 -45 -272 -45q-94 0 -177 16t-170 63.5t-148.5 123t-100.5 202.5t-39 295zM315 684q0 -135 28 -234.5t69 -154.5t103.5 -88 t117.5 -42t127 -9q174 0 272 67v922q-97 67 -267 67h-5q-72 0 -127 -9t-117.5 -42t-103.5 -88t-69 -154.5t-28 -234.5z" />
-<glyph unicode="&#x153;" horiz-adv-x="1886" d="M100 510q0 156 43 264.5t117 162.5t151.5 76.5t170.5 22.5q256 0 381 -159q112 163 359 163h3q205 0 328 -134t123 -359q0 -74 -7 -141h-704q35 -260 289 -261q201 0 352 74v-168q-158 -67 -357 -67h-6q-260 0 -378 166q-121 -166 -383 -166q-92 0 -170 22.5t-152 76.5 t-117 162.5t-43 264.5zM289 510q0 -115 22.5 -189.5t67.5 -110.5t91 -48t111.5 -12t111.5 12t91 48t67.5 110.5t22.5 189.5t-22.5 189.5t-67.5 110.5t-91 48t-111.5 12t-111.5 -12t-91 -48t-67.5 -110.5t-22.5 -189.5zM1063 575h524q-27 297 -262 297q-129 0 -189.5 -66.5 t-72.5 -230.5z" />
-<glyph unicode="&#x178;" horiz-adv-x="1277" d="M25 1366h229l383 -559l383 559h231l-520 -754v-612h-188v610zM393 1483v174h187v-174h-187zM696 1483v174h187v-174h-187z" />
-<glyph unicode="&#x2c6;" horiz-adv-x="882" d="M207 1155l180 250h98l183 -250h-146l-86 133l-86 -133h-143z" />
-<glyph unicode="&#x2dc;" horiz-adv-x="1079" d="M215 1153v135q57 68 124 74q11 1 22 1q55 0 105 -25l122 -61q50 -25 103 -25q12 0 23 2q66 6 124 73v-135q-57 -68 -123 -74q-11 -1 -22 -1q-54 0 -105 25l-122 60q-50 26 -105 26q-11 0 -22 -1q-67 -6 -124 -74z" />
-<glyph unicode="&#x2000;" horiz-adv-x="887" />
-<glyph unicode="&#x2001;" horiz-adv-x="1774" />
-<glyph unicode="&#x2002;" horiz-adv-x="887" />
-<glyph unicode="&#x2003;" horiz-adv-x="1774" />
-<glyph unicode="&#x2004;" horiz-adv-x="591" />
-<glyph unicode="&#x2005;" horiz-adv-x="443" />
-<glyph unicode="&#x2006;" horiz-adv-x="295" />
-<glyph unicode="&#x2007;" horiz-adv-x="295" />
-<glyph unicode="&#x2008;" horiz-adv-x="221" />
-<glyph unicode="&#x2009;" horiz-adv-x="354" />
-<glyph unicode="&#x200a;" horiz-adv-x="98" />
-<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
-<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
-<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 530v160h624v-160h-624z" />
-<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 516v160h860v-160h-860z" />
-<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 516v160h1501v-160h-1501z" />
-<glyph unicode="&#x2018;" horiz-adv-x="552" d="M96 1384h193l151 -401h-172z" />
-<glyph unicode="&#x2019;" horiz-adv-x="552" d="M96 983l152 401h192l-172 -401h-172z" />
-<glyph unicode="&#x201a;" horiz-adv-x="643" d="M147 -229l152 401h193l-173 -401h-172z" />
-<glyph unicode="&#x201c;" horiz-adv-x="843" d="M96 1384h193l151 -401h-172zM401 1384h193l151 -401h-172z" />
-<glyph unicode="&#x201d;" horiz-adv-x="827" d="M96 983l152 401h192l-172 -401h-172zM401 983l152 401h192l-172 -401h-172z" />
-<glyph unicode="&#x201e;" horiz-adv-x="940" d="M147 -229l152 401h193l-173 -401h-172zM453 -229l151 401h193l-172 -401h-172z" />
-<glyph unicode="&#x2022;" horiz-adv-x="864" d="M170 684q0 104 73.5 177t178.5 73q102 0 176 -73t74 -177t-74 -178t-176 -74q-104 0 -178 74t-74 178z" />
-<glyph unicode="&#x2026;" horiz-adv-x="1607" d="M219 0v174h187v-174h-187zM711 0v174h186v-174h-186zM1200 0v174h186v-174h-186z" />
-<glyph unicode="&#x202f;" horiz-adv-x="354" />
-<glyph unicode="&#x2039;" horiz-adv-x="733" d="M63 522l347 389h223l-365 -389l365 -391h-223z" />
-<glyph unicode="&#x203a;" horiz-adv-x="792" d="M137 131l365 391l-365 389h223l347 -389l-347 -391h-223z" />
-<glyph unicode="&#x205f;" horiz-adv-x="443" />
-<glyph unicode="&#x20ac;" horiz-adv-x="1400" d="M102 451v141h142q-2 29 -2 92q0 33 4 94h-144v142h162q29 135 94.5 231t153.5 144t176 68.5t189 20.5q213 0 417 -81v-177q-192 86 -395 86q-184 0 -290.5 -70.5t-145.5 -221.5h614v-142h-635q-4 -61 -4 -94q0 -63 4 -92h635v-141h-614q39 -154 145.5 -224.5t290.5 -70.5 q203 0 395 86v-176q-205 -82 -417 -82q-512 0 -613 467h-162z" />
-<glyph unicode="&#x2122;" horiz-adv-x="1568" d="M100 1278v88h555v-88h-231v-594h-94v594h-230zM762 684v682h102l223 -295l226 295h102v-682h-94v539l-234 -308l-231 308v-539h-94z" />
-<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
-<glyph unicode="&#xfb01;" horiz-adv-x="1202" d="M82 858v166h143v47q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-47h264v-166h-264v-858h-187v858h-143zM874 1180v198h201v-198h-201zM881 0v1022h188v-1022h-188z" />
-<glyph unicode="&#xfb02;" horiz-adv-x="1220" d="M82 858v166h143v47q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-47h264v-166h-264v-858h-187v858h-143zM881 0v1411h188v-1411h-188z" />
-<glyph unicode="&#xfb03;" horiz-adv-x="1939" d="M82 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM757 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5 t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM1545 1180v198h200v-198h-200zM1551 0v1022h188v-1022h-188z" />
-<glyph unicode="&#xfb04;" horiz-adv-x="1952" d="M82 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM757 858v164h143v49q0 381 340 381q57 0 111 -14v-162q-41 8 -76 8q-47 0 -71.5 -4t-56.5 -22.5t-46 -64.5 t-14 -122v-49h264v-164h-264v-858h-187v858h-143zM1557 0v1411h188v-1411h-188z" />
-</font>
-</defs></svg> 
\ No newline at end of file
diff --git a/static/fnt/mavenpro-medium-webfont.ttf b/static/fnt/mavenpro-medium-webfont.ttf
deleted file mode 100644 (file)
index a73e903..0000000
Binary files a/static/fnt/mavenpro-medium-webfont.ttf and /dev/null differ
diff --git a/static/fnt/mavenpro-medium-webfont.woff b/static/fnt/mavenpro-medium-webfont.woff
deleted file mode 100644 (file)
index 9a0ed4d..0000000
Binary files a/static/fnt/mavenpro-medium-webfont.woff and /dev/null differ
diff --git a/static/fnt/mavenpro-regular-webfont.eot b/static/fnt/mavenpro-regular-webfont.eot
deleted file mode 100644 (file)
index b41ecdd..0000000
Binary files a/static/fnt/mavenpro-regular-webfont.eot and /dev/null differ
diff --git a/static/fnt/mavenpro-regular-webfont.svg b/static/fnt/mavenpro-regular-webfont.svg
deleted file mode 100644 (file)
index 074ad9f..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
-<svg xmlns="http://www.w3.org/2000/svg">
-<metadata></metadata>
-<defs>
-<font id="maven_proregular" horiz-adv-x="1529" >
-<font-face units-per-em="2048" ascent="1638" descent="-410" />
-<missing-glyph horiz-adv-x="692" />
-<glyph horiz-adv-x="2048" />
-<glyph horiz-adv-x="2048" />
-<glyph unicode="&#xd;" horiz-adv-x="682" />
-<glyph unicode=" "  horiz-adv-x="692" />
-<glyph unicode="&#x09;" horiz-adv-x="692" />
-<glyph unicode="&#xa0;" horiz-adv-x="692" />
-<glyph unicode="!" horiz-adv-x="761" d="M303 0v164h164v-164h-164zM311 1366h148l-15 -1022h-118z" />
-<glyph unicode="&#x22;" horiz-adv-x="780" d="M166 1384h170l-35 -411h-98zM440 1384h170l-37 -411h-98z" />
-<glyph unicode="#" horiz-adv-x="1210" d="M145 258v123h189l55 326h-186v122h209l41 242h125l-41 -242h270l43 242h125l-43 -242h147v-122h-168l-57 -326h166v-123h-186l-37 -219h-125l37 219h-271l-39 -219h-125l39 219h-168zM459 381h270l57 326h-272z" />
-<glyph unicode="$" horiz-adv-x="1181" d="M154 1004q0 143 95 249.5t294 126.5v209h110v-205q166 -8 314 -81v-152q-150 84 -314 86v-469q88 -29 147.5 -55.5t124 -73.5t97.5 -120t33 -171q0 -156 -105.5 -250t-296.5 -110v-209h-110v205q-207 6 -377 96v160q158 -104 377 -109v520q-76 23 -117 37t-101.5 44 t-92 63.5t-55 87t-23.5 121.5zM301 1004q0 -78 61.5 -121t180.5 -80v428q-242 -34 -242 -227zM653 135q254 33 254 213q0 106 -65.5 165.5t-188.5 102.5v-481z" />
-<glyph unicode="%" horiz-adv-x="1777" d="M145 983q0 111 27 189.5t73 116.5t93 54.5t104 16.5q123 0 209 -83t86 -294q0 -113 -26.5 -190.5t-71.5 -116.5t-93 -55.5t-104 -16.5q-57 0 -104 16.5t-93 55.5t-73 116.5t-27 190.5zM258 983q0 -162 50 -217t134 -55t133.5 55t49.5 217t-49.5 216t-133.5 54 q-86 0 -135 -54t-49 -216zM395 0l858 1366h131l-858 -1366h-131zM1042 385q0 113 27 190.5t73 116.5t93 55.5t104 16.5q55 0 103.5 -16.5t93.5 -55.5t71.5 -116.5t26.5 -190.5t-26.5 -190.5t-71.5 -116.5t-93.5 -55.5t-103.5 -16.5q-57 0 -104 16.5t-93 55.5t-73 116.5 t-27 190.5zM1155 385q0 -162 50 -217t134 -55t133.5 55t49.5 217t-49.5 217t-133.5 55t-134 -55t-50 -217z" />
-<glyph unicode="&#x26;" horiz-adv-x="1310" d="M111 377q0 119 56 220t220 191q-135 168 -135 283q0 127 91 218t218 91q131 0 221 -87t90 -214q0 -109 -59 -183.5t-192 -152.5l297 -354q57 100 83 205h134q-33 -177 -121 -322l227 -272h-192l-125 152q-162 -168 -385 -168q-111 0 -198 38.5t-134 101t-71.5 127 t-24.5 126.5zM256 381q0 -33 12.5 -71t42 -82t90 -72.5t144.5 -28.5q172 0 291 129l-357 428q-78 -41 -128 -85t-68.5 -87t-22.5 -67.5t-4 -63.5zM395 1069q0 -72 129 -213q131 70 171 116t40 105q0 68 -49 112t-121 44q-70 0 -120 -48t-50 -116z" />
-<glyph unicode="'" horiz-adv-x="509" d="M166 1384h170l-35 -411h-98z" />
-<glyph unicode="(" horiz-adv-x="706" d="M190 625q1 450 228 858h155q-29 -47 -59.5 -112.5t-74.5 -180.5t-72.5 -264.5t-28.5 -300.5q0 -150 28.5 -300.5t72.5 -264t76 -180t58 -111.5h-155q-227 408 -228 856z" />
-<glyph unicode=")" horiz-adv-x="706" d="M133 -231q29 47 59.5 111.5t75.5 180t74 265t29 301.5q0 195 -53.5 396.5t-98.5 296.5t-86 163h158q227 -408 227 -856q0 -451 -227 -858h-158z" />
-<glyph unicode="*" horiz-adv-x="841" d="M172 1112l178 78l-178 80l55 94l156 -111l-21 187h111l-16 -187l153 111l56 -94l-177 -80l177 -78l-56 -98l-153 112l16 -184h-111l21 184l-156 -112z" />
-<glyph unicode="+" horiz-adv-x="1128" d="M199 551v123h303v303h123v-303h301v-123h-301v-301h-123v301h-303z" />
-<glyph unicode="," horiz-adv-x="641" d="M152 -229l151 401h168l-172 -401h-147z" />
-<glyph unicode="-" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
-<glyph unicode="." horiz-adv-x="671" d="M260 0v164h164v-164h-164z" />
-<glyph unicode="/" horiz-adv-x="827" d="M-29 -201l752 1749h135l-754 -1749h-133z" />
-<glyph unicode="0" horiz-adv-x="1345" d="M133 677.5q0 700.5 538.5 700.5t538.5 -700.5t-538.5 -700.5t-538.5 700.5zM281 678q0 -145 23.5 -250.5t60 -165t92 -94.5t104.5 -45t110.5 -10t110.5 10t104.5 45t92.5 94.5t60.5 164.5t23.5 251q0 178 -34 298t-95.5 174t-121.5 73.5t-140 19.5t-140.5 -19.5 t-122 -73.5t-95 -174t-33.5 -298z" />
-<glyph unicode="1" horiz-adv-x="923" d="M211 1032v174l252 160h168v-1366h-148v1212z" />
-<glyph unicode="2" horiz-adv-x="1189" d="M158 0v129q102 76 208.5 169t223.5 215t189.5 252t72.5 234q0 238 -322 238q-190 0 -358 -119v162q172 104 371 104q233 0 344.5 -112.5t111.5 -272.5q0 -111 -56 -232.5t-151.5 -236t-190 -206t-200.5 -177.5h619v-147h-862z" />
-<glyph unicode="3" horiz-adv-x="1193" d="M166 70v151q160 -90 346 -90q182 0 278.5 67.5t96.5 194.5t-98.5 194.5t-284.5 67.5h-150v142h156q150 0 228.5 56t78.5 162.5t-83 164t-240 57.5q-152 0 -295 -72v150q145 69 306 69h5q233 0 344 -108.5t111 -259.5q0 -172 -148 -260l-29 -17q10 -4 26.5 -12t58.5 -39 t75 -67.5t59.5 -99t26.5 -132.5q0 -78 -27.5 -147.5t-84 -129t-157.5 -94t-235 -34.5q-197 0 -364 86z" />
-<glyph unicode="4" horiz-adv-x="1210" d="M78 410v157l645 799h166v-819h194v-137h-194v-410h-148v410h-663zM242 547h499v608z" />
-<glyph unicode="5" horiz-adv-x="1202" d="M178 735l70 631h702v-145h-575l-39 -406q121 37 266 37q84 0 153.5 -18.5t136 -63.5t105.5 -135t39 -217t-39 -217t-105.5 -135.5t-136 -63.5t-153.5 -18q-236 0 -391 84v153q150 -94 381 -94q80 0 138 19.5t89 46t47.5 69.5t20.5 77t4 79q0 92 -24.5 152.5t-71.5 88 t-93.5 37t-109.5 9.5q-96 0 -177 -16.5t-112 -33.5l-33 -14z" />
-<glyph unicode="6" horiz-adv-x="1193" d="M133 625q0 141 25.5 257.5t65.5 193.5t96.5 135.5t111.5 90t118.5 51t111 25.5t94.5 6q109 0 198 -26v-137q-98 24 -176 24h-6q-47 0 -92 -6t-113.5 -32.5t-122 -73t-97.5 -136.5t-58 -213q133 90 309 90q215 0 340 -115.5t125 -314.5q0 -84 -25.5 -162.5t-78 -147 t-145.5 -109.5t-216 -41q-465 0 -465 641zM281 621q2 -141 27.5 -241.5t57 -149t83 -75t79 -29.5t70.5 -3q102 0 172 38t98.5 98t37.5 103.5t9 83.5q0 139 -81.5 215t-235.5 76q-201 0 -317 -116z" />
-<glyph unicode="7" horiz-adv-x="1175" d="M143 1219v147h932v-137l-592 -1229h-164l596 1219h-772z" />
-<glyph unicode="8" horiz-adv-x="1191" d="M125 406q0 203 184 307q51 25 51 24l-20 11q-12 6 -44 30.5t-56.5 55t-44 84t-19.5 112.5q0 145 103.5 249.5t318.5 104.5t318.5 -103t103.5 -251q0 -184 -148 -274l-30 -17q10 -4 25.5 -11t54.5 -34.5t69.5 -62.5t55 -95.5t24.5 -129.5q0 -78 -25.5 -149t-79 -134.5 t-148.5 -101t-220 -37.5q-240 0 -356.5 127t-116.5 295zM272 406q0 -27 4.5 -55.5t23.5 -71.5t52 -76t96.5 -56.5t149.5 -23.5t149.5 23.5t96.5 56.5t52 76t23.5 71.5t4.5 55.5q0 123 -84 188t-242 65t-242 -65.5t-84 -187.5zM324 1030q0 -111 72.5 -173t201.5 -62 t201.5 62.5t72.5 172.5q0 209 -274 209t-274 -209z" />
-<glyph unicode="9" horiz-adv-x="1202" d="M119 924q0 84 25.5 162.5t77.5 147.5t145.5 109.5t216.5 40.5q465 0 465 -643q0 -195 -48.5 -340t-116 -223t-160.5 -124t-160.5 -58t-137.5 -12q-104 0 -199 24v139q81 -24 175 -24h8q47 0 92 6t113.5 32.5t120.5 73t97.5 136.5t57.5 213q-121 -90 -307 -90 q-215 0 -340 115.5t-125 314.5zM266 922q0 -139 82 -215t236 -76q207 0 317 117q-2 141 -27.5 241.5t-57.5 148.5t-83 74.5t-78.5 29.5t-70.5 3q-102 0 -172 -38t-98.5 -98t-38 -103t-9.5 -84z" />
-<glyph unicode=":" horiz-adv-x="800" d="M324 98v164h163v-164h-163zM324 754v164h163v-164h-163z" />
-<glyph unicode=";" horiz-adv-x="854" d="M199 -229l151 401h168l-172 -401h-147zM352 754v164h164v-164h-164z" />
-<glyph unicode="&#x3c;" horiz-adv-x="1351" d="M246 481v140l844 383v-164l-672 -289l672 -291v-162z" />
-<glyph unicode="=" horiz-adv-x="1306" d="M260 381v143h789v-143h-789zM260 698v142h789v-142h-789z" />
-<glyph unicode="&#x3e;" horiz-adv-x="1329" d="M270 98v162l670 291l-670 289v164l842 -383v-140z" />
-<glyph unicode="?" horiz-adv-x="993" d="M164 1145v160q113 77 285 77q61 0 120.5 -14t129 -50t112.5 -117t43 -197q0 -82 -31.5 -142.5t-76.5 -98.5l-91 -76q-45 -38 -76.5 -101.5t-31.5 -151.5v-90h-148v100q0 104 32 181t77 120t90 78t77 79t32 102q0 231 -269 231q-156 0 -274 -90zM391 0v164h164v-164h-164z " />
-<glyph unicode="@" horiz-adv-x="1869" d="M170 371q0 244 133 445.5t334 309t410 107.5q82 0 171 -21.5t180 -73t162.5 -126t116.5 -189t45 -254.5q0 -221 -91 -362t-242 -141q-137 0 -191 100q-96 -100 -295 -100q-143 0 -225 62t-82 171q0 49 16.5 94t59.5 94.5t138 79t230 29.5q74 0 152 -10q2 14 2 43 q0 35 -6 59.5t-23.5 50t-60.5 39t-109 13.5q-143 0 -256 -43l17 118q123 41 239 41q156 0 237 -70.5t81 -207.5q0 -59 -11.5 -142.5t-17.5 -137.5q-4 -40 -4 -81v-26q2 -54 30.5 -79.5t86.5 -25.5q90 0 169 114t79 312q0 145 -55.5 262t-144.5 186.5t-191.5 106.5t-206.5 37 q-311 0 -554 -230.5t-243 -553.5q0 -154 51 -270.5t123 -177t163 -98.5t153.5 -47t113.5 -9q254 0 430 100l-18 -92q-172 -82 -428 -82q-86 0 -174.5 22.5t-178.5 74.5t-158.5 127t-112.5 192.5t-44 259.5zM715 300q0 -116 188 -116q49 0 88 8.5t65.5 17.5t47 31.5t31 37 t18.5 46t11 46t6.5 51.5t5.5 47q-55 8 -136 8q-325 0 -325 -177z" />
-<glyph unicode="A" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625z" />
-<glyph unicode="B" horiz-adv-x="1271" d="M178 0v1366h453q454 0 454 -365q0 -57 -15 -106t-36.5 -78t-42 -50.5t-37.5 -29.5l-14 -10q10 -2 24.5 -8t54.5 -33t69.5 -61.5t54 -98t24.5 -139.5q0 -387 -495 -387h-494zM326 137h358q156 0 246 54.5t90 203.5q0 63 -21.5 108.5t-52.5 71t-80 39t-91 17.5t-97 4h-352 v-498zM326 774h338q125 0 199.5 43t74.5 172q0 133 -72.5 184.5t-220.5 51.5h-319v-451z" />
-<glyph unicode="C" horiz-adv-x="1286" d="M125 684q0 700 633 700q219 0 416 -79v-142q-195 82 -400 82q-260 0 -381 -135t-121 -426t121 -426t381 -135q209 0 400 80v-142q-196 -77 -411 -77h-5q-633 0 -633 700z" />
-<glyph unicode="D" horiz-adv-x="1372" d="M178 0v1366h428q641 0 641 -682q0 -684 -641 -684h-428zM326 139h270q262 0 383 131t121 414t-121 414t-383 131h-270v-1090z" />
-<glyph unicode="E" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862z" />
-<glyph unicode="F" horiz-adv-x="1097" d="M178 0v1366h838v-139h-690v-471h606v-144h-606v-612h-148z" />
-<glyph unicode="G" horiz-adv-x="1386" d="M125 684q0 700 633 700q227 0 430 -86v-143q-199 88 -414 88q-260 0 -381 -135t-121 -424q0 -291 121 -425t381 -134q164 0 313 49v426h-329v143h477v-653q-221 -106 -477 -106q-633 0 -633 700z" />
-<glyph unicode="H" horiz-adv-x="1380" d="M178 0v1366h148v-612h727v612h147v-1366h-147v612h-727v-612h-148z" />
-<glyph unicode="I" horiz-adv-x="501" d="M178 0v1366h148v-1366h-148z" />
-<glyph unicode="J" horiz-adv-x="915" d="M59 55v154q117 -82 252 -82q139 0 200.5 68.5t61.5 224.5v946h148v-958q0 -125 -35 -213.5t-95.5 -131.5t-124 -61t-136.5 -18q-147 0 -271 71z" />
-<glyph unicode="K" horiz-adv-x="1294" d="M178 0v1366h148v-704l632 704h199l-586 -639l633 -727h-194l-535 623l-149 -164v-459h-148z" />
-<glyph unicode="L" horiz-adv-x="1067" d="M178 0v1366h148v-1223h698v-143h-846z" />
-<glyph unicode="M" horiz-adv-x="1619" d="M178 0v1366h160l469 -694l471 694h160v-1366h-148v1124l-483 -696l-481 696v-1124h-148z" />
-<glyph unicode="N" horiz-adv-x="1490" d="M178 0v1366h168l815 -1151v1151h148v-1366h-166l-817 1151v-1151h-148z" />
-<glyph unicode="O" d="M125 684q0 700 633 700q635 0 635 -700t-635 -700q-633 0 -633 700zM272 684q0 -291 117 -426t369 -135q254 0 370.5 135t116.5 426t-116.5 426t-370.5 135q-252 0 -369 -135t-117 -426z" />
-<glyph unicode="P" horiz-adv-x="1220" d="M176 0v1366h486q229 0 351 -108.5t122 -313.5t-122 -313.5t-351 -108.5h-338v-522h-148zM324 664h319q109 0 181.5 23.5t105.5 68.5t45 88t12 100t-12 100t-45 88.5t-105.5 69t-181.5 23.5h-319v-561z" />
-<glyph unicode="Q" d="M125 684q0 700 633 700q635 0 635 -700q0 -651 -553 -698q41 -170 268 -176v-134q-113 0 -196 30t-127 81t-65.5 98t-29.5 101q-565 37 -565 698zM272 684q0 -291 117 -426t369 -135q254 0 370.5 135t116.5 426t-116.5 426t-370.5 135q-252 0 -369 -135t-117 -426z" />
-<glyph unicode="R" horiz-adv-x="1271" d="M178 0v1366h502q223 0 343 -101.5t120 -295.5q0 -88 -28 -160t-63.5 -112t-84.5 -69.5t-80 -39t-58 -15.5l375 -573h-174l-356 551h-348v-551h-148zM326 690h346q102 0 170.5 23.5t99.5 65.5t42 83t11 96.5t-11 96.5t-42 83t-99.5 65.5t-170.5 23.5h-346v-537z" />
-<glyph unicode="S" horiz-adv-x="1150" d="M125 1004q0 158 114.5 269t356.5 111q174 0 342 -81v-152q-152 86 -328 86q-164 0 -251 -60t-87 -173q0 -70 56.5 -117t139.5 -69.5t181.5 -57.5t181 -76t139 -127t56.5 -209q0 -170 -128 -267t-357 -97q-223 0 -404 96v160q162 -109 393 -109q348 0 349 217 q0 92 -56.5 154.5t-139.5 91.5l-181 60q-98 32 -181 65t-139.5 105.5t-56.5 179.5z" />
-<glyph unicode="T" horiz-adv-x="1163" d="M25 1223v143h1112v-143h-482v-1223h-147v1223h-483z" />
-<glyph unicode="U" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5z" />
-<glyph unicode="V" horiz-adv-x="1251" d="M25 1366h157l441 -1155l438 1155h158l-525 -1366h-145z" />
-<glyph unicode="W" horiz-adv-x="1947" d="M25 1366h155l359 -1143l360 1143h148l358 -1143l360 1143h156l-440 -1366h-150l-358 1114l-359 -1114h-151z" />
-<glyph unicode="X" horiz-adv-x="1206" d="M25 0l485 692l-473 674h176l387 -549l385 549h180l-475 -676l484 -690h-177l-397 563l-395 -563h-180z" />
-<glyph unicode="Y" horiz-adv-x="1224" d="M25 1366h178l405 -596l404 596h178l-508 -739v-627h-147v627z" />
-<glyph unicode="Z" horiz-adv-x="1247" d="M131 0v129l780 1090h-759v147h952v-125l-785 -1094h785v-147h-973z" />
-<glyph unicode="[" horiz-adv-x="784" d="M244 -238v1663h370v-139h-223v-1384h223v-140h-370z" />
-<glyph unicode="\" horiz-adv-x="753" d="M-76 1548h135l752 -1749h-133z" />
-<glyph unicode="]" horiz-adv-x="780" d="M172 -98h221v1384h-221v139h369v-1663h-369v140z" />
-<glyph unicode="^" horiz-adv-x="1146" d="M274 969l213 444h113l213 -444h-123l-145 325l-148 -325h-123z" />
-<glyph unicode="_" horiz-adv-x="1118" d="M0 -49h1118v-90h-1118v90z" />
-<glyph unicode="`" horiz-adv-x="448" d="M94 1391h150l116 -236h-96z" />
-<glyph unicode="a" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -317.5v-307q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203z" />
-<glyph unicode="b" horiz-adv-x="1167" d="M170 307v1106h147v-414q123 41 252 41q70 0 131.5 -12t125 -47t109.5 -90t74.5 -149.5t28.5 -217.5q0 -135 -31.5 -236.5t-77.5 -157.5t-110.5 -91t-119 -45t-111.5 -10q-111 0 -193 27.5t-123 63.5t-66.5 86t-30.5 82.5t-5 63.5zM317 299q1 -180 271 -180q57 0 103 13 t95.5 51t77 125t27.5 216q0 199 -76 289t-246 90q-127 0 -252 -51v-553z" />
-<glyph unicode="c" horiz-adv-x="966" d="M119 512q0 156 43 265.5t116.5 163.5t150.5 76.5t169 22.5q154 0 303 -57v-141q-139 57 -287 57q-182 0 -265 -92t-83 -295q0 -205 83 -296t265 -91q154 0 287 57v-143q-143 -55 -303 -55q-72 0 -134.5 12t-128 48t-111.5 92.5t-75.5 152.5t-29.5 223z" />
-<glyph unicode="d" horiz-adv-x="1150" d="M129 524q0 152 41 258.5t113.5 159.5t147.5 74.5t165 21.5q135 0 254 -39v412h147v-1106q0 -41 -12 -84t-50 -102.5t-130 -97t-227 -37.5q-57 0 -111.5 10t-118 45t-109.5 91t-78 157.5t-32 236.5zM276 524q0 -129 28 -216t77 -125t94 -51t103 -13q272 0 272 180v553 q-125 51 -254 51q-168 0 -244 -90t-76 -289z" />
-<glyph unicode="e" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -109q8 -62 8 -78v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267z" />
-<glyph unicode="f" horiz-adv-x="675" d="M82 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176z" />
-<glyph unicode="g" horiz-adv-x="1101" d="M123 516q0 160 43 271.5t115.5 162.5t138 70.5t137.5 19.5q131 0 220 -36.5t125 -94t48.5 -99.5t12.5 -83v-770q0 -68 -19.5 -125t-64.5 -111.5t-137.5 -85t-223.5 -30.5q-143 0 -291 37v139q131 -37 275 -37q98 0 164.5 17.5t96.5 50t41 64.5t11 77v102 q-115 -37 -240 -37q-68 0 -127 11.5t-120.5 45.5t-105.5 88t-71.5 144t-27.5 209zM270 516q0 -115 24.5 -189.5t72 -110.5t95.5 -48t113 -12q123 0 240 47v530q0 172 -258 172q-55 0 -98 -13t-90 -50t-73 -120t-26 -206z" />
-<glyph unicode="h" horiz-adv-x="1181" d="M193 0v1411h147v-436q155 65 309 65q188 0 290.5 -113.5t102.5 -320.5v-606h-147v606q0 287 -262 287q-143 0 -293 -62v-831h-147z" />
-<glyph unicode="i" horiz-adv-x="540" d="M193 1204v162h163v-162h-163zM201 0h147v1022h-147v-1022z" />
-<glyph unicode="j" horiz-adv-x="514" d="M6 -258q127 2 168 50t41 157v1073h147v-1081q0 -336 -356 -336v137zM207 1204v162h164v-162h-164z" />
-<glyph unicode="k" horiz-adv-x="1124" d="M188 0v1411h148v-891l459 502h200l-436 -469l492 -553h-197l-391 449l-127 -138v-311h-148z" />
-<glyph unicode="l" horiz-adv-x="552" d="M207 0v1411h147v-1411h-147z" />
-<glyph unicode="m" horiz-adv-x="1824" d="M193 0v942q197 98 409 98q168 0 270 -84q197 84 379 84q186 0 288.5 -98t102.5 -276v-666h-147v666q0 43 -8 76.5t-32.5 71.5t-79 58.5t-134.5 20.5q-129 0 -283 -51q33 -74 33 -176v-666h-147v666q0 45 -8.5 78.5t-33 70.5t-80.5 57.5t-138 20.5q-125 0 -244 -37v-856 h-147z" />
-<glyph unicode="n" horiz-adv-x="1189" d="M168 0v940q201 100 432 100q193 0 299.5 -98t106.5 -276v-666h-148v666q0 43 -9 76.5t-35 71.5t-84 58.5t-146 20.5q-129 0 -269 -41v-852h-147z" />
-<glyph unicode="o" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t150.5 76.5t169 22.5t170 -22.5t151.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-151.5 -76.5t-170 -22.5t-169 22.5t-150.5 76.5t-117 163.5t-43 265.5zM248 512q0 -205 79 -299t253 -94t253.5 93t79.5 300t-79.5 300 t-253.5 93t-253 -94t-79 -299z" />
-<glyph unicode="p" horiz-adv-x="1159" d="M168 -379v1096q0 41 12.5 84t50 103.5t130 98t227.5 37.5q57 0 111.5 -10t118 -45t109.5 -92t77.5 -158.5t31.5 -236.5q0 -152 -41 -258.5t-113.5 -159.5t-147.5 -74.5t-165 -21.5q-125 0 -254 39v-402h-147zM315 172q117 -51 254 -51q168 0 244 89t76 290 q0 129 -27.5 215t-77 125t-94.5 52t-102 13q-272 0 -273 -182v-551z" />
-<glyph unicode="q" horiz-adv-x="1150" d="M115 498q0 135 31.5 236.5t77.5 158.5t109.5 92t118 45t111.5 10q111 0 193 -27.5t123 -63.5t66.5 -86t30.5 -82.5t5 -63.5v-1096h-147v402q-117 -39 -252 -39q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM262 500q0 -201 76 -290t244 -89q135 0 252 51v551 q0 182 -271 182q-57 0 -102 -13t-94.5 -52t-77 -125t-27.5 -215z" />
-<glyph unicode="r" horiz-adv-x="702" d="M166 0v940q231 98 496 98v-141q-168 -4 -349 -53v-844h-147z" />
-<glyph unicode="s" horiz-adv-x="872" d="M100 743q0 123 89.5 210t273.5 87q131 0 262 -63v-148q-117 70 -246 70q-231 0 -231 -156q0 -53 56 -85.5t137 -54t162 -51.5t137.5 -101.5t56.5 -182.5q0 -133 -99.5 -208.5t-275.5 -75.5q-170 0 -309 77v156q147 -90 303 -90q233 0 233 141q0 76 -56 119t-137 64.5 t-162 47t-137.5 86t-56.5 158.5z" />
-<glyph unicode="t" horiz-adv-x="706" d="M49 887v135h178v289h148v-289h264v-135h-264v-623q0 -78 32.5 -109.5t114.5 -31.5q63 0 117 24v-139q-65 -24 -130 -24h-5q-41 0 -80 9t-88 35.5t-79 89t-30 154.5v615h-178z" />
-<glyph unicode="u" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5z" />
-<glyph unicode="v" horiz-adv-x="1062" d="M66 1022h157l309 -838l310 838h157l-393 -1022h-149z" />
-<glyph unicode="w" horiz-adv-x="1603" d="M78 1022h155l261 -791l235 791h141l238 -791l260 791h156l-340 -1022h-150l-233 764l-234 -764h-149z" />
-<glyph unicode="x" horiz-adv-x="1032" d="M57 0l365 518l-354 504h174l268 -379l268 379h180l-356 -504l-2 -4l363 -514h-177l-274 389l-274 -389h-181z" />
-<glyph unicode="y" horiz-adv-x="1028" d="M63 1022h158l291 -809l295 809h158l-418 -1122q-109 -295 -322 -295q-49 0 -96 18v135q41 -18 88 -18q39 0 77 28.5t62.5 68.5t45 81t28.5 70l10 28z" />
-<glyph unicode="z" horiz-adv-x="950" d="M92 0v133l557 748h-543v141h734v-129l-561 -752h561v-141h-748z" />
-<glyph unicode="{" horiz-adv-x="763" d="M186 551v100q129 33 129 148v319q0 150 79 229.5t229 79.5v-139q-82 -4 -121 -36.5t-39 -123.5v-329q0 -119 -119 -199q119 -80 119 -199v-329q0 -88 39 -122t121 -38v-137q-150 0 -229 78.5t-79 228.5v319q0 111 -129 150z" />
-<glyph unicode="|" horiz-adv-x="907" d="M379 -203v1565h147v-1565h-147z" />
-<glyph unicode="}" horiz-adv-x="753" d="M156 -88q82 4 120.5 38t38.5 122v329q0 117 121 199q-121 82 -121 199v329q0 90 -37.5 123t-121.5 37v139q150 0 228.5 -79.5t78.5 -229.5v-319q0 -115 129 -148v-100q-129 -39 -129 -150v-319q0 -150 -79 -228.5t-228 -78.5v137z" />
-<glyph unicode="~" horiz-adv-x="1024" d="M201 473v125q57 72 123 79q11 1 22 1q54 0 104 -27l124 -65q50 -27 103 -27q12 0 23 1q66 7 123 79v-123q-57 -72 -123 -79q-11 -1 -22 -1q-54 0 -104 27l-124 65q-50 25 -103 25q-12 0 -23 -1q-66 -7 -123 -79z" />
-<glyph unicode="&#xa1;" horiz-adv-x="741" d="M303 860v162h164v-162h-164zM311 -344l15 1022h118l15 -1022h-148z" />
-<glyph unicode="&#xa2;" horiz-adv-x="976" d="M117 682q0 270 116.5 390t294.5 136v158h103v-156q131 -2 266 -55v-141q-125 51 -266 55v-772q143 4 266 57v-143q-125 -49 -266 -55v-156h-103v158q-411 41 -411 524zM264 684q0 -178 63.5 -270t200.5 -111v762q-137 -20 -200.5 -111.5t-63.5 -269.5z" />
-<glyph unicode="&#xa3;" horiz-adv-x="1136" d="M104 -31v150q80 39 177 45v411h-177v136h177v293q0 158 105.5 269t328.5 111q162 0 309 -79v-150q-139 88 -297 88q-299 0 -299 -233v-299h412v-136h-412v-415q51 -6 145.5 -25.5t154.5 -30.5q47 -8 107 -8q18 0 36 1q83 3 153 30v-150q-80 -31 -180 -31q-24 0 -49 2 q-129 9 -230.5 37.5t-229.5 31.5h-14q-120 0 -217 -48z" />
-<glyph unicode="&#xa5;" horiz-adv-x="1232" d="M37 1366h180l404 -588l403 588h180l-444 -645h256v-123h-322v-192h322v-123h-322v-283h-147v283h-322v123h322v192h-322v123h256z" />
-<glyph unicode="&#xa7;" horiz-adv-x="1112" d="M119 727q0 164 186 223q-82 51 -82 144q0 119 100.5 204.5t309.5 85.5q156 0 297 -61v-145q-129 69 -281 69h-4q-274 0 -274 -153q0 -25 8 -40.5t68.5 -44t179.5 -55.5q84 -18 140 -38.5t116.5 -56.5t92.5 -94t32 -136q0 -76 -51.5 -130.5t-125.5 -78.5q72 -59 72 -158 q0 -131 -111.5 -204.5t-310.5 -73.5q-203 0 -350 73v152q135 -84 340 -84q285 0 285 137q0 61 -48.5 102.5t-218.5 79.5q-184 43 -277 101.5t-93 181.5zM266 727q0 -25 8.5 -40t70 -44t179.5 -55q135 -29 219 -74q117 35 117 115q0 61 -49 102t-217 80q-98 23 -176 49 q-152 -29 -152 -133z" />
-<glyph unicode="&#xa8;" horiz-adv-x="788" d="M162 1155v162h162v-162h-162zM465 1155v162h162v-162h-162z" />
-<glyph unicode="&#xa9;" horiz-adv-x="1593" d="M98 682q0 291 206 495.5t495 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -495 204.5t-206 493.5zM172 682q0 -258 184.5 -442.5t442.5 -184.5q260 0 443 184.5t183 442.5q0 260 -183 444.5t-443 184.5q-258 0 -442.5 -184.5 t-184.5 -444.5zM449 682q0 410 368 410q129 0 244 -45v-82q-109 45 -234 45q-152 0 -222 -79t-70 -249t70.5 -249t221.5 -79q121 0 234 47v-82q-115 -45 -244 -45q-369 0 -368 408z" />
-<glyph unicode="&#xaa;" horiz-adv-x="806" d="M156 979q0 186 297 186l92 -6q0 143 -168 144q-92 0 -164 -29v80q72 28 168 28q246 0 246 -235v-172q0 -72 -46 -115t-92.5 -53t-95.5 -10t-96 10t-94 54t-47 118zM238 982q0 -110 155 -110q152 1 152 105v108q-37 6 -101 7q-206 0 -206 -110z" />
-<glyph unicode="&#xab;" horiz-adv-x="1132" d="M63 522l347 389h198l-364 -389l364 -391h-198zM453 522l346 389h198l-364 -389l364 -391h-198z" />
-<glyph unicode="&#xac;" horiz-adv-x="1273" d="M199 528v136h876v-340h-135v204h-741z" />
-<glyph unicode="&#xad;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
-<glyph unicode="&#xae;" horiz-adv-x="1593" d="M98 682q0 291 206 495.5t495 204.5q291 0 495.5 -204.5t204.5 -495.5q0 -289 -204.5 -493.5t-495.5 -204.5q-289 0 -495 204.5t-206 493.5zM172 682q0 -258 184.5 -442.5t442.5 -184.5q260 0 443 184.5t183 442.5q0 260 -183 444.5t-443 184.5q-258 0 -442.5 -184.5 t-184.5 -444.5zM545 283v798h293q131 0 201.5 -58t70.5 -173q0 -63 -22.5 -111.5t-57.5 -72t-58 -33t-46 -15.5l219 -335h-103l-206 323h-205v-323h-86zM631 688h203q113 0 150.5 43t37.5 112.5t-38 112.5t-150 43h-203v-311z" />
-<glyph unicode="&#xaf;" horiz-adv-x="825" d="M176 1180v108h479v-108h-479z" />
-<glyph unicode="&#xb0;" horiz-adv-x="792" d="M168 1142q0 228 223 228q109 0 166 -58.5t57 -168.5q0 -227 -223 -227q0 -1 -1 -1q-222 0 -222 227zM242 1143q0 -82 34.5 -118t114.5 -36q78 0 114 36t36 118q0 80 -36 116.5t-114 36.5q-80 0 -114.5 -36.5t-34.5 -116.5z" />
-<glyph unicode="&#xb1;" horiz-adv-x="1198" d="M231 68v122h727v-122h-727zM231 598v123h304v301h122v-301h301v-123h-301v-303h-122v303h-304z" />
-<glyph unicode="&#xb2;" horiz-adv-x="671" d="M109 948v70q133 98 252.5 230t119.5 239q0 125 -172 125q-106 0 -192 -62v86q90 55 198 56q125 0 185.5 -60.5t60.5 -145.5q0 -197 -321 -458h331v-80h-462z" />
-<glyph unicode="&#xb3;" horiz-adv-x="661" d="M109 985v82q90 -49 186 -49q203 0 203 141t-207 141h-80v76h84q164 0 164 119q0 117 -174 117q-88 0 -158 -37v80q82 37 168 37q125 0 184.5 -58.5t59.5 -138.5q0 -49 -23.5 -86t-48.5 -49l-23 -14q6 -2 14.5 -6.5t31 -20.5t40 -36.5t31.5 -54.5t14 -71 q0 -90 -66.5 -153.5t-203.5 -63.5q-102 0 -196 45z" />
-<glyph unicode="&#xb4;" horiz-adv-x="483" d="M123 1155l117 236h151l-170 -236h-98z" />
-<glyph unicode="&#xb5;" horiz-adv-x="1247" d="M203 -430v1452h147v-664q0 -45 9.5 -78.5t35 -70.5t83 -57.5t145.5 -20.5q135 0 268 39v852h147v-938q-193 -100 -430 -100q-152 2 -258 82v-496h-147z" />
-<glyph unicode="&#xb6;" horiz-adv-x="911" d="M78 946q0 203 121 311.5t350 108.5h203v-1366h-136v524h-67q-229 0 -350 108.5t-121 313.5z" />
-<glyph unicode="&#xb7;" horiz-adv-x="636" d="M238 475v164h161v-164h-161z" />
-<glyph unicode="&#xb8;" horiz-adv-x="518" d="M115 -246l168 236h116l-129 -236h-155z" />
-<glyph unicode="&#xb9;" horiz-adv-x="491" d="M109 1503v94l135 86h90v-735h-78v651z" />
-<glyph unicode="&#xba;" horiz-adv-x="854" d="M156 1090q0 86 23.5 147t64.5 92t84 43t94 12q266 0 266 -294q0 -293 -266 -293q-113 0 -189.5 64.5t-76.5 228.5zM238 1089.5q0 -114.5 44 -166t140 -51.5q98 0 141 51.5t43 166t-43 167t-141 52.5q-96 0 -140 -52.5t-44 -167z" />
-<glyph unicode="&#xbb;" horiz-adv-x="1132" d="M137 131l363 391l-363 389h197l348 -389l-348 -391h-197zM526 131l363 391l-363 389h197l348 -389l-348 -391h-197z" />
-<glyph unicode="&#xbc;" horiz-adv-x="1433" d="M106 1200v94l134 86h92v-733h-80v651zM147 -45l895 1448h97l-895 -1448h-97zM788 221v84l347 428h90v-438h104v-74h-104v-221h-80v221h-357zM877 295h268v326z" />
-<glyph unicode="&#xbd;" horiz-adv-x="1460" d="M106 1200v94l134 86h92v-733h-80v651zM147 -45l895 1448h97l-895 -1448h-97zM870 0v70q133 98 252 229t119 238q0 127 -172 127q-104 0 -192 -64v88q98 55 198 55q127 0 186.5 -60t59.5 -146q0 -195 -322 -457h332v-80h-461z" />
-<glyph unicode="&#xbe;" horiz-adv-x="1654" d="M135 684v80q94 -47 189 -47q200 0 200 140q0 69 -53 105.5t-154 36.5h-79v74h84q164 0 164 118q0 120 -175 120q-84 0 -157 -39v82q82 37 168 37q125 0 184 -58.5t59 -140.5q0 -49 -23.5 -86t-45.5 -49l-25 -15q6 -2 14.5 -6t31 -20.5t39.5 -35.5t31.5 -53t14.5 -71 q0 -90 -65.5 -154.5t-204.5 -64.5q-98 0 -197 47zM369 -45l895 1448h96l-895 -1448h-96zM1010 221v84l346 428h90v-438h104v-74h-104v-221h-80v221h-356zM1098 295h268v326z" />
-<glyph unicode="&#xbf;" horiz-adv-x="989" d="M147 20q0 82 32 141.5t77 97.5l90 76q45 38 77 101.5t32 151.5v92h147v-102q0 -104 -31.5 -181.5t-76.5 -119t-90.5 -77.5t-77 -80t-31.5 -100q0 -233 270 -233q141 0 275 90v-160q-115 -77 -287 -77q-47 0 -93 7t-105.5 31.5t-102.5 64.5t-74 111.5t-31 165.5zM446 860 v162h164v-162h-164z" />
-<glyph unicode="&#xc0;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM438 1718h152l117 -235h-99z" />
-<glyph unicode="&#xc1;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM614 1483l117 235h152l-170 -235h-99z" />
-<glyph unicode="&#xc2;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM455 1483l163 248h87l163 -248h-114l-92 145l-93 -145h-114z" />
-<glyph unicode="&#xc3;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM373 1485v117q53 66 115 72h19q51 0 97 -24l112 -60q46 -24 97 -24q10 0 21 1q61 6 114 71v-112q-53 -66 -114 -73q-12 -1 -24 -1q-49 0 -94 24l-112 59q-44 24 -92 24q-12 0 -24 -1q-62 -8 -115 -73z M422 567h479l-239 625z" />
-<glyph unicode="&#xc4;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM428 1483v164h164v-164h-164zM731 1483v164h164v-164h-164z" />
-<glyph unicode="&#xc5;" horiz-adv-x="1325" d="M43 0l545 1366h147l543 -1366h-158l-164 426h-589l-164 -426h-160zM422 567h479l-239 625zM496 1591q0 82 43 125t122.5 43t122.5 -43t43 -125t-43 -125t-122.5 -43t-122.5 43t-43 125zM569 1591.5q0 -53.5 21 -76t72 -22.5q49 0 70.5 22.5t21.5 76t-21.5 77t-70.5 23.5 q-51 0 -72 -23.5t-21 -77z" />
-<glyph unicode="&#xc6;" horiz-adv-x="1939" d="M51 0l811 1366h955v-141h-691v-471h607v-144h-607v-469h691v-141h-838v428h-506l-252 -428h-170zM559 575h420v644h-45z" />
-<glyph unicode="&#xc7;" horiz-adv-x="1286" d="M125 686q0 700 633 700q217 0 416 -77v-142q-187 80 -400 80q-260 0 -381 -134t-121 -425t121 -426t381 -135q209 0 400 80v-141q-172 -68 -357 -78l-125 -232h-155l163 232q-575 35 -575 698z" />
-<glyph unicode="&#xc8;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM444 1718h152l117 -235h-99z" />
-<glyph unicode="&#xc9;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM565 1483l115 235h151l-169 -235h-97z" />
-<glyph unicode="&#xca;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM412 1483l166 248h84l165 -248h-114l-92 145l-95 -145h-114z" />
-<glyph unicode="&#xcb;" horiz-adv-x="1159" d="M178 0v1366h862v-139h-714v-471h628v-144h-628v-473h714v-139h-862zM395 1483v164h162v-164h-162zM698 1483v164h162v-164h-162z" />
-<glyph unicode="&#xcc;" horiz-adv-x="501" d="M33 1718h151l117 -235h-98zM178 0v1366h148v-1366h-148z" />
-<glyph unicode="&#xcd;" horiz-adv-x="501" d="M178 0h148v1366h-148v-1366zM203 1483l116 235h152l-170 -235h-98z" />
-<glyph unicode="&#xce;" horiz-adv-x="501" d="M45 1483l164 248h86l164 -248h-115l-92 145l-92 -145h-115zM178 0h148v1366h-148v-1366z" />
-<glyph unicode="&#xcf;" horiz-adv-x="501" d="M18 1483v164h164v-164h-164zM178 0h148v1366h-148v-1366zM322 1483v164h163v-164h-163z" />
-<glyph unicode="&#xd0;" horiz-adv-x="1468" d="M109 621v122h159v623h428q641 0 641 -682q0 -684 -641 -684h-428v621h-159zM416 139h270q262 0 383 131t121 414t-121 414t-383 131h-270v-486h350v-122h-350v-482z" />
-<glyph unicode="&#xd1;" horiz-adv-x="1490" d="M178 0v1366h168l815 -1151v1151h148v-1366h-166l-817 1151v-1151h-148zM455 1485v117q53 66 115 72h19q51 0 98 -24l113 -60q46 -24 97 -24q10 0 21 1q61 6 114 71v-112q-53 -66 -114 -73q-12 -1 -24 -1q-49 0 -94 24l-113 59q-45 24 -93 24q-12 0 -24 -1 q-62 -8 -115 -73z" />
-<glyph unicode="&#xd2;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM563 1718h150l116 -235h-96z" />
-<glyph unicode="&#xd3;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM686 1483l115 235h151l-170 -235h-96z" />
-<glyph unicode="&#xd4;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM551 1483l164 248h86l164 -248h-115l-92 145l-92 -145h-115z" />
-<glyph unicode="&#xd5;" d="M125 684q0 700 633 700q635 0 635 -700t-635 -700q-633 0 -633 700zM272 684q0 -291 117 -426t369 -135q254 0 370.5 135t116.5 426t-116.5 426t-370.5 135q-252 0 -369 -135t-117 -426zM471 1485v117q53 66 113 72h19q51 0 99 -24l112 -60q47 -24 97 -24q10 0 21 1 q61 6 115 71v-112q-53 -66 -115 -73q-12 -1 -24 -1q-49 0 -94 24l-112 59q-46 24 -93 24q-12 0 -25 -1q-60 -8 -113 -73z" />
-<glyph unicode="&#xd6;" d="M125 684q0 700 633 700t633 -700t-633 -700t-633 700zM272 684q0 -291 117 -426t369 -135t368.5 135t116.5 426t-116.5 426t-368.5 135t-369 -135t-117 -426zM524 1483v164h164v-164h-164zM827 1483v164h164v-164h-164z" />
-<glyph unicode="&#xd7;" horiz-adv-x="985" d="M158 160l272 364l-272 365h131l207 -277l209 277h131l-273 -365l273 -364h-131l-209 276l-207 -276h-131z" />
-<glyph unicode="&#xd8;" d="M102 0l158 188q-135 172 -135 496q0 700 633 700q256 0 411 -118l86 100h160l-160 -188q137 -174 138 -494q0 -702 -635 -702q-254 0 -410 118l-86 -100h-160zM272 684q0 -252 84 -383l721 856q-115 88 -319 88q-252 0 -369 -135t-117 -426zM440 209q115 -88 318 -88 q254 0 370.5 135t116.5 428q0 244 -84 381z" />
-<glyph unicode="&#xd9;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM520 1718h152l116 -235h-96z" />
-<glyph unicode="&#xda;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM678 1483l117 235h151l-170 -235h-98z" />
-<glyph unicode="&#xdb;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM526 1483l164 248h86l164 -248h-115l-92 145l-92 -145h-115z" />
-<glyph unicode="&#xdc;" horiz-adv-x="1476" d="M178 561v805h148v-813q0 -434 407.5 -434t407.5 434v813h147v-805q0 -281 -143.5 -429t-411.5 -148t-411.5 148.5t-143.5 428.5zM500 1483v164h164v-164h-164zM803 1483v164h164v-164h-164z" />
-<glyph unicode="&#xdd;" horiz-adv-x="1224" d="M25 1366h178l405 -596l404 596h178l-508 -739v-627h-147v627zM539 1483l116 235h150l-170 -235h-96z" />
-<glyph unicode="&#xde;" horiz-adv-x="1167" d="M176 0v1366h148v-207h266q227 0 349 -109.5t122 -314.5t-122 -313.5t-349 -108.5h-266v-313h-148zM324 455h245q109 0 181.5 23.5t105.5 68.5t45 88t12 100.5t-12 100.5t-45 88t-105.5 68.5t-181.5 23.5h-245v-561z" />
-<glyph unicode="&#xdf;" horiz-adv-x="1255" d="M180 0v868q0 113 23.5 203t73 162t136.5 111.5t208 39.5q229 0 341.5 -112.5t112.5 -267.5q0 -51 -19.5 -100.5t-46 -84.5t-54 -61.5t-46.5 -40.5l-20 -12q10 -4 27.5 -12.5t61.5 -39t76.5 -66.5t60.5 -94.5t28 -123.5q0 -170 -150.5 -269.5t-384.5 -99.5v133 q162 0 274.5 61.5t112.5 188.5q0 129 -120.5 188.5t-266.5 59.5v129q145 0 232.5 62.5t87.5 181.5q0 123 -85 183t-222 60q-141 0 -217 -87t-76 -300v-860h-148z" />
-<glyph unicode="&#xe0;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM356 1391h150l117 -236h-97z" />
-<glyph unicode="&#xe1;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM467 1155l117 236h151l-170 -236h-98z" />
-<glyph unicode="&#xe2;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM362 1155l164 246h86l164 -246h-114l-93 143l-92 -143h-115z" />
-<glyph unicode="&#xe3;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t93 59t101 36t105.5 17.5t85 7t59.5 1l166 -10q0 137 -75.5 195.5t-223.5 58.5q-150 0 -299 -47v141q141 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 315q0 -194 278 -194h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -201zM268 1157v117q53 66 114 72q11 1 21 1q50 0 97 -25l114 -60q46 -24 97 -24q10 0 21 1q61 6 114 72v-113q-53 -66 -114 -73q-12 -1 -24 -1q-49 0 -94 24 l-114 60q-45 23 -94 23q-11 0 -23 -1q-61 -7 -115 -73z" />
-<glyph unicode="&#xe4;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM336 1155v162h164v-162h-164zM639 1155v162h164v-162h-164z" />
-<glyph unicode="&#xe5;" horiz-adv-x="1093" d="M115 313q0 70 25.5 125.5t62.5 90t94 59t101 36t104.5 17.5t86 7t60.5 1l164 -10q0 137 -75.5 195.5t-223.5 58.5q-166 0 -299 -49v143q151 53 307 53q215 0 328 -108.5t113 -315.5v-309q0 -98 -45.5 -167.5t-117 -100.5t-134 -43t-123.5 -12q-47 0 -96.5 6t-112 27.5 t-108.5 55t-78.5 96t-32.5 144.5zM262 313q0 -192 278 -192h3q272 0 272 186v197q-68 12 -182 12q-371 0 -371 -203zM393 1264q0 82 43 125t123 43t123 -43t43 -125t-43 -125t-123 -43t-123 43t-43 125zM467 1263.5q0 -53.5 20.5 -77t71.5 -23.5q49 0 70.5 23.5t21.5 77 t-21.5 76t-70.5 22.5q-51 0 -71.5 -22.5t-20.5 -76z" />
-<glyph unicode="&#xe6;" horiz-adv-x="1824" d="M111 313q0 70 25.5 125.5t62.5 90t93 59t101 36t105.5 17.5t85 7t59.5 1l166 -10q0 137 -76 195.5t-223 58.5q-166 0 -299 -49v143q152 53 305 53q268 0 377 -168q112 168 362 168q94 0 171 -29.5t122 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-746 q14 -154 94 -223.5t240 -69.5q219 0 350 76v-152q-152 -71 -360 -71h-6q-248 0 -365 151q-103 -151 -367 -151h-3q-47 0 -96.5 6t-112 27.5t-108.5 55t-78.5 96t-32.5 144.5zM258 313q0 -192 278 -192h3q270 0 270 186v197q-66 12 -180 12q-371 0 -371 -203zM956 555h598 q0 35 -8 78t-33.5 110.5t-92 112.5t-165.5 45q-150 0 -219.5 -79t-79.5 -267z" />
-<glyph unicode="&#xe7;" horiz-adv-x="966" d="M119 512q0 156 43 265.5t116.5 163.5t150.5 76.5t169 22.5q154 0 303 -57v-141q-140 57 -287 57q-182 0 -265 -92t-83 -295q0 -205 83 -296t265 -91q154 0 287 57v-143q-115 -41 -225 -53l-127 -230h-156l164 228q-86 4 -157.5 30.5t-138 83t-104.5 162t-38 252.5z" />
-<glyph unicode="&#xe8;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM395 1391h152l117 -236h-99z" />
-<glyph unicode="&#xe9;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM506 1155l117 236h151l-170 -236h-98z" />
-<glyph unicode="&#xea;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM371 1155l164 246h86l163 -246h-114l-92 143l-93 -143h-114z" />
-<glyph unicode="&#xeb;" horiz-adv-x="1136" d="M129 498v7v8q0 527 449 527q94 0 170.5 -29.5t121.5 -69.5t79 -104.5t48.5 -109.5t22.5 -108.5t8 -78.5v-44l-4 -72h-745q14 -154 94 -223.5t237 -69.5q223 0 351 76v-152q-148 -71 -359 -71h-6q-90 0 -165 21.5t-147.5 74.5t-113.5 159.5t-41 258.5zM279 555h598 q0 35 -8.5 78t-34 110.5t-92 112.5t-164.5 45q-152 0 -220.5 -79t-78.5 -267zM344 1155v162h164v-162h-164zM647 1155v162h164v-162h-164z" />
-<glyph unicode="&#xec;" horiz-adv-x="552" d="M57 1391h152l117 -236h-99zM203 0v1022h147v-1022h-147z" />
-<glyph unicode="&#xed;" horiz-adv-x="552" d="M203 0h147v1022h-147v-1022zM227 1155l115 236h152l-170 -236h-97z" />
-<glyph unicode="&#xee;" horiz-adv-x="552" d="M68 1155l165 246h84l166 -246h-114l-93 143l-94 -143h-114zM203 0h147v1022h-147v-1022z" />
-<glyph unicode="&#xef;" horiz-adv-x="552" d="M43 1155v162h162v-162h-162zM203 0h147v1022h-147v-1022zM346 1155v162h162v-162h-162z" />
-<glyph unicode="&#xf0;" horiz-adv-x="1193" d="M119 444q0 199 125 314.5t340 115.5q186 0 307 -90q-25 244 -174 365l-230 -129l-55 98l170 94q-86 33 -192 33q-41 0 -111 -8v137q66 10 127 10q184 0 328 -86l190 107l53 -98l-145 -84q197 -193 197 -596q0 -643 -465 -643q-123 0 -216.5 41t-145.5 109.5t-77.5 147 t-25.5 162.5zM266 446q0 -41 9.5 -84t38 -103t98.5 -98t172 -38q43 0 70.5 3t78.5 29.5t83 75t57.5 148.5t27.5 242q-110 116 -314 116h-3q-154 0 -236 -75.5t-82 -215.5z" />
-<glyph unicode="&#xf1;" horiz-adv-x="1189" d="M168 0v940q201 100 432 100q193 0 299.5 -98t106.5 -276v-666h-148v666q0 43 -9 76.5t-35 71.5t-84 58.5t-146 20.5q-129 0 -269 -41v-852h-147zM299 1157v117q53 66 113 72q11 1 21 1q49 0 97 -25l112 -60q47 -24 97 -24q10 0 21 1q61 6 114 72v-113q-53 -66 -114 -73 q-12 -1 -24 -1q-49 0 -94 24l-112 60q-46 23 -94 23q-12 0 -24 -1q-60 -7 -113 -73z" />
-<glyph unicode="&#xf2;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM362 1391h152l117 -236h-99z" />
-<glyph unicode="&#xf3;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM530 1155l117 236h152l-170 -236h-99z" />
-<glyph unicode="&#xf4;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM373 1155l166 246h84l165 -246h-114l-92 143l-95 -143h-114z" />
-<glyph unicode="&#xf5;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t150.5 76.5t169 22.5t170 -22.5t151.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-151.5 -76.5t-170 -22.5t-169 22.5t-150.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 79 -300t253 -93t253.5 93t79.5 300t-79.5 300 t-253.5 93t-253 -93t-79 -300zM301 1159v115q53 66 113 73q12 1 23 1q49 0 95 -24l112 -60q46 -25 95 -25q11 0 23 1q61 7 115 73v-115q-53 -66 -115 -73q-12 -1 -23 -1q-50 0 -95 24l-112 60q-47 24 -97 24q-10 0 -21 -1q-60 -6 -113 -72z" />
-<glyph unicode="&#xf6;" horiz-adv-x="1155" d="M100 512q0 156 43 265.5t117 163.5t151.5 76.5t170 22.5t169 -22.5t150.5 -76.5t117 -163.5t43 -265.5t-43 -265.5t-117 -163.5t-150.5 -76.5t-169 -22.5t-170 22.5t-151.5 76.5t-117 163.5t-43 265.5zM248 512q0 -207 80 -300t254 -93t252.5 94t78.5 299t-78.5 299 t-252.5 94t-254 -93t-80 -300zM348 1155v162h162v-162h-162zM651 1155v162h162v-162h-162z" />
-<glyph unicode="&#xf7;" horiz-adv-x="1286" d="M227 465h832v123h-832v-123zM561 152v163h164v-163h-164zM561 735v164h164v-164h-164z" />
-<glyph unicode="&#xf8;" horiz-adv-x="1175" d="M76 0l127 152q-96 133 -97 360q0 156 43.5 265.5t117 163.5t151.5 76.5t170 22.5q180 0 297 -82l53 64h160l-125 -150q94 -131 94 -360q0 -156 -43 -265.5t-116.5 -163.5t-150.5 -76.5t-169 -22.5q-182 0 -297 79l-55 -63h-160zM254 512q0 -158 45 -246l496 586 q-78 53 -207 53q-174 0 -254 -93t-80 -300zM381 170q76 -51 207 -51q174 0 253 93t79 300q0 156 -46 246z" />
-<glyph unicode="&#xf9;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM383 1391h152l116 -236h-98z" />
-<glyph unicode="&#xfa;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM541 1155l116 236h150l-170 -236h-96z" />
-<glyph unicode="&#xfb;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM389 1155l164 246h84l166 -246h-115l-92 143l-92 -143h-115z" />
-<glyph unicode="&#xfc;" horiz-adv-x="1202" d="M168 430v592h147v-600q0 -156 68 -228.5t213 -72.5q143 0 212 72.5t69 228.5v600h147v-592q0 -213 -111.5 -329.5t-316.5 -116.5t-316.5 116.5t-111.5 329.5zM362 1155v162h164v-162h-164zM666 1155v162h163v-162h-163z" />
-<glyph unicode="&#xfd;" horiz-adv-x="1028" d="M63 1022h158l293 -809l293 809h158l-418 -1122q-109 -295 -320 -295q-51 0 -98 18v135q41 -18 88 -18q63 0 118.5 68.5t80.5 138.5l24 69zM449 1155l116 236h152l-170 -236h-98z" />
-<glyph unicode="&#xfe;" horiz-adv-x="1136" d="M170 -377v1776h147v-414q123 41 252 41q70 0 131.5 -12.5t125 -47t109.5 -90t74.5 -149.5t28.5 -217q0 -152 -42 -258.5t-114.5 -159.5t-148.5 -74.5t-164 -21.5q-129 0 -252 41v-414h-147zM317 184q117 -51 252 -51q170 0 246 89t76 288t-76 289t-246 90 q-135 0 -252 -51v-654z" />
-<glyph unicode="&#xff;" horiz-adv-x="1028" d="M63 1022h158l293 -809l293 809h158l-418 -1122q-109 -295 -320 -295q-51 0 -98 18v135q41 -18 88 -18q63 0 118.5 68.5t80.5 138.5l24 69zM279 1155v162h161v-162h-161zM582 1155v162h161v-162h-161z" />
-<glyph unicode="&#x152;" horiz-adv-x="2029" d="M123 684q0 700 633 700q160 0 274 -45v27h864v-139h-716v-471h628v-144h-628v-473h716v-139h-864v27q-117 -43 -274 -43q-633 0 -633 700zM270 684q0 -291 117 -426t369 -135q170 0 274 59v1004q-109 59 -274 59q-252 0 -369 -135t-117 -426z" />
-<glyph unicode="&#x153;" horiz-adv-x="1921" d="M102 512q0 156 43 265.5t117 163.5t150.5 76.5t169.5 22.5q279 0 397 -186q113 186 377 186q94 0 170 -29.5t122 -69.5t78.5 -104.5t47 -109.5t22.5 -108.5t8 -78.5v-44l-2 -72h-745q14 -154 94 -223.5t238 -69.5q219 0 350 76v-152q-148 -71 -359 -71h-6 q-276 0 -393 190q-119 -190 -399 -190q-92 0 -169 22.5t-151 76.5t-117 163.5t-43 265.5zM250 512q0 -207 79 -300t253 -93t253.5 93t79.5 300t-79.5 300t-253.5 93t-253 -93t-79 -300zM1055 555h600q0 33 -8.5 77t-35 111.5t-93 112.5t-162.5 45q-152 0 -221.5 -79 t-79.5 -267z" />
-<glyph unicode="&#x178;" horiz-adv-x="1224" d="M25 1366h178l405 -596l404 596h178l-508 -739v-627h-147v627zM375 1483v164h164v-164h-164zM678 1483v164h164v-164h-164z" />
-<glyph unicode="&#x2c6;" horiz-adv-x="823" d="M207 1155l166 246h84l164 -246h-115l-92 143l-92 -143h-115z" />
-<glyph unicode="&#x2dc;" horiz-adv-x="1038" d="M215 1157v117q53 66 113 72q11 1 21 1q49 0 97 -25l112 -60q47 -24 97 -24q10 0 21 1q61 6 115 72v-113q-53 -66 -115 -73q-12 -1 -23 -1q-50 0 -95 24l-112 60q-46 23 -94 23q-12 0 -24 -1q-60 -7 -113 -73z" />
-<glyph unicode="&#x2000;" horiz-adv-x="879" />
-<glyph unicode="&#x2001;" horiz-adv-x="1759" />
-<glyph unicode="&#x2002;" horiz-adv-x="879" />
-<glyph unicode="&#x2003;" horiz-adv-x="1759" />
-<glyph unicode="&#x2004;" horiz-adv-x="586" />
-<glyph unicode="&#x2005;" horiz-adv-x="439" />
-<glyph unicode="&#x2006;" horiz-adv-x="293" />
-<glyph unicode="&#x2007;" horiz-adv-x="293" />
-<glyph unicode="&#x2008;" horiz-adv-x="219" />
-<glyph unicode="&#x2009;" horiz-adv-x="351" />
-<glyph unicode="&#x200a;" horiz-adv-x="97" />
-<glyph unicode="&#x2010;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
-<glyph unicode="&#x2011;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
-<glyph unicode="&#x2012;" horiz-adv-x="950" d="M162 539v143h624v-143h-624z" />
-<glyph unicode="&#x2013;" horiz-adv-x="1146" d="M141 528v127h860v-127h-860z" />
-<glyph unicode="&#x2014;" horiz-adv-x="1824" d="M162 528v136h1501v-136h-1501z" />
-<glyph unicode="&#x2018;" horiz-adv-x="522" d="M96 1384h168l152 -401h-148z" />
-<glyph unicode="&#x2019;" horiz-adv-x="522" d="M96 983l152 401h168l-172 -401h-148z" />
-<glyph unicode="&#x201a;" horiz-adv-x="614" d="M147 -229l152 401h166l-170 -401h-148z" />
-<glyph unicode="&#x201c;" horiz-adv-x="800" d="M96 1384h168l152 -401h-148zM375 1384h168l151 -401h-147z" />
-<glyph unicode="&#x201d;" horiz-adv-x="792" d="M96 983l152 401h168l-172 -401h-148zM375 983l151 401h168l-172 -401h-147z" />
-<glyph unicode="&#x201e;" horiz-adv-x="892" d="M147 -229l152 401h166l-170 -401h-148zM426 -229l152 401h167l-172 -401h-147z" />
-<glyph unicode="&#x2022;" horiz-adv-x="849" d="M184 684q0 98 70 167t168 69q96 0 165.5 -69t69.5 -167t-69.5 -168t-165.5 -70q-98 0 -168 70t-70 168z" />
-<glyph unicode="&#x2026;" horiz-adv-x="1576" d="M219 0v164h164v-164h-164zM711 0v164h161v-164h-161zM1200 0v164h162v-164h-162z" />
-<glyph unicode="&#x202f;" horiz-adv-x="351" />
-<glyph unicode="&#x2039;" horiz-adv-x="710" d="M63 522l347 389h198l-364 -389l364 -391h-198z" />
-<glyph unicode="&#x203a;" horiz-adv-x="776" d="M137 131l363 391l-363 389h197l348 -389l-348 -391h-197z" />
-<glyph unicode="&#x205f;" horiz-adv-x="439" />
-<glyph unicode="&#x20ac;" horiz-adv-x="1390" d="M102 467v123h136q-4 61 -4 94q-1 3 -1 6q0 33 5 88h-136v123h150q90 483 614 483q215 0 416 -79v-142q-194 82 -399 82q-207 0 -325 -84t-157 -260h658v-123h-674q-4 -61 -4 -94q0 -37 4 -94h674v-123h-658q39 -176 157 -260t325 -84q209 0 399 80v-142 q-200 -77 -411 -77h-5q-524 0 -614 483h-150z" />
-<glyph unicode="&#x2122;" horiz-adv-x="1550" d="M100 1294v72h557v-72h-241v-610h-74v610h-242zM762 684v682h80l235 -346l236 346h80v-682h-74v561l-242 -348l-241 348v-561h-74z" />
-<glyph unicode="&#xe000;" horiz-adv-x="1020" d="M0 0v1020h1020v-1020h-1020z" />
-<glyph unicode="&#xfb01;" horiz-adv-x="1202" d="M84 887v137h176v53q0 106 28.5 182t63.5 112t89.5 56.5t82 22.5t66.5 2q53 0 100 -10v-133q-41 8 -74 8q-39 0 -59 -2t-54 -14.5t-52.5 -35t-33 -70.5t-14.5 -116v-55h287v-137h-287v-887h-143v887h-176zM885 1204v162h164v-162h-164zM893 0h147v1022h-147v-1022z" />
-<glyph unicode="&#xfb02;" horiz-adv-x="1220" d="M84 887v137h176v53q0 106 28.5 182t63.5 112t89.5 56.5t82 22.5t66.5 2q53 0 100 -10v-133q-41 8 -74 8q-39 0 -59 -2t-54 -14.5t-52.5 -35t-33 -70.5t-14.5 -116v-55h287v-137h-287v-887h-143v887h-176zM893 0h147v1411h-147v-1411z" />
-<glyph unicode="&#xfb03;" horiz-adv-x="1890" d="M82 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM757 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2 q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM1543 1204v162h163v-162h-163zM1551 0h147v1022h-147v-1022z" />
-<glyph unicode="&#xfb04;" horiz-adv-x="1902" d="M82 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM757 887v135h176v55q0 106 27.5 182t62.5 112t89.5 56.5t83 22.5t67.5 2 q55 0 98 -12v-131q-41 8 -74 8q-49 0 -74.5 -4t-61.5 -24.5t-53 -73t-17 -136.5v-57h280v-135h-280v-887h-148v887h-176zM1557 0v1411h147v-1411h-147z" />
-</font>
-</defs></svg> 
\ No newline at end of file
diff --git a/static/fnt/mavenpro-regular-webfont.ttf b/static/fnt/mavenpro-regular-webfont.ttf
deleted file mode 100644 (file)
index 8fb8aac..0000000
Binary files a/static/fnt/mavenpro-regular-webfont.ttf and /dev/null differ
diff --git a/static/fnt/mavenpro-regular-webfont.woff b/static/fnt/mavenpro-regular-webfont.woff
deleted file mode 100644 (file)
index 16f0e00..0000000
Binary files a/static/fnt/mavenpro-regular-webfont.woff and /dev/null differ
diff --git a/static/img/apply-progress.gif b/static/img/apply-progress.gif
deleted file mode 100644 (file)
index 610f733..0000000
Binary files a/static/img/apply-progress.gif and /dev/null differ
diff --git a/static/img/arrows.svg b/static/img/arrows.svg
deleted file mode 100644 (file)
index f1239a7..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="96"
-   height="48"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="arrows.svg"
-   inkscape:export-filename="/media/data/projects/motioneye/static/img/settings.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="5.0625"
-     inkscape:cx="43.687152"
-     inkscape:cy="14.670355"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1030"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
-     inkscape:bbox-paths="true"
-     inkscape:snap-bbox-edge-midpoints="true"
-     inkscape:snap-bbox-midpoints="true"
-     inkscape:object-paths="true"
-     inkscape:object-nodes="true"
-     inkscape:snap-smooth-nodes="true"
-     inkscape:snap-midpoints="true"
-     inkscape:snap-object-midpoints="true"
-     inkscape:snap-center="true"
-     inkscape:snap-page="true"
-     inkscape:snap-intersection-paths="true"
-     showguides="true"
-     inkscape:guide-bbox="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1004.3622)">
-    <path
-       style="fill:none;stroke:#3498db;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       d="m 30,1012.3622 -17,17 17,15"
-       id="path3755"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="ccc" />
-    <path
-       sodipodi:nodetypes="ccc"
-       inkscape:connector-curvature="0"
-       id="path3757"
-       d="m 66,1012.3622 17,17 -17,15"
-       style="fill:none;stroke:#3498db;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
-  </g>
-</svg>
diff --git a/static/img/camera-progress.gif b/static/img/camera-progress.gif
deleted file mode 100644 (file)
index 748c0c7..0000000
Binary files a/static/img/camera-progress.gif and /dev/null differ
diff --git a/static/img/combo-box-arrow.svg b/static/img/combo-box-arrow.svg
deleted file mode 100644 (file)
index 5f57b93..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="20"
-   height="16"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="combo-box-arrow.svg"
-   inkscape:export-filename="/media/data/projects/motioneye/static/img/combo-box-arrow.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="1"
-     inkscape:cx="7.1473113"
-     inkscape:cy="7.817561"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1027"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
-     inkscape:bbox-paths="true"
-     inkscape:snap-bbox-edge-midpoints="true"
-     inkscape:snap-bbox-midpoints="true"
-     inkscape:object-paths="true"
-     inkscape:object-nodes="true"
-     inkscape:snap-smooth-nodes="true"
-     inkscape:snap-midpoints="true"
-     inkscape:snap-object-midpoints="true"
-     inkscape:snap-center="true"
-     inkscape:snap-page="true"
-     inkscape:snap-intersection-paths="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1036.3622)">
-    <path
-       style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
-       d="m 1,1040.8622 14,0 -7,8 z"
-       id="path3809"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cccc" />
-  </g>
-</svg>
diff --git a/static/img/logout.svg b/static/img/logout.svg
deleted file mode 100644 (file)
index cda383d..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   id="svg2"
-   xml:space="preserve"
-   viewBox="0 0 64 64"
-   version="1.1"
-   inkscape:version="0.48.5 r10040"
-   width="100%"
-   height="100%"
-   sodipodi:docname="logout.svg"><defs
-     id="defs8" /><sodipodi:namedview
-     pagecolor="#666666"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1077"
-     inkscape:window-height="782"
-     id="namedview6"
-     showgrid="false"
-     inkscape:zoom="5.1943359"
-     inkscape:cx="24.357877"
-     inkscape:cy="26.948648"
-     inkscape:window-x="258"
-     inkscape:window-y="33"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="svg2" /><metadata
-     id="metadata8"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><path
-     sodipodi:nodetypes="ssccccccsccccc"
-     style="fill:#3498db;fill-opacity:1"
-     id="path3"
-     d="m 21.267338,42.732446 c 3.741665,3.741354 9.807034,3.741354 13.548699,0 2.776803,-2.77657 3.490747,-6.8354 2.144089,-10.273324 l 5.763494,-5.763661 c 1.49694,-1.496847 1.496489,-3.922567 0,-5.419481 -1.496034,-1.496525 -3.922182,-1.496525 -5.419118,0 l -5.763946,5.764034 c -3.437493,-1.346908 -7.495965,-0.633431 -10.27367,2.144832 -3.740312,3.740676 -3.740765,9.806754 4.52e-4,13.5476 z m 6.435428,-6.434662 c 0.898073,0.897093 0.898073,2.353493 0,3.2516 -0.897621,0.897939 -2.35394,0.897939 -3.251561,0 -0.89717,-0.898107 -0.89717,-2.354507 0,-3.2516 0.897621,-0.898107 2.353489,-0.898107 3.251561,0 z"
-     inkscape:connector-curvature="0" /><path
-     sodipodi:type="arc"
-     style="fill:none;stroke:#3498db;stroke-width:1.80392128;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-     id="path4097"
-     sodipodi:cx="16"
-     sodipodi:cy="16.5"
-     sodipodi:rx="14"
-     sodipodi:ry="12.5"
-     d="m 30,16.5 a 14,12.5 0 1 1 -28,0 14,12.5 0 1 1 28,0 z"
-     transform="matrix(1.5714286,0,0,1.7600001,6.8571426,2.9599978)" /></svg>
\ No newline at end of file
diff --git a/static/img/main-loading-progress.gif b/static/img/main-loading-progress.gif
deleted file mode 100644 (file)
index 4f7a78e..0000000
Binary files a/static/img/main-loading-progress.gif and /dev/null differ
diff --git a/static/img/modal-progress.gif b/static/img/modal-progress.gif
deleted file mode 100644 (file)
index df1657c..0000000
Binary files a/static/img/modal-progress.gif and /dev/null differ
diff --git a/static/img/motioneye-icon.svg b/static/img/motioneye-icon.svg
deleted file mode 100644 (file)
index 93dfd9c..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   width="64"
-   height="64"
-   xml:space="preserve"
-   sodipodi:docname="motioneye-icon.svg"
-   inkscape:export-filename="/home/ccrisan/projects/motioneye/static/img/motioneye-logo.png"
-   inkscape:export-xdpi="960"
-   inkscape:export-ydpi="960"><metadata
-     id="metadata8"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
-     id="defs6" /><sodipodi:namedview
-     pagecolor="#969696"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1920"
-     inkscape:window-height="1030"
-     id="namedview4"
-     showgrid="false"
-     inkscape:zoom="3.2304687"
-     inkscape:cx="-17.830274"
-     inkscape:cy="37.141088"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="g10"
-     showguides="true"
-     inkscape:guide-bbox="true" /><g
-     id="g10"
-     inkscape:groupmode="layer"
-     inkscape:label="ink_ext_XXXXXX"
-     transform="matrix(1.25,0,0,-1.25,0,64)"><path
-       style="fill:#515151;fill-opacity:1;stroke:none"
-       d="m 32,6 c -5.264933,0 -10.15584,1.684955 -14.25,4.375 4.426748,0.474546 8.794165,1.616509 13.75,4.0625 5.014819,-2.5549 10.152222,-3.621792 14.78125,-4.15625 C 42.179879,7.5768125 37.280255,6 32,6 z m 17,5.59375 c -5.21085,0.03453 -12.330126,1.666208 -17.5,4.03125 -5.435679,-1.964064 -11.012887,-4.147356 -17.3125,-4 -0.419974,0.0099 -0.823167,0.03114 -1.25,0.0625 L 8,12.09375 l 4.6875,1.5625 c 6.429941,2.095118 12.084993,4.395085 17.0625,13.03125 1.154473,0 3.425649,5e-4 4.5,5e-4 4.957862,-8.587269 10.904708,-10.983331 17.03125,-13.03175 l 4.625,-1.5 -4.8125,-0.46875 C 50.437916,11.625136 49.744407,11.588818 49,11.59375 z M 12.65625,14.6875 C 8.5126665,19.293305 6,25.316864 6,32 6,46.359404 17.640596,58 32,58 46.359404,58 58,46.359404 58,32 58,25.341351 55.491547,19.287285 51.375,14.6875 49.406504,15.410957 47.442694,16.253379 45.5,17.34375 49.260018,18.43176 52,21.91463 52,26 c 0,4.945036 -4.027245,9 -9,9 -4.217962,0 -7.765819,-2.914031 -8.75,-6.8125 -1.243768,0 -3.31109,0 -4.5,0 C 28.774204,32.087724 25.219217,35 21,35 c -4.972755,0 -9.03125,-4.054964 -9.03125,-9 0,-4.133444 2.851912,-7.644 6.6875,-8.6875 -1.945667,-1.058808 -3.952016,-1.881003 -6,-2.625 z M 21,23 c -1.656854,0 -3,1.343146 -3,3 0,1.656854 1.343146,3 3,3 1.656854,0 3,-1.343146 3,-3 0,-1.656854 -1.343146,-3 -3,-3 z m 22,0 c -1.656854,0 -3,1.343146 -3,3 0,1.656854 1.343146,3 3,3 1.656854,0 3,-1.343146 3,-3 0,-1.656854 -1.343146,-3 -3,-3 z m -11,8 c 0.618234,2.299881 1.278866,4.581981 4,6 l -4,6 -4,-6 c 2.383634,-1.6888 3.483037,-3.758112 4,-6 z"
-       transform="matrix(0.8,0,0,-0.8,0,51.2)"
-       id="path3938"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="scccssccccccccccscsssccssccssccssssssssssccccc" /></g></svg>
\ No newline at end of file
diff --git a/static/img/motioneye-logo.svg b/static/img/motioneye-logo.svg
deleted file mode 100644 (file)
index ea469a3..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 64 64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><g id="g10" transform="matrix(1.25,0,0,-1.25,0,64)"><path id="path3938" d="m32,6c-5.2649,0-10.156,1.685-14.25,4.375,4.4267,0.47455,8.7942,1.6165,13.75,4.0625,5.0148-2.5549,10.152-3.6218,14.781-4.1562-4.101-2.7052-9.001-4.282-14.281-4.282zm17,5.5938c-5.2108,0.03453-12.33,1.6662-17.5,4.0312-5.4357-1.9641-11.013-4.1474-17.312-4-0.41997,0.0099-0.82317,0.03114-1.25,0.0625l-4.938,0.406,4.6875,1.5625c6.4299,2.0951,12.085,4.3951,17.062,13.031,1.1545,0,3.4256,0.0005,4.5,0.0005,4.9579-8.5873,10.905-10.983,17.031-13.032l4.625-1.5-4.8125-0.46875c-0.656-0.062-1.35-0.098-2.094-0.093zm-36.344,3.094c-4.1433,4.605-6.656,10.629-6.656,17.312,0,14.359,11.641,26,26,26s26-11.641,26-26c0-6.659-2.508-12.713-6.625-17.312-1.968,0.723-3.932,1.565-5.875,2.656,3.76,1.088,6.5,4.571,6.5,8.656,0,4.945-4.0272,9-9,9-4.218,0-7.7658-2.914-8.75-6.8125h-4.5c-0.976,3.9-4.531,6.812-8.75,6.812-4.9728,0-9.0312-4.055-9.0312-9,0-4.1334,2.8519-7.644,6.6875-8.6875-1.9457-1.0588-3.952-1.881-6-2.625zm8.344,8.312c-1.6569,0-3,1.3431-3,3s1.3431,3,3,3,3-1.3431,3-3-1.3431-3-3-3zm22,0c-1.6569,0-3,1.3431-3,3s1.3431,3,3,3,3-1.3431,3-3-1.3431-3-3-3zm-11,8c0.61823,2.2999,1.2789,4.582,4,6l-4,6-4-6c2.3836-1.6888,3.483-3.7581,4-6z" transform="matrix(0.8,0,0,-0.8,0,51.2)" fill="#FFF"/></g></svg>
diff --git a/static/img/no-camera.svg b/static/img/no-camera.svg
deleted file mode 100644 (file)
index b7f1800..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   width="160"
-   height="160"
-   xml:space="preserve"
-   sodipodi:docname="no-camera.svg"><metadata
-     id="metadata8"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
-     id="defs6" /><sodipodi:namedview
-     pagecolor="#969696"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1920"
-     inkscape:window-height="1027"
-     id="namedview4"
-     showgrid="false"
-     inkscape:zoom="1"
-     inkscape:cx="194.39325"
-     inkscape:cy="58.632885"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="g10" /><g
-     id="g10"
-     inkscape:groupmode="layer"
-     inkscape:label="ink_ext_XXXXXX"
-     transform="matrix(1.25,0,0,-1.25,0,160)"><g
-       id="g3916"
-       transform="matrix(0.23474178,0,0,0.23474178,-6.596479,-6.504096)"><path
-         id="path28"
-         transform="matrix(0.8,0,0,-0.8,0,599)"
-         d="m 370.46875,230.75 c -25.42532,0 -48.59369,9.59353 -66.125,25.34375 l 57.96875,57.96875 c 3.96533,-0.60105 8.02296,-0.9375 12.15625,-0.9375 44.43125,0 80.4375,36.00625 80.4375,80.4375 0,4.1334 -0.33645,8.19036 -0.9375,12.15625 L 554.5,506.25 c 14.98807,-4.91141 25.8125,-18.99541 25.8125,-35.625 l 0,-162.5 c 0,-20.71 -16.79,-37.5 -37.5,-37.5 l -20.875,0 0,-7.1875 c 0,-8.9975 -7.31375,-16.28125 -16.3125,-16.28125 -8.99625,0 -16.28125,7.28375 -16.28125,16.28125 l 0,7.1875 -39.4375,0 C 431.86011,246.414 402.99373,230.75 370.46875,230.75 z m -13.59375,13 27.5,0 c 8.63,0 15.625,6.99625 15.625,15.625 0,4.41759 -1.83365,8.40792 -4.78125,11.25 l -49.1875,0 c -2.94775,-2.84208 -4.78125,-6.83241 -4.78125,-11.25 0,-8.62875 6.99625,-15.625 15.625,-15.625 z"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         inkscape:connector-curvature="0" /><path
-         id="path3901"
-         d="m 164.25,382.5 c -16.569,0 -30,-13.432 -30,-30 l 0,-130 c 0,-16.568 13.431,-30 30,-30 l 203.075,0 -35.8,35.775 c -9.41466,-5.39918 -20.31889,-8.5 -31.95,-8.5 -35.543,0 -64.35,28.831 -64.35,64.375 0,11.62338 3.11098,22.51462 8.5,31.925 L 177.3,382.5 l -13.05,0 z"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         inkscape:connector-curvature="0" /><path
-         id="path36"
-         transform="matrix(0.8,0,0,-0.8,0,599)"
-         d="m 374.0625,54.96875 c -176.1485,0 -319.5,143.28875 -319.5,319.4375 0,176.15678 143.35153,319.5 319.5,319.5 176.15077,0 319.5,-143.34335 319.5,-319.5 0,-176.14864 -143.3492,-319.4375 -319.5,-319.4375 z m 0,50 c 149.1292,0 269.5,120.31114 269.5,269.4375 0,64.41395 -22.47282,123.45239 -60,169.75 L 204.34375,164.9375 c 46.29727,-37.51599 105.31199,-59.96875 169.71875,-59.96875 z m -209.5,99.71875 379.21875,379.21875 c -46.2979,37.52569 -105.30826,60 -169.71875,60 -149.12653,0 -269.5,-120.36428 -269.5,-269.5 0,-64.4098 22.47246,-123.42872 60,-169.71875 z"
-         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
-         inkscape:connector-curvature="0" /></g></g></svg>
\ No newline at end of file
diff --git a/static/img/no-preview.svg b/static/img/no-preview.svg
deleted file mode 100644 (file)
index 17c39f3..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   width="64"
-   height="64"
-   xml:space="preserve"
-   sodipodi:docname="no-preview.svg"
-   inkscape:export-filename="/home/ccrisan/projects/motioneye/static/img/motioneye-logo.png"
-   inkscape:export-xdpi="960"
-   inkscape:export-ydpi="960"><metadata
-     id="metadata8"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
-     id="defs6" /><sodipodi:namedview
-     pagecolor="#969696"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="1"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1920"
-     inkscape:window-height="1030"
-     id="namedview4"
-     showgrid="false"
-     inkscape:zoom="5.0625"
-     inkscape:cx="53.237951"
-     inkscape:cy="28.106834"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="g10"
-     showguides="true"
-     inkscape:guide-bbox="true" /><g
-     id="g10"
-     inkscape:groupmode="layer"
-     inkscape:label="ink_ext_XXXXXX"
-     transform="matrix(1.25,0,0,-1.25,0,64)"><g
-       id="g3757"
-       style="opacity:0.7"><rect
-         transform="scale(1,-1)"
-         y="-42.400002"
-         x="4"
-         height="34.400002"
-         width="43.200001"
-         id="rect3751"
-         style="fill:none;stroke:#ffffff;stroke-width:1.60000001999999997;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /><path
-         sodipodi:nodetypes="cccccc"
-         transform="matrix(0.8,0,0,-0.8,0,51.2)"
-         inkscape:connector-curvature="0"
-         id="path3753"
-         d="m 11.5,46.5 11,-14 6,6 11,-14 13,22 z"
-         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /><path
-         transform="matrix(1.0251562,0,0,-1.0251562,-4.199999,56.75)"
-         d="m 20.484682,24.142662 a 2.3411067,2.3411067 0 1 1 -4.682213,0 2.3411067,2.3411067 0 1 1 4.682213,0 z"
-         sodipodi:ry="2.3411067"
-         sodipodi:rx="2.3411067"
-         sodipodi:cy="24.142662"
-         sodipodi:cx="18.143576"
-         id="path3755"
-         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.56073773000000005;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
-         sodipodi:type="arc" /></g></g></svg>
\ No newline at end of file
diff --git a/static/img/settings.svg b/static/img/settings.svg
deleted file mode 100644 (file)
index c11c28e..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="96"
-   height="48"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="settings.svg"
-   inkscape:export-filename="/media/data/projects/motioneye/static/img/settings.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="17.085938"
-     inkscape:cx="93.239037"
-     inkscape:cy="25.695745"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1027"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
-     inkscape:bbox-paths="true"
-     inkscape:snap-bbox-edge-midpoints="true"
-     inkscape:snap-bbox-midpoints="true"
-     inkscape:object-paths="true"
-     inkscape:object-nodes="true"
-     inkscape:snap-smooth-nodes="true"
-     inkscape:snap-midpoints="true"
-     inkscape:snap-object-midpoints="true"
-     inkscape:snap-center="true"
-     inkscape:snap-page="true"
-     inkscape:snap-intersection-paths="true"
-     showguides="true"
-     inkscape:guide-bbox="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1004.3622)">
-    <path
-       style="fill:#3498db;fill-opacity:1;stroke:none"
-       d="m 23,30 c -2.045693,0 -3.788228,1.240478 -4.5625,3 L 5,33 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 13.4375,0 c 0.774272,1.759522 2.516807,3 4.5625,3 2.045693,0 3.788228,-1.240478 4.5625,-3 L 43,37 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 L 27.5625,33 C 26.788228,31.240478 25.045693,30 23,30 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
-       transform="translate(0,1004.3622)"
-       id="rect3755"
-       inkscape:connector-curvature="0" />
-    <path
-       style="fill:#3498db;fill-opacity:1;stroke:none"
-       d="m 36,20 c -2.045693,0 -3.788228,1.240478 -4.5625,3 L 5,23 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 26.4375,0 c 0.774272,1.759522 2.516807,3 4.5625,3 2.045693,0 3.788228,-1.240478 4.5625,-3 L 43,27 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 l -2.4375,0 C 39.788228,21.240478 38.045693,20 36,20 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
-       transform="translate(0,1004.3622)"
-       id="rect3757"
-       inkscape:connector-curvature="0" />
-    <path
-       style="fill:#3498db;fill-opacity:1;stroke:none"
-       d="m 15,10 c -2.045692,0 -3.788228,1.240478 -4.5625,3 L 5,13 c -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 l 5.4375,0 c 0.774272,1.759522 2.516808,3 4.5625,3 2.045692,0 3.788228,-1.240478 4.5625,-3 L 43,17 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 L 19.5625,13 C 18.788228,11.240478 17.045692,10 15,10 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z"
-       transform="translate(0,1004.3622)"
-       id="rect3759"
-       inkscape:connector-curvature="0" />
-    <path
-       sodipodi:type="arc"
-       style="fill:none;stroke:#3498db;stroke-width:2.6408689;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       id="path3764"
-       sodipodi:cx="67.901237"
-       sodipodi:cy="6.4814816"
-       sodipodi:rx="20.098766"
-       sodipodi:ry="19.518518"
-       d="m 88.000004,6.4814816 a 20.098766,19.518518 0 1 1 -40.197533,0 20.098766,19.518518 0 1 1 40.197533,0 z"
-       transform="matrix(0.74631447,0,0,0.76850096,21.324322,1023.3813)" />
-    <path
-       inkscape:connector-curvature="0"
-       id="path3766"
-       d="m 82,1030.3622 c 0.554,0 1,-0.446 1,-1 l 0,-2 c 0,-0.554 -0.446,-1 -1,-1 -13.004,0 -8.7461,0 -20,0 -0.554,0 -1,0.446 -1,1 l 0,2 c 0,0.554 0.446,1 1,1 12.4816,0 8.4097,0 20,0 z"
-       style="fill:#3498db;fill-opacity:1;stroke:none"
-       sodipodi:nodetypes="cssccsscc" />
-  </g>
-</svg>
diff --git a/static/img/slider-arrow.svg b/static/img/slider-arrow.svg
deleted file mode 100644 (file)
index 543e0e9..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="11"
-   height="18"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="slider-arrow.svg"
-   inkscape:export-filename="/media/data/projects/motioneye/static/img/slider-arrow.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="1"
-     inkscape:cx="20.539051"
-     inkscape:cy="-1.1141645"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1027"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
-     inkscape:bbox-paths="true"
-     inkscape:snap-bbox-edge-midpoints="true"
-     inkscape:snap-bbox-midpoints="true"
-     inkscape:object-paths="true"
-     inkscape:object-nodes="true"
-     inkscape:snap-smooth-nodes="true"
-     inkscape:snap-midpoints="true"
-     inkscape:snap-object-midpoints="true"
-     inkscape:snap-center="true"
-     inkscape:snap-page="true"
-     inkscape:snap-intersection-paths="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1034.3622)">
-    <path
-       style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       d="m 1,1040.3622 0,10 9,0 0,-10 -4.5,-4.5 z"
-       id="path3809"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cccccc" />
-  </g>
-</svg>
diff --git a/static/img/small-progress.gif b/static/img/small-progress.gif
deleted file mode 100644 (file)
index e0be29f..0000000
Binary files a/static/img/small-progress.gif and /dev/null differ
diff --git a/static/img/top-bar-buttons.svg b/static/img/top-bar-buttons.svg
deleted file mode 100644 (file)
index a10c006..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="80"
-   height="16"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="top-bar-buttons.svg"
-   inkscape:export-filename="/media/data/projects/motioneye/static/img/top-bar-buttons.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#b39a9a"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.42745098"
-     inkscape:pageshadow="2"
-     inkscape:zoom="2.25"
-     inkscape:cx="111.39282"
-     inkscape:cy="5.668431"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1030"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
-     inkscape:bbox-paths="true"
-     inkscape:snap-bbox-edge-midpoints="true"
-     inkscape:snap-bbox-midpoints="true"
-     inkscape:object-paths="true"
-     inkscape:object-nodes="true"
-     inkscape:snap-smooth-nodes="true"
-     inkscape:snap-midpoints="true"
-     inkscape:snap-object-midpoints="true"
-     inkscape:snap-center="true"
-     inkscape:snap-page="true"
-     inkscape:snap-intersection-paths="true"
-     showguides="true"
-     inkscape:guide-bbox="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1036.3622)">
-    <path
-       sodipodi:type="arc"
-       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       id="path3752"
-       sodipodi:cx="7.8036885"
-       sodipodi:cy="8.2353296"
-       sodipodi:rx="5.6186557"
-       sodipodi:ry="5.3845448"
-       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
-       transform="matrix(1.1568604,0,0,1.2071587,-1.0277779,1034.4208)" />
-    <path
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
-       d="m 5.5,5.5 5,5"
-       id="path3756"
-       inkscape:connector-curvature="0"
-       transform="translate(0,1036.3622)"
-       sodipodi:nodetypes="cc" />
-    <path
-       sodipodi:nodetypes="cc"
-       inkscape:connector-curvature="0"
-       id="path3758"
-       d="m 10.5,1041.8622 -5,5"
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
-    <path
-       sodipodi:type="arc"
-       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       id="path3792"
-       sodipodi:cx="7.8036885"
-       sodipodi:cy="8.2353296"
-       sodipodi:rx="5.6186557"
-       sodipodi:ry="5.3845448"
-       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
-       transform="matrix(-1.1568604,0,0,1.2071587,33.027778,1034.4209)" />
-    <path
-       sodipodi:nodetypes="cc"
-       inkscape:connector-curvature="0"
-       id="path3794"
-       d="m 21.5,1046.8622 3,-3"
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
-    <path
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
-       d="m 24.5,1043.8622 0,-2"
-       id="path3796"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
-    <path
-       sodipodi:nodetypes="cc"
-       inkscape:connector-curvature="0"
-       id="path3798"
-       d="m 24.5,1043.8622 2,0"
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
-    <path
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
-       d="m 27.5,1042.8622 -1,1"
-       id="path3800"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
-    <path
-       sodipodi:nodetypes="cc"
-       inkscape:connector-curvature="0"
-       id="path3802"
-       d="m 25.5,1040.8622 -1,1"
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
-    <path
-       transform="matrix(-1.1568604,0,0,1.2071587,49.027778,1034.4209)"
-       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
-       sodipodi:ry="5.3845448"
-       sodipodi:rx="5.6186557"
-       sodipodi:cy="8.2353296"
-       sodipodi:cx="7.8036885"
-       id="path3759"
-       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       sodipodi:type="arc" />
-    <path
-       sodipodi:nodetypes="ccccc"
-       inkscape:connector-curvature="0"
-       id="path3761"
-       d="m 36.5,1041.8622 7,0 0,5 -7,0 z"
-       style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#3498db;fill-opacity:1;stroke:none;stroke-width:8.06000042;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
-       d="m 56.025,1041.1703 c -0.717378,0 -1.358863,0.3074 -1.769063,0.7819 l -1.988437,0 c -0.436275,0 -0.7875,0.3512 -0.7875,0.7875 l 0,3.375 c 0,0.4363 0.351225,0.7875 0.7875,0.7875 l 7.425,0 c 0.436275,0 0.7875,-0.3512 0.7875,-0.7875 l 0,-3.375 c 0,-0.4363 -0.351225,-0.7875 -0.7875,-0.7875 l -0.2925,0 0,-0.1012 a 0.450045,0.450045 0 1 0 -0.9,0 l 0,0.1012 -0.705938,0 c -0.409047,-0.4745 -1.051682,-0.7819 -1.769062,-0.7819 z m -0.36,0.3319 0.72,0 c 0.12465,0 0.225,0.1004 0.225,0.225 l 0,0.09 c 0,0.1247 -0.10035,0.225 -0.225,0.225 l -0.72,0 c -0.12465,0 -0.225,-0.1003 -0.225,-0.225 l 0,-0.09 c 0,-0.1246 0.10035,-0.225 0.225,-0.225 z m 0.36,1.35 c 0.919555,0 1.665,0.7455 1.665,1.665 0,0.9196 -0.745445,1.665 -1.665,1.665 -0.919553,0 -1.665,-0.7454 -1.665,-1.665 0,-0.9195 0.745447,-1.665 1.665,-1.665 z"
-       id="path3803"
-       inkscape:connector-curvature="0" />
-    <path
-       sodipodi:type="arc"
-       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       id="path3815"
-       sodipodi:cx="7.8036885"
-       sodipodi:cy="8.2353296"
-       sodipodi:rx="5.6186557"
-       sodipodi:ry="5.3845448"
-       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
-       transform="matrix(-1.1568604,0,0,1.2071587,65.027778,1034.4209)" />
-    <path
-       transform="matrix(-1.1568604,0,0,1.2071587,81.027778,1034.4209)"
-       d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
-       sodipodi:ry="5.3845448"
-       sodipodi:rx="5.6186557"
-       sodipodi:cy="8.2353296"
-       sodipodi:cx="7.8036885"
-       id="path3817"
-       style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-       sodipodi:type="arc" />
-    <path
-       style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
-       d="m 69.5,1040.8622 6,3.5 -6,3.5 z"
-       id="path3819"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cccc" />
-  </g>
-</svg>
diff --git a/static/img/validation-error.svg b/static/img/validation-error.svg
deleted file mode 100644 (file)
index 6c4d5da..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="20"
-   height="20"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="validation-error.svg"
-   inkscape:export-filename="/media/data/projects/motioneye/static/img/validation-error.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="11.390625"
-     inkscape:cx="-0.48299409"
-     inkscape:cy="28.256052"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1027"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
-     inkscape:bbox-paths="true"
-     inkscape:snap-bbox-edge-midpoints="true"
-     inkscape:snap-bbox-midpoints="true"
-     inkscape:object-paths="true"
-     inkscape:object-nodes="true"
-     inkscape:snap-smooth-nodes="true"
-     inkscape:snap-midpoints="true"
-     inkscape:snap-object-midpoints="true"
-     inkscape:snap-center="true"
-     inkscape:snap-page="true"
-     inkscape:snap-intersection-paths="true"
-     showguides="true"
-     inkscape:guide-bbox="true" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1032.3622)">
-    <path
-       style="fill:#c11c00;fill-opacity:1;stroke:none"
-       d="M 7 3 C 3.1340068 3 0 6.1340067 0 10 C -5.9211895e-16 13.865993 3.1340068 17 7 17 C 10.865993 17 14 13.865993 14 10 C 14 6.1340067 10.865993 3 7 3 z M 6 5.25 L 8.125 5.25 L 8.125 8.625 L 7.8125 11.0625 L 6.3125 11.0625 L 6 8.625 L 6 5.25 z M 6 11.90625 L 8.125 11.90625 L 8.125 14 L 6 14 L 6 11.90625 z "
-       transform="translate(0,1032.3622)"
-       id="path3751" />
-    <g
-       style="font-size:13px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans Bold"
-       id="text3753" />
-  </g>
-</svg>
diff --git a/static/js/css-browser-selector.js b/static/js/css-browser-selector.js
deleted file mode 100644 (file)
index 79759df..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
-CSS Browser Selector js v0.5.3 (July 2, 2013)
-
--- original --
-Rafael Lima (http://rafael.adm.br)
-http://rafael.adm.br/css_browser_selector
-License: http://creativecommons.org/licenses/by/2.5/
-Contributors: http://rafael.adm.br/css_browser_selector#contributors
--- /original --
-
-Fork project: http://code.google.com/p/css-browser-selector/
-Song Hyo-Jin (shj at xenosi.de)
-*/
-function css_browser_selector(n){var b=n.toLowerCase(),f=function(c){return b.indexOf(c)>-1},h="gecko",k="webkit",p="safari",j="chrome",d="opera",e="mobile",l=0,a=window.devicePixelRatio?(window.devicePixelRatio+"").replace(".","_"):"1";var i=[(!(/opera|webtv/.test(b))&&/msie\s(\d+)/.test(b)&&(l=RegExp.$1*1))?("ie ie"+l+((l==6||l==7)?" ie67 ie678 ie6789":(l==8)?" ie678 ie6789":(l==9)?" ie6789 ie9m":(l>9)?" ie9m":"")):(/firefox\/(\d+)\.(\d+)/.test(b)&&(re=RegExp))?h+" ff ff"+re.$1+" ff"+re.$1+"_"+re.$2:f("gecko/")?h:f(d)?d+(/version\/(\d+)/.test(b)?" "+d+RegExp.$1:(/opera(\s|\/)(\d+)/.test(b)?" "+d+RegExp.$2:"")):f("konqueror")?"konqueror":f("blackberry")?e+" blackberry":(f(j)||f("crios"))?k+" "+j:f("iron")?k+" iron":!f("cpu os")&&f("applewebkit/")?k+" "+p:f("mozilla/")?h:"",f("android")?e+" android":"",f("tablet")?"tablet":"",f("j2me")?e+" j2me":f("ipad; u; cpu os")?e+" chrome android tablet":f("ipad;u;cpu os")?e+" chromedef android tablet":f("iphone")?e+" ios iphone":f("ipod")?e+" ios ipod":f("ipad")?e+" ios ipad tablet":f("mac")?"mac":f("darwin")?"mac":f("webtv")?"webtv":f("win")?"win"+(f("windows nt 6.0")?" vista":""):f("freebsd")?"freebsd":(f("x11")||f("linux"))?"linux":"",(a!="1")?" retina ratio"+a:"","js portrait"].join(" ");if(window.jQuery&&!window.jQuery.browser){window.jQuery.browser=l?{msie:1,version:l}:{}}return i}(function(j,b){var c=css_browser_selector(navigator.userAgent);var g=j.documentElement;g.className+=" "+c;var a=c.replace(/^\s*|\s*$/g,"").split(/ +/);b.CSSBS=1;for(var f=0;f<a.length;f++){b["CSSBS_"+a[f]]=1}var e=function(d){return j.documentElement[d]||j.body[d]};if(b.jQuery){(function(q){var h="portrait",k="landscape";var i="smartnarrow",u="smartwide",x="tabletnarrow",r="tabletwide",w=i+" "+u+" "+x+" "+r+" pc";var v=q(g);var s=0,o=0;function d(){try{var l=e("clientWidth"),p=e("clientHeight");if(l>p){v.removeClass(h).addClass(k)}else{v.removeClass(k).addClass(h)}if(l==o){return}o=l;clearTimeout(s)}catch(m){}s=setTimeout(n,100)}function n(){try{v.removeClass(w);v.addClass((o<=360)?i:(o<=640)?u:(o<=768)?x:(o<=1024)?r:"pc")}catch(l){}}q(b).on("resize orientationchange",d).trigger("resize")})(b.jQuery)}})(document,window);
diff --git a/static/js/frame.js b/static/js/frame.js
deleted file mode 100644 (file)
index 276e100..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-
-var refreshDisabled = false;
-
-
-    /* camera frame */
-
-function setupCameraFrame() {
-    var cameraFrameDiv = $('div.camera-frame')
-    var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
-    var cameraProgress = cameraFrameDiv.find('div.camera-progress');
-    var cameraImg = cameraFrameDiv.find('img.camera');
-    var cameraId = cameraFrameDiv.attr('id').substring(6);
-    var progressImg = cameraFrameDiv.find('img.camera-progress');
-    var body = $('body');
-    
-    cameraFrameDiv[0].refreshDivider = 0;
-    cameraFrameDiv[0].streamingFramerate = parseInt(cameraFrameDiv.attr('streaming_framerate')) || 1;
-    cameraFrameDiv[0].streamingServerResize = cameraFrameDiv.attr('streaming_server_resize') == 'True';
-    cameraFrameDiv[0].proto = cameraFrameDiv.attr('proto');
-    cameraFrameDiv[0].url = cameraFrameDiv.attr('url');
-    progressImg.attr('src', staticUrl + 'img/camera-progress.gif');
-    
-    cameraProgress.addClass('visible');
-    cameraPlaceholder.css('opacity', '0');
-    
-    /* fade in */
-    cameraFrameDiv.animate({'opacity': 1}, 100);
-    
-    /* error and load handlers */
-    cameraImg.error(function () {
-        this.error = true;
-        this.loading = 0;
-        
-        cameraImg.addClass('error').removeClass('loading');
-        cameraPlaceholder.css('opacity', 1);
-        cameraProgress.removeClass('visible');
-        cameraFrameDiv.removeClass('motion-detected');
-    });
-    cameraImg.load(function () {
-        if (refreshDisabled) {
-            return; /* refresh temporarily disabled for updating */
-        }
-        
-        this.error = false;
-        this.loading = 0;
-        
-        cameraImg.removeClass('error').removeClass('loading');
-        cameraPlaceholder.css('opacity', 0);
-        cameraProgress.removeClass('visible');
-        
-        /* there's no point in looking for a cookie update more often than once every second */
-        var now = new Date().getTime();
-        if ((!this.lastCookieTime || now - this.lastCookieTime > 1000) && (cameraFrameDiv[0].proto != 'mjpeg')) {
-            if (getCookie('motion_detected_' + cameraId) == 'true') {
-                cameraFrameDiv.addClass('motion-detected');
-            }
-            else {
-                cameraFrameDiv.removeClass('motion-detected');
-            }
-            
-            this.lastCookieTime = now;
-        }
-
-        if (this.naturalWidth / this.naturalHeight > body.width() / body.height()) {
-            cameraImg.css('width', '100%');
-            cameraImg.css('height', 'auto');
-        }
-        else {
-            cameraImg.css('width', 'auto');
-            cameraImg.css('height', '100%');
-        }
-    });
-    
-    cameraImg.addClass('loading');
-}
-
-function refreshCameraFrame() {
-    var $cameraFrame = $('div.camera-frame');
-    var cameraFrame = $cameraFrame[0];
-    var img = $cameraFrame.find('img.camera')[0];
-    var cameraId = cameraFrame.id.substring(6);
-    
-    if (cameraFrame.proto == 'mjpeg') {
-        /* no manual refresh for simple mjpeg cameras */
-        var url = cameraFrame.url.replace('127.0.0.1', window.location.host.split(':')[0]);
-        url += (url.indexOf('?') > 0 ? '&' : '?') + '_=' + new Date().getTime();
-        img.src = url;
-        return; 
-    }
-    
-    /* at a refresh interval of 50ms, the refresh rate is limited to 20 fps */
-    var count = 1000 / (refreshInterval * cameraFrame.streamingFramerate);
-    if (count <= 2) {
-        /* skipping frames (showing the same frame twice) at this rate won't be visible,
-         * while the effective framerate will be as close as possible to the motion's one */
-        count -= 1;
-    }
-    
-    if (img.error) {
-        /* in case of error, decrease the refresh rate to 1 fps */
-        count = 1000 / refreshInterval;
-    }
-    
-    if (cameraFrame.refreshDivider < count) {
-        cameraFrame.refreshDivider++;
-    }
-    else {
-        (function () {
-            if (refreshDisabled) {
-                /* camera refreshing disabled, retry later */
-                
-                return;
-            }
-            
-            if (img.loading) {
-                img.loading++; /* increases each time the camera would refresh but is still loading */
-                
-                if (img.loading > 2 * 1000 / refreshInterval) { /* limits the retry at one every two seconds */
-                    img.loading = 0;
-                }
-                else {
-                    return; /* wait for the previous frame to finish loading */
-                }
-            }
-            
-            var timestamp = new Date().getTime();
-            var uri = baseUri + 'picture/' + cameraId + '/current/?_=' + timestamp;
-            if (cameraFrame.serverSideResize) {
-                uri += '&width=' + img.width;
-            }
-            
-            uri = addAuthParams('GET', uri);
-            img.src = uri;
-            img.loading = 1;
-            
-            cameraFrame.refreshDivider = 0;
-        })();
-    }
-
-    setTimeout(refreshCameraFrame, refreshInterval);
-}
-
-
-    /* startup function */
-
-$(document).ready(function () {
-    setupCameraFrame();
-    refreshCameraFrame();
-});
-
diff --git a/static/js/jquery.min.js b/static/js/jquery.min.js
deleted file mode 100644 (file)
index ce1b6b6..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
-*/
-(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
-}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
-u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/static/js/jquery.mousewheel.js b/static/js/jquery.mousewheel.js
deleted file mode 100644 (file)
index 9d65c71..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-/*! Copyright (c) 2013 Brandon Aaron (http://brandonaaron.net)
- * Licensed under the MIT License (LICENSE.txt).
- *
- * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
- * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
- * Thanks to: Seamus Leahy for adding deltaX and deltaY
- *
- * Version: 3.1.3
- *
- * Requires: 1.2.2+
- */
-
-(function (factory) {
-    if ( typeof define === 'function' && define.amd ) {
-        // AMD. Register as an anonymous module.
-        define(['jquery'], factory);
-    } else if (typeof exports === 'object') {
-        // Node/CommonJS style for Browserify
-        module.exports = factory;
-    } else {
-        // Browser globals
-        factory(jQuery);
-    }
-}(function ($) {
-
-    var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];
-    var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
-    var lowestDelta, lowestDeltaXY;
-
-    if ( $.event.fixHooks ) {
-        for ( var i = toFix.length; i; ) {
-            $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;
-        }
-    }
-
-    $.event.special.mousewheel = {
-        setup: function() {
-            if ( this.addEventListener ) {
-                for ( var i = toBind.length; i; ) {
-                    this.addEventListener( toBind[--i], handler, false );
-                }
-            } else {
-                this.onmousewheel = handler;
-            }
-        },
-
-        teardown: function() {
-            if ( this.removeEventListener ) {
-                for ( var i = toBind.length; i; ) {
-                    this.removeEventListener( toBind[--i], handler, false );
-                }
-            } else {
-                this.onmousewheel = null;
-            }
-        }
-    };
-
-    $.fn.extend({
-        mousewheel: function(fn) {
-            return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
-        },
-
-        unmousewheel: function(fn) {
-            return this.unbind("mousewheel", fn);
-        }
-    });
-
-
-    function handler(event) {
-        var orgEvent = event || window.event,
-            args = [].slice.call(arguments, 1),
-            delta = 0,
-            deltaX = 0,
-            deltaY = 0,
-            absDelta = 0,
-            absDeltaXY = 0,
-            fn;
-        event = $.event.fix(orgEvent);
-        event.type = "mousewheel";
-
-        // Old school scrollwheel delta
-        if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; }
-        if ( orgEvent.detail )     { delta = orgEvent.detail * -1; }
-
-        // New school wheel delta (wheel event)
-        if ( orgEvent.deltaY ) {
-            deltaY = orgEvent.deltaY * -1;
-            delta  = deltaY;
-        }
-        if ( orgEvent.deltaX ) {
-            deltaX = orgEvent.deltaX;
-            delta  = deltaX * -1;
-        }
-
-        // Webkit
-        if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; }
-        if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX * -1; }
-
-        // Look for lowest delta to normalize the delta values
-        absDelta = Math.abs(delta);
-        if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; }
-        absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX));
-        if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; }
-
-        // Get a whole value for the deltas
-        fn = delta > 0 ? 'floor' : 'ceil';
-        delta  = Math[fn](delta / lowestDelta);
-        deltaX = Math[fn](deltaX / lowestDeltaXY);
-        deltaY = Math[fn](deltaY / lowestDeltaXY);
-
-        // Add event and delta to the front of the arguments
-        args.unshift(event, delta, deltaX, deltaY);
-
-        return ($.event.dispatch || $.event.handle).apply(this, args);
-    }
-
-}));
diff --git a/static/js/jquery.timepicker.min.js b/static/js/jquery.timepicker.min.js
deleted file mode 100644 (file)
index 72b6b11..0000000
+++ /dev/null
@@ -1 +0,0 @@
-!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){function b(a){if(a.minTime&&(a.minTime=s(a.minTime)),a.maxTime&&(a.maxTime=s(a.maxTime)),a.durationTime&&"function"!=typeof a.durationTime&&(a.durationTime=s(a.durationTime)),a.disableTimeRanges.length>0){for(var b in a.disableTimeRanges)a.disableTimeRanges[b]=[s(a.disableTimeRanges[b][0]),s(a.disableTimeRanges[b][1])];a.disableTimeRanges=a.disableTimeRanges.sort(function(a,b){return a[0]-b[0]})}return a}function c(b){var c=b.data("timepicker-settings"),d=b.data("timepicker-list");d&&d.length&&(d.remove(),b.data("timepicker-list",!1)),d=a("<ul />",{"class":"ui-timepicker-list"});var e=a("<div />",{"class":"ui-timepicker-wrapper",tabindex:-1});e.css({display:"none",position:"absolute"}).append(d),c.className&&e.addClass(c.className),null===c.minTime&&null===c.durationTime||!c.showDuration||e.addClass("ui-timepicker-with-duration");var f=c.minTime;"function"==typeof c.durationTime?f=s(c.durationTime()):null!==c.durationTime&&(f=c.durationTime);var h=null!==c.minTime?c.minTime:0,j=null!==c.maxTime?c.maxTime:h+v-1;h>=j&&(j+=v);for(var k=c.disableTimeRanges,l=0,m=k.length,n=h;j>=n;n+=60*c.step){var o=n%v,t=a("<li />");if(t.data("time",o),t.text(r(o,c.timeFormat)),(null!==c.minTime||null!==c.durationTime)&&c.showDuration){var u=a("<span />");u.addClass("ui-timepicker-duration"),u.text(" ("+q(n-f)+")"),t.append(u)}m>l&&(o>=k[l][1]&&(l+=1),k[l]&&o>=k[l][0]&&o<k[l][1]&&t.addClass("ui-timepicker-disabled")),d.append(t)}e.data("timepicker-input",b),b.data("timepicker-list",e);var w=c.appendTo;"string"==typeof w?w=a(w):"function"==typeof w&&(w=w(b)),w.append(e),i(b,d),d.on("click","li",function(){b.off("focus.timepicker"),b.on("focus.timepicker-ie-hack",function(){b.off("focus.timepicker-ie-hack"),b.on("focus.timepicker",y.show)}),g(b)||b[0].focus(),d.find("li").removeClass("ui-timepicker-selected"),a(this).addClass("ui-timepicker-selected"),p(b)&&(b.trigger("hideTimepicker"),e.hide())})}function d(){return new Date(1970,1,1,0,0,0)}function e(b){"ontouchstart"in document?a("body").on("touchstart.ui-timepicker",f):(a("body").on("mousedown.ui-timepicker",f),b.closeOnWindowScroll&&a(window).on("scroll.ui-timepicker",f))}function f(b){var c=a(b.target),d=c.closest(".ui-timepicker-input");0===d.length&&0===c.closest(".ui-timepicker-wrapper").length&&(y.hide(),a("body").unbind(".ui-timepicker"),a(window).unbind(".ui-timepicker"))}function g(a){var b=a.data("timepicker-settings");return(window.navigator.msMaxTouchPoints||"ontouchstart"in document)&&b.disableTouchKeyboard}function h(b,c,d){if(!d&&0!==d)return!1;var e=b.data("timepicker-settings"),f=!1,g=30*e.step;return c.find("li").each(function(b,c){var e=a(c),h=e.data("time")-d;return Math.abs(h)<g||h==g?(f=e,!1):void 0}),f}function i(a,b){b.find("li").removeClass("ui-timepicker-selected");var c=s(k(a));if(c){var d=h(a,b,c);if(d){var e=d.offset().top-b.offset().top;(e+d.outerHeight()>b.outerHeight()||0>e)&&b.scrollTop(b.scrollTop()+d.position().top-d.outerHeight()),d.addClass("ui-timepicker-selected")}}}function j(){if(""!==this.value){var b=a(this),c=b.data("timepicker-list");if(!c||!c.is(":visible")){var d=s(this.value);if(null===d)return b.trigger("timeFormatError"),void 0;var e=b.data("timepicker-settings"),f=!1;if(null!==e.minTime&&d<e.minTime?f=!0:null!==e.maxTime&&d>e.maxTime&&(f=!0),a.each(e.disableTimeRanges,function(){return d>=this[0]&&d<this[1]?(f=!0,!1):void 0}),e.forceRoundTime){var g=d%(60*e.step);g>=30*e.step?d+=60*e.step-g:d-=g}var h=r(d,e.timeFormat);f?l(b,h,"error")&&b.trigger("timeRangeError"):l(b,h)}}}function k(a){return a.is("input")?a.val():a.data("ui-timepicker-value")}function l(a,b,c){return a.data("ui-timepicker-value")!=b?(a.data("ui-timepicker-value",b),a.is("input")&&a.val(b),"select"==c?a.trigger("selectTime").trigger("changeTime").trigger("change"):"error"!=c&&a.trigger("changeTime"),!0):(a.trigger("selectTime"),!1)}function m(b){var c=a(this),d=c.data("timepicker-list");if(!d||!d.is(":visible")){if(40!=b.keyCode)return n(b,c);g(c)||c.focus()}switch(b.keyCode){case 13:return p(c)&&y.hide.apply(this),b.preventDefault(),!1;case 38:var e=d.find(".ui-timepicker-selected");return e.length?e.is(":first-child")||(e.removeClass("ui-timepicker-selected"),e.prev().addClass("ui-timepicker-selected"),e.prev().position().top<e.outerHeight()&&d.scrollTop(d.scrollTop()-e.outerHeight())):(d.find("li").each(function(b,c){return a(c).position().top>0?(e=a(c),!1):void 0}),e.addClass("ui-timepicker-selected")),!1;case 40:return e=d.find(".ui-timepicker-selected"),0===e.length?(d.find("li").each(function(b,c){return a(c).position().top>0?(e=a(c),!1):void 0}),e.addClass("ui-timepicker-selected")):e.is(":last-child")||(e.removeClass("ui-timepicker-selected"),e.next().addClass("ui-timepicker-selected"),e.next().position().top+2*e.outerHeight()>d.outerHeight()&&d.scrollTop(d.scrollTop()+e.outerHeight())),!1;case 27:d.find("li").removeClass("ui-timepicker-selected"),y.hide();break;case 9:y.hide();break;default:return n(b,c)}}function n(a,b){return!b.data("timepicker-settings").disableTextInput||a.ctrlKey||a.altKey||a.metaKey||2!=a.keyCode&&a.keyCode<46}function o(b){var c=a(this),d=c.data("timepicker-list");if(!d||!d.is(":visible"))return!0;switch(b.keyCode){case 96:case 97:case 98:case 99:case 100:case 101:case 102:case 103:case 104:case 105:case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 65:case 77:case 80:case 186:case 8:case 46:i(c,d);break;default:return}}function p(a){var b=a.data("timepicker-settings"),c=a.data("timepicker-list"),d=null,e=c.find(".ui-timepicker-selected");if(e.hasClass("ui-timepicker-disabled"))return!1;if(e.length?d=e.data("time"):k(a)&&(d=s(k(a)),i(a,c)),null!==d){var f=r(d,b.timeFormat);l(a,f,"select")}return!0}function q(a){var b,c=Math.round(a/60);if(Math.abs(c)<60)b=[c,x.mins];else if(60==c)b=["1",x.hr];else{var d=(c/60).toFixed(1);"."!=x.decimal&&(d=d.replace(".",x.decimal)),b=[d,x.hrs]}return b.join(" ")}function r(a,b){if(null!==a){for(var c,d,e=new Date(u.valueOf()+1e3*a),f="",g=0;g<b.length;g++)switch(d=b.charAt(g)){case"a":f+=e.getHours()>11?"pm":"am";break;case"A":f+=e.getHours()>11?"PM":"AM";break;case"g":c=e.getHours()%12,f+=0===c?"12":c;break;case"G":f+=e.getHours();break;case"h":c=e.getHours()%12,0!==c&&10>c&&(c="0"+c),f+=0===c?"12":c;break;case"H":c=e.getHours(),f+=c>9?c:"0"+c;break;case"i":var h=e.getMinutes();f+=h>9?h:"0"+h;break;case"s":a=e.getSeconds(),f+=a>9?a:"0"+a;break;default:f+=d}return f}}function s(a){if(""===a)return null;if(!a||a+0==a)return a;"object"==typeof a&&(a=a.getHours()+":"+t(a.getMinutes())+":"+t(a.getSeconds())),a=a.toLowerCase(),new Date(0);var b;if(-1===a.indexOf(":")?(b=a.match(/^([0-9]):?([0-5][0-9])?:?([0-5][0-9])?\s*([pa]?)m?$/),b||(b=a.match(/^([0-2][0-9]):?([0-5][0-9])?:?([0-5][0-9])?\s*([pa]?)m?$/))):b=a.match(/^(\d{1,2})(?::([0-5][0-9]))?(?::([0-5][0-9]))?\s*([pa]?)m?$/),!b)return null;var c,d=parseInt(1*b[1],10);c=b[4]?12==d?"p"==b[4]?12:0:d+("p"==b[4]?12:0):d;var e=1*b[2]||0,f=1*b[3]||0;return 3600*c+60*e+f}function t(a){return("0"+a).slice(-2)}var u=d(),v=86400,w={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,timeFormat:"g:ia",scrollDefaultNow:!1,scrollDefaultTime:!1,selectOnBlur:!1,disableTouchKeyboard:!0,forceRoundTime:!1,appendTo:"body",disableTimeRanges:[],closeOnWindowScroll:!1,disableTextInput:!1},x={decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},y={init:function(c){return this.each(function(){var d=a(this);if("SELECT"==d[0].tagName){for(var e={type:"text",value:d.val()},f=d[0].attributes,g=0;g<f.length;g++)e[f[g].nodeName]=f[g].nodeValue;var h=a("<input />",e);d.replaceWith(h),d=h}var i=a.extend({},w);c&&(i=a.extend(i,c)),i.lang&&(x=a.extend(x,i.lang)),i=b(i),d.data("timepicker-settings",i),d.prop("autocomplete","off"),d.on("click.timepicker focus.timepicker",y.show),d.on("change.timepicker",j),d.on("keydown.timepicker",m),d.on("keyup.timepicker",o),d.addClass("ui-timepicker-input"),j.call(d.get(0))})},show:function(){var b=a(this),d=b.data("timepicker-settings");g(b)&&b.blur();var f=b.data("timepicker-list");if(!b.prop("readonly")&&(f&&0!==f.length&&"function"!=typeof d.durationTime||(c(b),f=b.data("timepicker-list")),!f.is(":visible"))){y.hide(),f.show(),b.offset().top+b.outerHeight(!0)+f.outerHeight()>a(window).height()+a(window).scrollTop()?f.offset({left:b.offset().left+parseInt(f.css("marginLeft").replace("px",""),10),top:b.offset().top-f.outerHeight()+parseInt(f.css("marginTop").replace("px",""),10)}):f.offset({left:b.offset().left+parseInt(f.css("marginLeft").replace("px",""),10),top:b.offset().top+b.outerHeight()+parseInt(f.css("marginTop").replace("px",""),10)});var i=f.find(".ui-timepicker-selected");if(i.length||(k(b)?i=h(b,f,s(k(b))):d.scrollDefaultNow?i=h(b,f,s(new Date)):d.scrollDefaultTime!==!1&&(i=h(b,f,s(d.scrollDefaultTime)))),i&&i.length){var j=f.scrollTop()+i.position().top-i.outerHeight();f.scrollTop(j)}else f.scrollTop(0);e(d),b.trigger("showTimepicker")}},hide:function(){a(".ui-timepicker-wrapper:visible").each(function(){var b=a(this),c=b.data("timepicker-input"),d=c.data("timepicker-settings");d&&d.selectOnBlur&&p(c),b.hide(),c.trigger("hideTimepicker")})},option:function(c,d){var e=this,f=e.data("timepicker-settings"),g=e.data("timepicker-list");if("object"==typeof c)f=a.extend(f,c);else if("string"==typeof c&&"undefined"!=typeof d)f[c]=d;else if("string"==typeof c)return f[c];return f=b(f),e.data("timepicker-settings",f),g&&(g.remove(),e.data("timepicker-list",!1)),e},getSecondsFromMidnight:function(){return s(k(this))},getTime:function(){var a=this,b=new Date;return b.setHours(0,0,0,0),new Date(b.valueOf()+1e3*s(k(a)))},setTime:function(a){var b=this,c=r(s(a),b.data("timepicker-settings").timeFormat);l(b,c)},remove:function(){var a=this;a.hasClass("ui-timepicker-input")&&(a.removeAttr("autocomplete","off"),a.removeClass("ui-timepicker-input"),a.removeData("timepicker-settings"),a.off(".timepicker"),a.data("timepicker-list")&&a.data("timepicker-list").remove(),a.removeData("timepicker-list"))}};a.fn.timepicker=function(b){return y[b]?y[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?(a.error("Method "+b+" does not exist on jQuery.timepicker"),void 0):y.init.apply(this,arguments)}});
\ No newline at end of file
diff --git a/static/js/main.js b/static/js/main.js
deleted file mode 100644 (file)
index ecdf58f..0000000
+++ /dev/null
@@ -1,3917 +0,0 @@
-
-var pushConfigs = {};
-var pushConfigReboot = false;
-var refreshDisabled = {}; /* dictionary indexed by cameraId, tells if refresh is disabled for a given camera */
-var fullScreenCameraId = null;
-var inProgress = false;
-var refreshInterval = 50; /* milliseconds */
-var username = '';
-var password = '';
-var baseUri = null;
-var signatureRegExp = new RegExp('[^a-zA-Z0-9/?_.=&{}\\[\\]":, _-]', 'g');
-
-
-    /* utils */
-
-var sha1 = (function () {
-    function hash(msg) {
-        var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
-
-        msg += String.fromCharCode(0x80);
-
-        var l = msg.length / 4 + 2;
-        var N = Math.ceil(l / 16);
-        var M = new Array(N);
-
-        for (var i = 0; i < N; i++) {
-            M[i] = new Array(16);
-            for (var j = 0; j < 16; j++) {
-                M[i][j] = (msg.charCodeAt(i * 64 + j * 4) << 24) | (msg.charCodeAt(i * 64 + j * 4 + 1) << 16) | 
-                (msg.charCodeAt(i * 64 + j * 4 + 2) << 8) | (msg.charCodeAt(i * 64 + j * 4 + 3));
-            }
-        }
-        M[N-1][14] = ((msg.length-1) * 8) / Math.pow(2, 32);
-        M[N-1][14] = Math.floor(M[N-1][14]);
-        M[N-1][15] = ((msg.length-1) * 8) & 0xffffffff;
-
-        var H0 = 0x67452301;
-        var H1 = 0xefcdab89;
-        var H2 = 0x98badcfe;
-        var H3 = 0x10325476;
-        var H4 = 0xc3d2e1f0;
-
-        var W = new Array(80);
-        var a, b, c, d, e;
-        for (var i = 0; i < N; i++) {
-            for (var t = 0; t < 16; t++) W[t] = M[i][t];
-            for (var t = 16; t < 80; t++) W[t] = ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
-
-            a = H0; b = H1; c = H2; d = H3; e = H4;
-
-            for (var t = 0; t < 80; t++) {
-                var s = Math.floor(t / 20);
-                var T = (ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t]) & 0xffffffff;
-                e = d;
-                d = c;
-                c = ROTL(b, 30);
-                b = a;
-                a = T;
-            }
-
-            H0 = (H0 + a) & 0xffffffff;
-            H1 = (H1 + b) & 0xffffffff; 
-            H2 = (H2 + c) & 0xffffffff; 
-            H3 = (H3 + d) & 0xffffffff; 
-            H4 = (H4 + e) & 0xffffffff;
-        }
-
-        return toHexStr(H0) + toHexStr(H1) + toHexStr(H2) + toHexStr(H3) + toHexStr(H4);
-    }
-
-    function f(s, x, y, z)  {
-        switch (s) {
-            case 0: return (x & y) ^ (~x & z);
-            case 1: return x ^ y ^ z;
-            case 2: return (x & y) ^ (x & z) ^ (y & z);
-            case 3: return x ^ y ^ z;
-        }
-    }
-
-    function ROTL(x, n) {
-        return (x << n) | (x >>> (32 - n));
-    }
-
-    function toHexStr(n) {
-        var s = "", v;
-        for (var i = 7; i >= 0; i--) {
-            v = (n >>> (i * 4)) & 0xf;
-            s += v.toString(16);
-        }
-        return s;
-    }
-    
-    return hash;
-}());
-
-function splitUrl(url) {
-    if (!url) {
-        url = window.location.href;
-    }
-    
-    var parts = url.split('?');
-    if (parts.length < 2 || parts[1].length === 0) {
-        return {baseUrl: parts[0], params: {}};
-    }
-    
-    var baseUrl = parts[0];
-    var paramStr = parts[1];
-    
-    parts = paramStr.split('&');
-    var params = {};
-    
-    for (var i = 0; i < parts.length; i++) {
-        var pair = parts[i].split('=');
-        params[pair[0]] = pair[1];
-    }
-    
-    return {baseUrl: baseUrl, params: params};
-}
-
-function qualifyUrl(url) {
-    var a = document.createElement('a');
-    a.href = url;
-    return a.href;
-}
-
-function qualifyUri(uri) {
-    var url = qualifyUrl(uri);
-    var pos = url.indexOf('//');
-    if (pos === -1) { /* not a full url */
-        return url;
-    }
-    
-    url = url.substring(pos + 2);
-    pos = url.indexOf('/');
-    if (pos === -1) { /* root with no trailing slash */
-        return '';
-    }
-    
-    return url.substring(pos);
-}
-        
-function computeSignature(method, uri, body) {
-    uri = qualifyUri(uri);
-    
-    var parts = splitUrl(uri);
-    var query = parts.params;
-    var baseUrl = parts.baseUrl;
-    
-    /* sort query arguments alphabetically */
-    query = Object.keys(query).map(function (key) {return {key: key, value: decodeURIComponent(query[key])};});
-    query = query.filter(function (q) {return q.key !== '_signature';});
-    query.sortKey(function (q) {return q.key;});
-    query = query.map(function (q) {return q.key + '=' + encodeURIComponent(q.value);}).join('&');
-    uri = baseUrl + '?' + query;
-    uri = uri.replace(signatureRegExp, '-');
-    body = body && body.replace(signatureRegExp, '-');
-    var password = window.password.replace(signatureRegExp, '-');
-    
-    return sha1(method + ':' + uri + ':' + (body || '') + ':' + password).toLowerCase();
-}
-
-function addAuthParams(method, url, body) {
-    if (!window.username) {
-        return url;
-    }
-
-    if (url.indexOf('?') < 0) {
-        url += '?';
-    }
-    else {
-        url += '&';
-    }
-    
-    url += '_username=' + window.username;
-    if (window._loginDialogSubmitted) {
-        url += '&_login=true';
-        delete _loginDialogSubmitted;
-    }
-    var signature = computeSignature(method, url, body);
-    url += '&_signature=' + signature;
-
-    return url;
-}
-
-function isAdmin() {
-    return username === adminUsername;
-}
-
-function ajax(method, url, data, callback, error, timeout) {
-    var origUrl = url;
-    var origData = data;
-    
-    if (url.indexOf('?') < 0) {
-        url += '?';
-    }
-    else {
-        url += '&';
-    }
-    
-    url += '_=' + new Date().getTime();
-
-    var json = false;
-    var processData = true;
-    if (method == 'POST') {
-        if (window.FormData && (data instanceof FormData)) {
-            json = false;
-            processData = false;
-        }
-        else if (typeof data == 'object') {
-            data = JSON.stringify(data);
-            json = true;
-        }
-    }
-    else { /* assuming GET */
-        if (data) {
-            url += '&' + $.param(data);
-            data = null;
-        }
-    }
-    
-    url = addAuthParams(method, url, processData ? data : null);
-    
-    var options = {
-        type: method,
-        url: url,
-        data: data,
-        timeout: timeout || 300 * 1000,
-        success: function (data) {
-            if (data && data.error == 'unauthorized') {
-                if (data.prompt) {
-                    runLoginDialog(function () {
-                        ajax(method, origUrl, origData, callback, error);
-                    });
-                }
-                
-                window._loginRetry = true;
-            }
-            else {
-                delete window._loginRetry;
-                if (callback) {
-                    $('body').toggleClass('admin', isAdmin());
-                    callback(data);
-                }
-            }
-        },
-        contentType: json ? 'application/json' : false,
-        processData: processData,
-        error: error || function (request, options, error) {
-            showErrorMessage();
-            if (callback) {
-                callback();
-            }
-        }
-    };
-    
-    $.ajax(options);
-}
-
-Object.keys = Object.keys || (function () {
-    var hasOwnProperty = Object.prototype.hasOwnProperty;
-    var hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString');
-    var dontEnums = [
-        'toString',
-        'toLocaleString',
-        'valueOf',
-        'hasOwnProperty',
-        'isPrototypeOf',
-        'propertyIsEnumerable',
-        'constructor'
-    ];
-    var dontEnumsLength = dontEnums.length;
-
-    return function (obj) {
-        if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
-            return [];
-        }
-
-        var result = [];
-        for (var prop in obj) {
-            if (hasOwnProperty.call(obj, prop)) {
-                result.push(prop);
-            }
-        }
-
-        if (hasDontEnumBug) {
-            for (var i = 0; i < dontEnumsLength; i++) {
-                if (hasOwnProperty.call(obj, dontEnums[i])) {
-                    result.push(dontEnums[i]);
-                }
-            }
-        }
-        
-        return result;
-    };
-})();
-
-Object.update = function (dest, source) {
-    for (var key in source) {
-        if (!source.hasOwnProperty(key)) {
-            continue;
-        }
-        
-        dest[key] = source[key];
-    }
-};
-
-Array.prototype.indexOf = Array.prototype.indexOf || function (obj) {
-    for (var i = 0; i < this.length; i++) {
-        if (this[i] === obj) {
-            return i;
-        }
-    }
-    
-    return -1;
-};
-
-Array.prototype.forEach = Array.prototype.forEach || function (callback, thisArg) {
-    for (var i = 0; i < this.length; i++) {
-        callback.call(thisArg, this[i], i, this);
-    }
-};
-
-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) {
-        if (uniqueElements.indexOf(element, Utils.equals) === -1) {
-            uniqueElements.push(element);
-        }
-    });
-    
-    return uniqueElements;
-};
-
-Array.prototype.filter = function (func, thisArg) {
-    var filtered = [];
-    for (var i = 0; i < this.length; i++) {
-        if (func.call(thisArg, this[i], i, this)) {
-            filtered.push(this[i]);
-        }
-    }
-    
-    return filtered;
-};
-
-Array.prototype.map = function (func, thisArg) {
-    var mapped = [];
-    for (var i = 0; i < this.length; i++) {
-        mapped.push(func.call(thisArg, this[i], i, this));
-    }
-    
-    return mapped;
-};
-
-Array.prototype.sortKey = function (keyFunc, reverse) {
-    this.sort(function (e1, e2) {
-        var k1 = keyFunc(e1);
-        var k2 = keyFunc(e2);
-        
-        if ((k1 < k2 && !reverse) || (k1 > k2 && reverse)) {
-            return -1;
-        }
-        else if ((k1 > k2 && !reverse) || (k1 < k2 && reverse)) {
-            return 1;
-        }
-        else {
-            return 0;
-        }
-    });
-};
-
-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) {
-        s = s.substring(0, p) + newStr + s.substring(p + oldStr.length, s.length);
-    }
-    
-    return s.toString();
-};
-
-function getCookie(name) {
-    if (document.cookie.length <= 0) {
-        return null;
-    }
-
-    var start = document.cookie.indexOf(name + '=');
-    if (start == -1) {
-        return null;
-    }
-     
-    var start = start + name.length + 1;
-    var end = document.cookie.indexOf(';', start);
-    if (end == -1) {
-        end = document.cookie.length;
-    }
-    
-    return unescape(document.cookie.substring(start, end));
-}
-
-function setCookie(name, value, days) {
-    var date, expires;
-    if (days) {
-        date = new Date();
-        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
-        expires = 'expires=' + date.toGMTString();
-    }
-    else {
-        expires = '';
-    }
-
-    document.cookie = name + '=' + value + '; ' + expires + '; path=/';
-}
-
-function remCookie(name) {
-    document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
-}
-
-function showErrorMessage(message) {
-    if (message == null || message == true) {
-        message = 'An error occurred. Refreshing is recommended.';
-    }
-    
-    showPopupMessage(message, 'error');
-}
-
-function doLogout() {
-    setCookie('username', '_');
-    window.location.reload(true);
-}
-
-
-/* UI initialization */
-
-function initUI() {
-    /* checkboxes */
-    makeCheckBox($('input[type=checkbox].styled'));
-
-    /* sliders */
-    $('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($('div.progress-bar'));
-
-    /* text validators */
-    makeTextValidator($('tr[required=true] input[type=text]'), true);
-    makeTextValidator($('tr[required=true] input[type=password]'), true);
-
-    /* number validators */
-    $('input[type=text].number').each(function () {
-        var $this = $(this);
-        var $tr = $this.parent().parent();
-        makeNumberValidator($this, Number($tr.attr('min')), Number($tr.attr('max')),
-                Boolean($tr.attr('floating')), Boolean($tr.attr('sign')), Boolean($tr.attr('required')));
-    });
-
-    /* time validators */
-    makeTimeValidator($('input[type=text].time'));
-    
-    /* custom validators */
-    makeCustomValidator($('#deviceNameEntry'), function (value) {
-        if (!value) {
-            return 'this field is required';
-        }
-
-        if (!value.toLowerCase().match(new RegExp('^[a-z0-9\-\_\+\ ]*$'))) {
-            return "special characters are not allowed in camera's name";
-        }
-        
-        return true;
-    }, '');
-    makeCustomValidator($('#rootDirectoryEntry'), function (value) {
-        if ($('#storageDeviceSelect').val() == 'custom-path' && String(value).trim() == '/') {
-            return 'files cannot be created directly on the root of your system';
-        }
-        
-        return true;
-    }, '');
-    makeCustomValidator($('#emailAddressesEntry'), function (value) {
-        if (!value.toLowerCase().match(new RegExp('^[a-z0-9\-\_\+\.\@\^\~\, ]+$'))) {
-            return 'enter a list of comma-separated valid email addresses';
-        }
-        
-        return true;
-    }, '');
-    $('tr[validate] input[type=text]').each(function () {
-        var $this = $(this);
-        var $tr = $this.parent().parent();
-        var required = $tr.attr('required');
-        var validate = $tr.attr('validate');
-        if (!validate) {
-            return;
-        }
-
-        makeCustomValidator($this, function (value) {
-            if (!value && required) {
-                return 'this field is required';
-            }
-
-            if (!value.toLowerCase().match(new RegExp(validate))) {
-                return 'enter a valid value';
-            }
-
-            return true;
-        }, '');
-    });
-    
-    /* input value processors */
-    makeStrippedInput($('tr[strip=true] input[type=text]'));
-    makeStrippedInput($('tr[strip=true] input[type=password]'));
-    
-    function checkMinimizeSection() {
-        var $switch = $(this);
-        var $sectionDiv = $switch.parents('div.settings-section-title:eq(0)');
-        
-        var $minimizeSpan = $switch.parent().find('span.minimize');
-        if ($switch.is(':checked') && !$minimizeSpan.hasClass('open')) {
-            $minimizeSpan.addClass('open');
-        }
-        else if (!$switch.is(':checked') && $minimizeSpan.hasClass('open') && !$sectionDiv.attr('minimize-switch-independent')) {
-            $minimizeSpan.removeClass('open');
-        }
-    }
-
-    /* ui elements that enable/disable other ui elements */
-    $('#motionEyeSwitch').change(updateConfigUi);
-    $('#showAdvancedSwitch').change(updateConfigUi);
-    $('#storageDeviceSelect').change(updateConfigUi);
-    $('#resolutionSelect').change(updateConfigUi);
-    $('#leftTextSelect').change(updateConfigUi);
-    $('#rightTextSelect').change(updateConfigUi);
-    $('#captureModeSelect').change(updateConfigUi);
-    $('#autoNoiseDetectSwitch').change(updateConfigUi);
-    $('#videoDeviceSwitch').change(checkMinimizeSection).change(updateConfigUi);
-    $('#textOverlaySwitch').change(checkMinimizeSection).change(updateConfigUi);
-    $('#videoStreamingSwitch').change(checkMinimizeSection).change(updateConfigUi);
-    $('#streamingServerResizeSwitch').change(updateConfigUi);
-    $('#stillImagesSwitch').change(checkMinimizeSection).change(updateConfigUi);
-    $('#preservePicturesSelect').change(updateConfigUi);
-    $('#motionDetectionSwitch').change(checkMinimizeSection).change(updateConfigUi);
-    $('#motionMoviesSwitch').change(checkMinimizeSection).change(updateConfigUi);
-    $('#preserveMoviesSelect').change(updateConfigUi);
-    $('#emailNotificationsSwitch').change(updateConfigUi);
-    $('#webHookNotificationsSwitch').change(updateConfigUi);
-    $('#commandNotificationsSwitch').change(updateConfigUi);
-    $('#workingScheduleSwitch').change(checkMinimizeSection).change(updateConfigUi);
-    
-    $('#mondayEnabledSwitch').change(updateConfigUi);
-    $('#tuesdayEnabledSwitch').change(updateConfigUi);
-    $('#wednesdayEnabledSwitch').change(updateConfigUi);
-    $('#thursdayEnabledSwitch').change(updateConfigUi);
-    $('#fridayEnabledSwitch').change(updateConfigUi);
-    $('#saturdayEnabledSwitch').change(updateConfigUi);
-    $('#sundayEnabledSwitch').change(updateConfigUi);
-    
-    /* minimizable sections */
-    $('span.minimize').click(function () {
-        $(this).toggleClass('open');
-        
-        /* enable the section switch when unminimizing */
-        if ($(this).hasClass('open')) {
-            var sectionSwitch = $(this).parent().find('input[type=checkbox]');
-            var sectionSwitchDiv = $(this).parent().find('div.check-box');
-            var sectionDiv = $(this).parents('div.settings-section-title:eq(0)');
-            if (sectionSwitch.length && !sectionSwitch.is(':checked') &&
-                !sectionSwitchDiv[0]._hideNull && !sectionDiv.attr('minimize-switch-independent')) {
-
-                sectionSwitch[0].checked = true;
-                sectionSwitch.change();
-            }
-        }
-            
-        updateConfigUi();
-    });
-
-    $('a.settings-section-title').click(function () {
-        $(this).parent().find('span.minimize').click();
-    });
-
-    /* additional configs */
-    var seenDependNames = {};
-    $('tr[depends]').each(function () {
-        var $tr = $(this);
-        var depends = $tr.attr('depends').split(' ');
-        depends.forEach(function (depend) {
-            depend = depend.split('=')[0];
-            depend = depend.replace(new RegExp('[^a-zA-Z0-9_]', 'g'), '');
-            
-            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 = this.value.trim();
-    });
-    
-    $('#rootDirectoryEntry').change(function () {
-        if (this.value.charAt(0) !== '/') {
-            this.value = '/' + this.value;
-        }
-    });
-    
-    /* fetch & push handlers */
-    $('#cameraSelect').focus(function () {
-        /* remember the previously selected index */
-        this._prevSelectedIndex = this.selectedIndex;
-    
-    }).change(function () {
-        if ($('#cameraSelect').val() === 'add') {
-            runAddCameraDialog();
-            this.selectedIndex = this._prevSelectedIndex;
-        }
-        else {
-            this._prevSelectedIndex = this.selectedIndex;
-            beginProgress([$(this).val()]);
-            fetchCurrentCameraConfig(endProgress);
-        }
-    });
-    $('input.main-config, select.main-config, textarea.main-config').change(function () {
-        pushMainConfig($(this).parents('tr:eq(0)').attr('reboot') == 'true');
-    });
-    $('input.camera-config, select.camera-config, textarea.camera-config').change(function () {
-        pushCameraConfig($(this).parents('tr:eq(0)').attr('reboot') == 'true');
-    });
-    
-    /* streaming framerate must be >= device framerate */
-    $('#framerateSlider').change(function (val) {
-        var value = Number($('#framerateSlider').val());
-        var streamingValue = Number($('#streamingFramerateSlider').val());
-        
-        if (streamingValue < value) {
-            $('#streamingFramerateSlider').val(value).change();
-        }
-    });
-    
-    /* preview controls */
-    $('#brightnessSlider').change(function () {pushPreview('brightness');});
-    $('#contrastSlider').change(function () {pushPreview('contrast');});
-    $('#saturationSlider').change(function () {pushPreview('saturation');});
-    $('#hueSlider').change(function () {pushPreview('hue');});
-    
-    /* apply button */
-    $('#applyButton').click(function () {
-        if ($(this).hasClass('progress')) {
-            return; /* in progress */
-        }
-        
-        doApply();
-    });
-    
-    /* shut down button */
-    $('#shutDownButton').click(function () {
-        doShutDown();
-    });
-    
-    /* reboot button */
-    $('#rebootButton').click(function () {
-        doReboot();
-    });
-    
-    /* whenever the window is resized,
-     * if a modal dialog is visible, it should be repositioned */
-    $(window).resize(updateModalDialogPosition);
-    
-    /* remove camera button */
-    $('div.button.rem-camera-button').click(doRemCamera);
-    
-    /* logout button */
-    $('div.button.logout-button').click(doLogout);
-    
-    /* autoselect urls in read-only entries */
-    $('#streamingSnapshotUrlEntry:text, #streamingMjpgUrlEntry:text, #streamingEmbedUrlEntry:text').click(function () {
-        this.select();
-    });
-}
-
-
-    /* settings */
-
-function openSettings(cameraId) {
-    if (cameraId != null) {
-        $('#cameraSelect').val(cameraId).change();
-    }
-    
-    $('div.settings').addClass('open').removeClass('closed');
-    $('div.page-container').addClass('stretched');
-    $('div.settings-top-bar').addClass('open').removeClass('closed');
-    
-    updateConfigUi();
-}
-
-function closeSettings() {
-    hideApply();
-    pushConfigs = {};
-    pushConfigReboot = false;
-    
-    $('div.settings').removeClass('open').addClass('closed');
-    $('div.page-container').removeClass('stretched');
-    $('div.settings-top-bar').removeClass('open').addClass('closed');
-}
-
-function isSettingsOpen() {
-    return $('div.settings').hasClass('open');   
-}
-
-function updateConfigUi() {
-    var objs = $('tr.settings-item, div.advanced-setting, table.advanced-setting, div.settings-section-title, table.settings, ' +
-            'div.check-box.camera-config, div.check-box.main-config');
-    
-    function markHideLogic() {
-        this._hideLogic = true;
-    }
-    
-    function markHideAdvanced() {
-        this._hideAdvanced = true;
-    }
-    
-    function markHideMinimized() {
-        this._hideMinimized = true;
-    }
-    
-    function unmarkHide() {
-        this._hideLogic = false;
-        this._hideAdvanced = false;
-        this._hideMinimized = false;
-    }
-    
-    objs.each(unmarkHide);
-    
-    /* hide sliders that, for some reason, don't have a value */
-    $('input.range').each(function () {
-        if  (this.value == '') {
-            $(this).parents('tr:eq(0)').each(markHideLogic);
-        }
-    });
-
-    /* minimizable sections */
-    $('span.minimize').each(function () {
-        var $this = $(this);
-        if (!$this.hasClass('open')) {
-            $this.parent().next('table.settings').find('tr').each(markHideMinimized);
-        }
-    });
-
-    /* general enable switch */
-    var motionEyeEnabled = $('#motionEyeSwitch').get(0).checked;
-    if (!motionEyeEnabled) {
-        objs.not($('#motionEyeSwitch').parents('div').get(0)).each(markHideLogic);
-    }
-    
-    if ($('#cameraSelect').find('option').length < 2) { /* no camera configured */
-        $('#videoDeviceSwitch').parent().each(markHideLogic);
-        $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHideLogic);
-    }
-    
-    if ($('#videoDeviceSwitch')[0].error) { /* config error */
-        $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHideLogic);
-    }
-        
-    /* advanced settings */
-    var showAdvanced = $('#showAdvancedSwitch').get(0).checked;
-    if (!showAdvanced) {
-        $('tr.advanced-setting, div.advanced-setting, table.advanced-setting').each(markHideAdvanced);
-    }
-    
-    /* hide resolution select if no resolution is selected (none matches) */
-    if ($('#resolutionSelect')[0].selectedIndex == -1) {
-        $('#resolutionSelect').parents('tr:eq(0)').each(markHideLogic);
-    }
-
-    /* storage device */
-    if ($('#storageDeviceSelect').val() !== 'network-share') {
-        $('#networkServerEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#networkUsernameEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#networkPasswordEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#networkShareNameEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* text */
-    if ($('#leftTextSelect').val() !== 'custom-text') {
-        $('#leftTextEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    if ($('#rightTextSelect').val() !== 'custom-text') {
-        $('#rightTextEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* still images capture mode */
-    if ($('#captureModeSelect').val() !== 'interval-snapshots') {
-        $('#snapshotIntervalEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* auto noise level */
-    if ($('#autoNoiseDetectSwitch').get(0).checked) {
-        $('#noiseLevelSlider').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* video device switch */
-    if (!$('#videoDeviceSwitch').get(0).checked) {
-        $('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHideLogic);
-    }
-    
-    /* text overlay switch */
-    if (!$('#textOverlaySwitch').get(0).checked) {
-        $('#textOverlaySwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
-    }
-    
-    /* video streaming */
-    if (!$('#videoStreamingSwitch').get(0).checked) {
-        $('#videoStreamingSwitch').parent().next('table.settings').find('tr.settings-item').not('.localhost-streaming').each(markHideLogic);
-    }
-    if (!$('#streamingServerResizeSwitch').get(0).checked) {
-        $('#streamingResolutionSlider').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* still images switch */
-    if (!$('#stillImagesSwitch').get(0).checked) {
-        $('#stillImagesSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
-    }
-    
-    /* preserve pictures */
-    if ($('#preservePicturesSelect').val() != '-1') {
-        $('#picturesLifetimeEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* motion detection switch */
-    if (!$('#motionDetectionSwitch').get(0).checked) {
-        $('#motionDetectionSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
-        
-        /* hide the entire motion movies section */
-        $('#motionMoviesSwitch').parent().each(markHideLogic);
-        $('#motionMoviesSwitch').parent().next('table.settings').each(markHideLogic);
-        
-        /* hide the entire notifications section */
-        $('#emailNotificationsSwitch').parents('table.settings').prev().each(markHideLogic);
-        $('#emailNotificationsSwitch').parents('table.settings').each(markHideLogic);
-        
-        /* hide the entire working schedule section */
-        $('#workingScheduleSwitch').parent().each(markHideLogic);
-        $('#workingScheduleSwitch').parent().next('table.settings').each(markHideLogic);
-    }
-    
-    /* motion movies switch */
-    if (!$('#motionMoviesSwitch').get(0).checked) {
-        $('#motionMoviesSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
-    }
-    
-    /* preserve movies */
-    if ($('#preserveMoviesSelect').val() != '-1') {
-        $('#moviesLifetimeEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    /* event notifications */
-    if (!$('#emailNotificationsSwitch').get(0).checked) {
-        $('#emailAddressesEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#smtpServerEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#smtpPortEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#smtpAccountEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#smtpPasswordEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#smtpTlsSwitch').parents('tr:eq(0)').each(markHideLogic);
-        $('#emailPictureTimeSpanEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-    
-    if (!$('#webHookNotificationsSwitch').get(0).checked) {
-        $('#webHookUrlEntry').parents('tr:eq(0)').each(markHideLogic);
-        $('#webHookHttpMethodSelect').parents('tr:eq(0)').each(markHideLogic);
-    }
-
-    if (!$('#commandNotificationsSwitch').get(0).checked) {
-        $('#commandNotificationsEntry').parents('tr:eq(0)').each(markHideLogic);
-    }
-
-    /* working schedule */
-    if (!$('#workingScheduleSwitch').get(0).checked) {
-        $('#workingScheduleSwitch').parent().next('table.settings').find('tr.settings-item').each(markHideLogic);
-    }
-    
-    /* additional configs */
-    $('tr[depends]').each(function () {
-        var $tr = $(this);
-        var depends = $tr.attr('depends').split(' ');
-        var conditionOk = true;
-        depends.every(function (depend) {
-            var neg = depend.indexOf('!') >= 0;
-            var parts = depend.split('=');
-            var boolCheck = parts.length == 1;
-            depend = parts[0].replace(new RegExp('[^a-zA-Z0-9_$]', 'g'), '');
-
-            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;
-                }
-            }
-
-            if (boolCheck) {
-                if (neg) {
-                    val = !val;
-                }
-                
-                if (!val) {
-                    conditionOk = false;
-                    return false;
-                }
-            }
-            else { /* comparison */
-                var equal = parts[parts.length - 1] == val;
-                if (equal == neg) {
-                    conditionOk = false;
-                    return false;
-                }
-            }
-
-            return true;
-        });
-        
-        if (!conditionOk) {
-            $tr.each(markHideLogic);
-        }
-    });
-    
-    /* hide sections that have no visible configs and no switch */
-    $('div.settings-section-title').each(function () {
-        var $this = $(this);
-        var $table = $this.next();
-        var controls = $table.find('input, select');
-
-        var switchButton = $this.children('div.check-box');
-        if (switchButton.length && !switchButton[0]._hideNull) {
-            return; /* has visible switch */
-        }
-
-        for (var i = 0; i < controls.length; i++) {
-            var control = $(controls[i]);
-            var tr = control.parents('tr:eq(0)')[0];
-            if (!tr._hideLogic && !tr._hideAdvanced && !tr._hideNull) {
-                return; /* has visible controls */
-            }
-        }
-
-        $table.find('div.settings-item-separator').each(function () {
-            $(this).parent().parent().each(markHideLogic);
-        });
-
-        $this.each(markHideLogic);
-        $table.each(markHideLogic);
-    });
-    
-    /* hide useless separators */
-    $('div.settings-container table.settings').each(function () {
-        var $table = $(this);
-        
-        /* filter visible rows */
-        var visibleTrs = $table.find('tr').filter(function () {
-            return !this._hideLogic && !this._hideAdvanced && !this._hideNull;
-        }).map(function () {
-            var $tr = $(this);
-            $tr.isSeparator = $tr.find('div.settings-item-separator').length > 0;
-            
-            return $tr;
-        }).get();
-
-        for (var i = 1; i < visibleTrs.length; i++) {
-            var $prevTr = visibleTrs[i - 1];
-            var $tr = visibleTrs[i];
-            if ($prevTr.isSeparator && $tr.isSeparator) {
-                $tr.each(markHideLogic);
-            }
-        }
-
-        /* filter visible rows again */
-        visibleTrs = $table.find('tr').filter(function () {
-            return !this._hideLogic && !this._hideAdvanced && !this._hideNull;
-        }).map(function () {
-            var $tr = $(this);
-            $tr.isSeparator = $tr.find('div.settings-item-separator').length > 0;
-            
-            return $tr;
-        }).get();
-
-        if (visibleTrs.length) {
-            /* test first row */
-            if (visibleTrs[0].isSeparator) {
-                visibleTrs[0].each(markHideLogic);
-            }
-            
-            /* test last row */
-            if (visibleTrs[visibleTrs.length - 1].isSeparator) {
-                visibleTrs[visibleTrs.length - 1].each(markHideLogic);
-            }
-        }
-    });
-    
-    var weekDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
-    weekDays.forEach(function (weekDay) {
-        var check = $('#' + weekDay + 'EnabledSwitch');
-        if (check.get(0).checked) {
-            check.parent().find('.time').show();
-        }
-        else {
-            check.parent().find('.time').hide();
-        }
-    });
-    
-    objs.each(function () {
-        if (this._hideLogic || this._hideAdvanced || this._hideMinimized || this._hideNull /* from dict2ui */) {
-            $(this).hide(200);
-        }
-        else {
-            $(this).show(200);
-        }
-    });
-    
-    /* re-validate all the validators */
-    $('div.settings').find('.validator').each(function () {
-        this.validate();
-    });
-    
-    /* update all checkboxes and sliders */
-    $('div.settings').find('input[type=checkbox], input.range').each(function () {
-        this.update();
-    });
-    
-    /* select the first option for the selects with no current selection */
-    $('div.settings').find('select').not('#cameraSelect').each(function () {
-        if (this.selectedIndex === -1) {
-            this.selectedIndex = 0;
-        }
-    });
-}
-
-function configUiValid() {
-    /* re-validate all the validators */
-    $('div.settings').find('.validator').each(function () {
-        this.validate();
-    });
-    
-    var valid = true;
-    $('div.settings input, select').each(function () {
-        if (this.invalid) {
-            valid = false;
-            return false;
-        }
-    });
-    
-    return valid;
-}
-
-function mainUi2Dict() {
-    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()
-    };
-
-    /* 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();
-            if (control.hasClass('number')) {
-                value = Number(value);
-            }
-        }
-        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 = Number(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) {
-    function markHideIfNull(field, elemId) {
-        var elem = $('#' + elemId);
-        var sectionDiv = elem.parents('div.settings-section-title:eq(0)');
-        var hideNull = (field === true) || (typeof field == 'string' && dict[field] == null);
-
-        if (sectionDiv.length) { /* element is a section */
-            sectionDiv.find('div.check-box').each(function () {this._hideNull = hideNull;});
-            if (hideNull) {
-                sectionDiv.find('input[type=checkbox]').each(function () {this.checked = true;});
-            }
-        }
-        else { /* element is a config option */
-            elem.parents('tr:eq(0)').each(function () {this._hideNull = hideNull;});
-        }
-    }
-    
-    $('#motionEyeSwitch')[0].checked = dict['enabled'];
-    
-    $('#showAdvancedSwitch')[0].checked = dict['show_advanced']; markHideIfNull('show_advanced', 'showAdvancedSwitch');
-    $('#adminUsernameEntry').val(dict['admin_username']); markHideIfNull('admin_username', 'adminUsernameEntry');
-    $('#adminPasswordEntry').val(dict['admin_password']); markHideIfNull('admin_password', 'adminPasswordEntry');
-    $('#normalUsernameEntry').val(dict['normal_username']); markHideIfNull('normal_username', 'normalUsernameEntry');
-    $('#normalPasswordEntry').val(dict['normal_password']); markHideIfNull('normal_password', 'normalPasswordEntry');
-
-    /* additional sections */
-    $('input[type=checkbox].additional-section.main-config').each(function () {
-        var name = this.id.substring(0, this.id.length - 6);
-        this.checked = dict[name];
-        markHideIfNull(name, this.id);
-    });
-
-    /* additional configs */
-    $('tr.additional-config').each(function () {
-        var $this = $(this);
-        var control = $this.find('input, select, textarea, div.html');
-        
-        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];
-        }
-        else if (id.endsWith('Html')) {
-            name = id.substring(0, id.length - 4);
-            control.html(dict['_' + name]);
-        }
-        
-        markHideIfNull('_' + name, id);
-    });
-
-    updateConfigUi();
-}
-
-function cameraUi2Dict() {
-    if ($('#videoDeviceSwitch')[0].error) { /* config error */
-        return {
-            'enabled': $('#videoDeviceSwitch')[0].checked,
-        };
-    }
-    
-    var dict = {
-        'enabled': $('#videoDeviceSwitch')[0].checked,
-        'name': $('#deviceNameEntry').val(),
-        'proto': $('#deviceTypeEntry')[0].proto,
-        
-        /* video device */
-        'light_switch_detect': $('#lightSwitchDetectSwitch')[0].checked,
-        'auto_brightness': $('#autoBrightnessSwitch')[0].checked,
-        'rotation': $('#rotationSelect').val(),
-        'framerate': $('#framerateSlider').val(),
-        'extra_options': $('#extraOptionsEntry').val().split(new RegExp('(\n)|(\r\n)|(\n\r)')).map(function (o) {
-            if (!o) {
-                return null;
-            }
-
-            o = o.trim();
-            if (!o.length) {
-                return null;
-            }
-
-            var parts = o.replace(new RegExp('\\s+', 'g'), ' ').split(' ');
-            if (parts.length < 2) {
-                return [parts[0], ''];
-            }
-            else if (parts.length == 2) {
-                return parts;
-            }
-            else {
-                return [parts[0], parts.slice(1).join(' ')];
-            }
-        }).filter(function (e) {return e;}),
-
-        /* file storage */
-        'storage_device': $('#storageDeviceSelect').val(),
-        'network_server': $('#networkServerEntry').val(),
-        'network_share_name': $('#networkShareNameEntry').val(),
-        'network_username': $('#networkUsernameEntry').val(),
-        'network_password': $('#networkPasswordEntry').val(),
-        'root_directory': $('#rootDirectoryEntry').val(),
-        
-        /* text overlay */
-        'text_overlay': $('#textOverlaySwitch')[0].checked,
-        'left_text': $('#leftTextSelect').val(),
-        'custom_left_text': $('#leftTextEntry').val(),
-        'right_text': $('#rightTextSelect').val(),
-        'custom_right_text': $('#rightTextEntry').val(),
-        
-        /* video streaming */
-        'video_streaming': $('#videoStreamingSwitch')[0].checked,
-        'streaming_framerate': $('#streamingFramerateSlider').val(),
-        'streaming_quality': $('#streamingQualitySlider').val(),
-        'streaming_resolution': $('#streamingResolutionSlider').val(),
-        'streaming_server_resize': $('#streamingServerResizeSwitch')[0].checked,
-        'streaming_port': $('#streamingPortEntry').val(),
-        'streaming_auth_mode': $('#streamingAuthModeSelect').val() || 'disabled', /* compatibility with old motion */
-        'streaming_motion': $('#streamingMotion')[0].checked,
-        
-        /* still images */
-        'still_images': $('#stillImagesSwitch')[0].checked,
-        'image_file_name': $('#imageFileNameEntry').val(),
-        'image_quality': $('#imageQualitySlider').val(),
-        'capture_mode': $('#captureModeSelect').val(),
-        'snapshot_interval': $('#snapshotIntervalEntry').val(),
-        'preserve_pictures': $('#preservePicturesSelect').val() >= 0 ? $('#preservePicturesSelect').val() : $('#picturesLifetimeEntry').val(),
-        
-        /* motion detection */
-        'motion_detection': $('#motionDetectionSwitch')[0].checked,
-        'show_frame_changes': $('#showFrameChangesSwitch')[0].checked,
-        'frame_change_threshold': $('#frameChangeThresholdSlider').val(),
-        'auto_noise_detect': $('#autoNoiseDetectSwitch')[0].checked,
-        'noise_level': $('#noiseLevelSlider').val(),
-        'event_gap': $('#eventGapEntry').val(),
-        'pre_capture': $('#preCaptureEntry').val(),
-        'post_capture': $('#postCaptureEntry').val(),
-        'minimum_motion_frames': $('#minimumMotionFramesEntry').val(),
-        
-        /* motion movies */
-        'motion_movies': $('#motionMoviesSwitch')[0].checked,
-        'movie_file_name': $('#movieFileNameEntry').val(),
-        'movie_quality': $('#movieQualitySlider').val(),
-        'max_movie_length': $('#maxMovieLengthEntry').val(),
-        'preserve_movies': $('#preserveMoviesSelect').val() >= 0 ? $('#preserveMoviesSelect').val() : $('#moviesLifetimeEntry').val(),
-        
-        /* motion notifications */
-        'email_notifications_enabled': $('#emailNotificationsSwitch')[0].checked,
-        'email_notifications_addresses': $('#emailAddressesEntry').val(),
-        'email_notifications_smtp_server': $('#smtpServerEntry').val(),
-        'email_notifications_smtp_port': $('#smtpPortEntry').val(),
-        'email_notifications_smtp_account': $('#smtpAccountEntry').val(),
-        'email_notifications_smtp_password': $('#smtpPasswordEntry').val(),
-        'email_notifications_smtp_tls': $('#smtpTlsSwitch')[0].checked,
-        'email_notifications_picture_time_span': $('#emailPictureTimeSpanEntry').val(),
-        'web_hook_notifications_enabled': $('#webHookNotificationsSwitch')[0].checked,
-        'web_hook_notifications_url': $('#webHookUrlEntry').val(),
-        'web_hook_notifications_http_method': $('#webHookHttpMethodSelect').val(),
-        'command_notifications_enabled': $('#commandNotificationsSwitch')[0].checked,
-        'command_notifications_exec': $('#commandNotificationsEntry').val(),
-        
-        /* working schedule */
-        'working_schedule': $('#workingScheduleSwitch')[0].checked,
-        'monday_from': $('#mondayEnabledSwitch')[0].checked ? $('#mondayFromEntry').val() : '',
-        'monday_to':$('#mondayEnabledSwitch')[0].checked ? $('#mondayToEntry').val() : '',
-        'tuesday_from': $('#tuesdayEnabledSwitch')[0].checked ? $('#tuesdayFromEntry').val() : '',
-        'tuesday_to': $('#tuesdayEnabledSwitch')[0].checked ? $('#tuesdayToEntry').val() : '',
-        'wednesday_from': $('#wednesdayEnabledSwitch')[0].checked ? $('#wednesdayFromEntry').val() : '',
-        'wednesday_to': $('#wednesdayEnabledSwitch')[0].checked ? $('#wednesdayToEntry').val() : '',
-        'thursday_from': $('#thursdayEnabledSwitch')[0].checked ? $('#thursdayFromEntry').val() : '',
-        'thursday_to': $('#thursdayEnabledSwitch')[0].checked ? $('#thursdayToEntry').val() : '',
-        'friday_from': $('#fridayEnabledSwitch')[0].checked ? $('#fridayFromEntry').val() : '',
-        'friday_to': $('#fridayEnabledSwitch')[0].checked ? $('#fridayToEntry').val() :'',
-        'saturday_from': $('#saturdayEnabledSwitch')[0].checked ? $('#saturdayFromEntry').val() : '',
-        'saturday_to': $('#saturdayEnabledSwitch')[0].checked ? $('#saturdayToEntry').val() : '',
-        'sunday_from': $('#sundayEnabledSwitch')[0].checked ? $('#sundayFromEntry').val() : '',
-        'sunday_to': $('#sundayEnabledSwitch')[0].checked ? $('#sundayToEntry').val() : '',
-        'working_schedule_type': $('#workingScheduleTypeSelect').val(),
-    };
-
-    if ($('#resolutionSelect')[0].selectedIndex != -1) {
-        dict.resolution = $('#resolutionSelect').val();
-    }
-
-    if ($('#brightnessSlider').val() !== '') {
-        dict.brightness = $('#brightnessSlider').val();
-    }
-
-    if ($('#contrastSlider').val() !== '') {
-        dict.contrast = $('#contrastSlider').val();
-    }
-    
-    if ($('#saturationSlider').val() !== '') {
-        dict.saturation = $('#saturationSlider').val();
-    }
-    
-    if ($('#hueSlider').val() !== '') {
-        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();
-            if (control.hasClass('number')) {
-                value = Number(value);
-            }
-        }
-        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 = Number(control.val());
-        }
-        else if (id.endsWith('Switch')) {
-            name = id.substring(0, id.length - 6);
-            value = control[0].checked;
-        }
-        
-        dict['_' + name] = value;
-    });
-
-    return dict;
-}
-
-function dict2CameraUi(dict) {
-    if (dict == null) {
-        /* errors while getting the configuration */
-        
-        $('#videoDeviceSwitch')[0].error = true;
-        $('#videoDeviceSwitch')[0].checked = true; /* so that the user can explicitly disable the camera */
-        updateConfigUi();
-        
-        return;
-    }
-    else {
-        $('#videoDeviceSwitch')[0].error = false;
-    }
-
-    function markHideIfNull(field, elemId) {
-        var elem = $('#' + elemId);
-        var sectionDiv = elem.parents('div.settings-section-title:eq(0)');
-        var hideNull = (field === true) || (typeof field == 'string' && dict[field] == null);
-
-        if (sectionDiv.length) { /* element is a section */
-            sectionDiv.find('div.check-box').each(function () {this._hideNull = hideNull;});
-            if (hideNull) {
-                sectionDiv.find('input[type=checkbox]').each(function () {this.checked = true;});
-            }
-        }
-        else { /* element is a config option */
-            elem.parents('tr:eq(0)').each(function () {this._hideNull = hideNull;});
-        }
-    }
-    
-    /* video device */
-    var prettyType = '';
-    switch (dict['proto']) {
-        case 'v4l2':
-            prettyType = 'V4L2 Camera';
-            break;
-
-        case 'netcam':
-            prettyType = 'Network Camera';
-            break;
-
-        case 'motioneye':
-            prettyType = 'Remote motionEye Camera';
-            break;
-
-        case 'mjpeg':
-            prettyType = 'Simple MJPEG Camera';
-            break;
-    }
-    
-    $('#videoDeviceSwitch')[0].checked = dict['enabled']; markHideIfNull('enabled', 'videoDeviceSwitch');
-    $('#deviceNameEntry').val(dict['name']); markHideIfNull('name', 'deviceNameEntry');
-    $('#deviceUriEntry').val(dict['device_url']); markHideIfNull('device_url', 'deviceUriEntry');
-    $('#deviceTypeEntry').val(prettyType); markHideIfNull(!prettyType, 'deviceTypeEntry');
-    $('#deviceTypeEntry')[0].proto = dict['proto'];
-    $('#lightSwitchDetectSwitch')[0].checked = dict['light_switch_detect']; markHideIfNull('light_switch_detect', 'lightSwitchDetectSwitch');
-    $('#autoBrightnessSwitch')[0].checked = dict['auto_brightness']; markHideIfNull('auto_brightness', 'autoBrightnessSwitch');
-    
-    $('#brightnessSlider').val(dict['brightness']); markHideIfNull('brightness', 'brightnessSlider');
-    $('#contrastSlider').val(dict['contrast']); markHideIfNull('contrast', 'contrastSlider');
-    $('#saturationSlider').val(dict['saturation']); markHideIfNull('saturation', 'saturationSlider');
-    $('#hueSlider').val(dict['hue']); markHideIfNull('hue', 'hueSlider');
-
-    $('#resolutionSelect').html('');
-    if (dict['available_resolutions']) {
-        dict['available_resolutions'].forEach(function (resolution) {
-            $('#resolutionSelect').append('<option value="' + resolution + '">' + resolution + '</option>');
-        });
-    }
-    $('#resolutionSelect').val(dict['resolution']); markHideIfNull('available_resolutions', 'resolutionSelect');
-    
-    $('#rotationSelect').val(dict['rotation']); markHideIfNull('rotation', 'rotationSelect');
-    $('#framerateSlider').val(dict['framerate']); markHideIfNull('framerate', 'framerateSlider');
-    $('#extraOptionsEntry').val(dict['extra_options'] ? (dict['extra_options'].map(function (o) {
-        return o.join(' ');
-    }).join('\r\n')) : ''); markHideIfNull('extra_options', 'extraOptionsEntry');
-    
-    /* file storage */
-    $('#storageDeviceSelect').empty();
-    dict['available_disks'] = dict['available_disks'] || [];
-    var storageDeviceOptions = {'network-share': true};
-    dict['available_disks'].forEach(function (disk) {
-        disk.partitions.forEach(function (partition) {
-            var target = partition.target.replaceAll('/', '-');
-            var option = 'local-disk' + target;
-            var label = partition.vendor;
-            if (partition.model) {
-                label += ' ' + partition.model;
-            }
-            if (disk.partitions.length > 1) {
-                label += '/part' + partition.part_no;
-            }
-            label += ' (' + partition.target + ')';
-            
-            storageDeviceOptions[option] = true;
-            
-            $('#storageDeviceSelect').append('<option value="' + option + '">' + label + '</option>');
-        });
-    });
-    $('#storageDeviceSelect').append('<option value="custom-path">Custom Path</option>');
-    if (dict['smb_shares']) {
-        $('#storageDeviceSelect').append('<option value="network-share">Network Share</option>');
-    }
-
-    if (storageDeviceOptions[dict['storage_device']]) {
-        $('#storageDeviceSelect').val(dict['storage_device']);
-    }
-    else {
-        $('#storageDeviceSelect').val('custom-path');
-    }
-    markHideIfNull('storage_device', 'storageDeviceSelect');
-    $('#networkServerEntry').val(dict['network_server']); markHideIfNull('network_server', 'networkServerEntry');
-    $('#networkShareNameEntry').val(dict['network_share_name']); markHideIfNull('network_share_name', 'networkShareNameEntry');
-    $('#networkUsernameEntry').val(dict['network_username']); markHideIfNull('network_username', 'networkUsernameEntry');
-    $('#networkPasswordEntry').val(dict['network_password']); markHideIfNull('network_password', 'networkPasswordEntry');
-    $('#rootDirectoryEntry').val(dict['root_directory']); markHideIfNull('root_directory', 'rootDirectoryEntry');
-    var percent = 0;
-    if (dict['disk_total'] != 0) {
-        percent = parseInt(dict['disk_used'] * 100 / dict['disk_total']);
-    }
-    
-    $('#diskUsageProgressBar').each(function () {
-        this.setProgress(percent);
-        this.setText((dict['disk_used'] / 1073741824).toFixed(1)  + '/' + (dict['disk_total'] / 1073741824).toFixed(1) + ' GB (' + percent + '%)');
-    }); markHideIfNull('disk_used', 'diskUsageProgressBar');
-    
-    /* text overlay */
-    $('#textOverlaySwitch')[0].checked = dict['text_overlay']; markHideIfNull('text_overlay', 'textOverlaySwitch');
-    $('#leftTextSelect').val(dict['left_text']); markHideIfNull('left_text', 'leftTextSelect');
-    $('#leftTextEntry').val(dict['custom_left_text']); markHideIfNull('custom_left_text', 'leftTextEntry');
-    $('#rightTextSelect').val(dict['right_text']); markHideIfNull('right_text', 'rightTextSelect');
-    $('#rightTextEntry').val(dict['custom_right_text']); markHideIfNull('custom_right_text', 'rightTextEntry');
-    
-    /* video streaming */
-    $('#videoStreamingSwitch')[0].checked = dict['video_streaming']; markHideIfNull('video_streaming', 'videoStreamingSwitch');
-    $('#streamingFramerateSlider').val(dict['streaming_framerate']); markHideIfNull('streaming_framerate', 'streamingFramerateSlider');
-    $('#streamingQualitySlider').val(dict['streaming_quality']); markHideIfNull('streaming_quality', 'streamingQualitySlider');
-    $('#streamingResolutionSlider').val(dict['streaming_resolution']); markHideIfNull('streaming_resolution', 'streamingResolutionSlider');
-    $('#streamingServerResizeSwitch')[0].checked = dict['streaming_server_resize']; markHideIfNull('streaming_server_resize', 'streamingServerResizeSwitch');
-    $('#streamingPortEntry').val(dict['streaming_port']); markHideIfNull('streaming_port', 'streamingPortEntry');
-    $('#streamingAuthModeSelect').val(dict['streaming_auth_mode']); markHideIfNull('streaming_auth_mode', 'streamingAuthModeSelect');
-    $('#streamingMotion')[0].checked = dict['streaming_motion']; markHideIfNull('streaming_motion', 'streamingMotion');
-    
-    var cameraUrl = location.protocol + '//' + location.host + '/picture/' + dict.id + '/';
-    
-    var snapshotUrl = null;
-    var mjpgUrl = null;
-    var embedUrl = null;
-    
-    if (dict['proto'] == 'mjpeg') {
-        mjpgUrl = dict['url'];
-        mjpgUrl = mjpgUrl.replace('127.0.0.1', window.location.host.split(':')[0]);
-        embedUrl = cameraUrl + 'frame/';
-    }
-    else {
-        snapshotUrl = cameraUrl + 'current/';
-        mjpgUrl = location.protocol + '//' + location.host.split(':')[0] + ':' + dict.streaming_port;
-        embedUrl = cameraUrl + 'frame/';
-    }
-
-    if (dict.proto == 'motioneye') {
-        /* cannot tell the mjpg streaming url for a remote motionEye camera */
-        mjpgUrl = '';
-    }
-
-    if ($('#normalPasswordEntry').val()) { /* anonymous access is disabled */ 
-        if (snapshotUrl) {
-            snapshotUrl = addAuthParams('GET', snapshotUrl);
-        }
-    }
-    
-    $('#streamingSnapshotUrlEntry').val(snapshotUrl); markHideIfNull(!snapshotUrl, 'streamingSnapshotUrlEntry');
-    $('#streamingMjpgUrlEntry').val(mjpgUrl); markHideIfNull(!mjpgUrl, 'streamingMjpgUrlEntry');
-    $('#streamingEmbedUrlEntry').val(embedUrl); markHideIfNull(!embedUrl, 'streamingEmbedUrlEntry');
-    
-    /* still images */
-    $('#stillImagesSwitch')[0].checked = dict['still_images']; markHideIfNull('still_images', 'stillImagesSwitch');
-    $('#imageFileNameEntry').val(dict['image_file_name']); markHideIfNull('image_file_name', 'imageFileNameEntry');
-    $('#imageQualitySlider').val(dict['image_quality']); markHideIfNull('image_quality', 'imageQualitySlider');
-    $('#captureModeSelect').val(dict['capture_mode']); markHideIfNull('capture_mode', 'captureModeSelect');
-    $('#snapshotIntervalEntry').val(dict['snapshot_interval']); markHideIfNull('snapshot_interval', 'snapshotIntervalEntry');
-    $('#preservePicturesSelect').val(dict['preserve_pictures']);
-    if ($('#preservePicturesSelect').val() == null) {
-        $('#preservePicturesSelect').val('-1');
-    }
-    markHideIfNull('preserve_pictures', 'preservePicturesSelect');
-    $('#picturesLifetimeEntry').val(dict['preserve_pictures']); markHideIfNull('preserve_pictures', 'picturesLifetimeEntry');
-    
-    /* motion detection */
-    $('#motionDetectionSwitch')[0].checked = dict['motion_detection']; markHideIfNull('motion_detection', 'motionDetectionSwitch');
-    $('#showFrameChangesSwitch')[0].checked = dict['show_frame_changes']; markHideIfNull('show_frame_changes', 'showFrameChangesSwitch');
-    $('#frameChangeThresholdSlider').val(dict['frame_change_threshold']); markHideIfNull('frame_change_threshold', 'frameChangeThresholdSlider');
-    $('#autoNoiseDetectSwitch')[0].checked = dict['auto_noise_detect']; markHideIfNull('auto_noise_detect', 'autoNoiseDetectSwitch');
-    $('#noiseLevelSlider').val(dict['noise_level']); markHideIfNull('noise_level', 'noiseLevelSlider');
-    $('#eventGapEntry').val(dict['event_gap']); markHideIfNull('event_gap', 'eventGapEntry');
-    $('#preCaptureEntry').val(dict['pre_capture']); markHideIfNull('pre_capture', 'preCaptureEntry');
-    $('#postCaptureEntry').val(dict['post_capture']); markHideIfNull('post_capture', 'postCaptureEntry');
-    $('#minimumMotionFramesEntry').val(dict['minimum_motion_frames']); markHideIfNull('minimum_motion_frames', 'minimumMotionFramesEntry');
-    
-    /* motion movies */
-    $('#motionMoviesSwitch')[0].checked = dict['motion_movies']; markHideIfNull('motion_movies', 'motionMoviesSwitch');
-    $('#movieFileNameEntry').val(dict['movie_file_name']); markHideIfNull('movie_file_name', 'movieFileNameEntry');
-    $('#movieQualitySlider').val(dict['movie_quality']); markHideIfNull('movie_quality', 'movieQualitySlider');
-    $('#maxMovieLengthEntry').val(dict['max_movie_length']); markHideIfNull('max_movie_length', 'maxMovieLengthEntry');
-    $('#preserveMoviesSelect').val(dict['preserve_movies']);
-    if ($('#preserveMoviesSelect').val() == null) {
-        $('#preserveMoviesSelect').val('-1');
-    }
-    markHideIfNull('preserve_movies', 'preserveMoviesSelect');
-    $('#moviesLifetimeEntry').val(dict['preserve_movies']); markHideIfNull('preserve_movies', 'moviesLifetimeEntry');
-    
-    /* motion notifications */
-    $('#emailNotificationsSwitch')[0].checked = dict['email_notifications_enabled']; markHideIfNull('email_notifications_enabled', 'emailNotificationsSwitch');
-    $('#emailAddressesEntry').val(dict['email_notifications_addresses']);
-    $('#smtpServerEntry').val(dict['email_notifications_smtp_server']);
-    $('#smtpPortEntry').val(dict['email_notifications_smtp_port']);
-    $('#smtpAccountEntry').val(dict['email_notifications_smtp_account']);
-    $('#smtpPasswordEntry').val(dict['email_notifications_smtp_password']);
-    $('#smtpTlsSwitch')[0].checked = dict['email_notifications_smtp_tls'];
-    $('#emailPictureTimeSpanEntry').val(dict['email_notifications_picture_time_span']);
-    $('#webHookNotificationsSwitch')[0].checked = dict['web_hook_notifications_enabled']; markHideIfNull('web_hook_notifications_enabled', 'webHookNotificationsSwitch');
-    $('#webHookUrlEntry').val(dict['web_hook_notifications_url']);
-    $('#webHookHttpMethodSelect').val(dict['web_hook_notifications_http_method']);
-    $('#commandNotificationsSwitch')[0].checked = dict['command_notifications_enabled']; markHideIfNull('command_notifications_enabled', 'commandNotificationsSwitch');
-    $('#commandNotificationsEntry').val(dict['command_notifications_exec']);
-
-    /* working schedule */
-    $('#workingScheduleSwitch')[0].checked = dict['working_schedule']; markHideIfNull('working_schedule', 'workingScheduleSwitch');
-    $('#mondayEnabledSwitch')[0].checked = Boolean(dict['monday_from'] && dict['monday_to']); markHideIfNull('monday_from', 'mondayEnabledSwitch');
-    $('#mondayFromEntry').val(dict['monday_from']); markHideIfNull('monday_from', 'mondayFromEntry');
-    $('#mondayToEntry').val(dict['monday_to']); markHideIfNull('monday_to', 'mondayToEntry');
-    
-    $('#tuesdayEnabledSwitch')[0].checked = Boolean(dict['tuesday_from'] && dict['tuesday_to']); markHideIfNull('tuesday_from', 'tuesdayEnabledSwitch');
-    $('#tuesdayFromEntry').val(dict['tuesday_from']); markHideIfNull('tuesday_from', 'tuesdayFromEntry');
-    $('#tuesdayToEntry').val(dict['tuesday_to']); markHideIfNull('tuesday_to', 'tuesdayToEntry');
-    
-    $('#wednesdayEnabledSwitch')[0].checked = Boolean(dict['wednesday_from'] && dict['wednesday_to']); markHideIfNull('wednesday_from', 'wednesdayEnabledSwitch');
-    $('#wednesdayFromEntry').val(dict['wednesday_from']); markHideIfNull('wednesday_from', 'wednesdayFromEntry');
-    $('#wednesdayToEntry').val(dict['wednesday_to']); markHideIfNull('wednesday_to', 'wednesdayToEntry');
-    
-    $('#thursdayEnabledSwitch')[0].checked = Boolean(dict['thursday_from'] && dict['thursday_to']); markHideIfNull('thursday_from', 'thursdayEnabledSwitch');
-    $('#thursdayFromEntry').val(dict['thursday_from']); markHideIfNull('thursday_from', 'thursdayFromEntry');
-    $('#thursdayToEntry').val(dict['thursday_to']); markHideIfNull('thursday_to', 'thursdayToEntry');
-    
-    $('#fridayEnabledSwitch')[0].checked = Boolean(dict['friday_from'] && dict['friday_to']); markHideIfNull('friday_from', 'fridayEnabledSwitch');
-    $('#fridayFromEntry').val(dict['friday_from']); markHideIfNull('friday_from', 'fridayFromEntry');
-    $('#fridayToEntry').val(dict['friday_to']); markHideIfNull('friday_to', 'fridayToEntry');
-    
-    $('#saturdayEnabledSwitch')[0].checked = Boolean(dict['saturday_from'] && dict['saturday_to']); markHideIfNull('saturday_from', 'saturdayEnabledSwitch');
-    $('#saturdayFromEntry').val(dict['saturday_from']); markHideIfNull('saturday_from', 'saturdayFromEntry');
-    $('#saturdayToEntry').val(dict['saturday_to']); markHideIfNull('saturday_to', 'saturdayToEntry');
-    
-    $('#sundayEnabledSwitch')[0].checked = Boolean(dict['sunday_from'] && dict['sunday_to']); markHideIfNull('sunday_from', 'sundayEnabledSwitch');
-    $('#sundayFromEntry').val(dict['sunday_from']); markHideIfNull('sunday_from', 'sundayFromEntry');
-    $('#sundayToEntry').val(dict['sunday_to']); markHideIfNull('sunday_to', 'sundayToEntry');
-    $('#workingScheduleTypeSelect').val(dict['working_schedule_type']); markHideIfNull('working_schedule_type', 'workingScheduleTypeSelect');
-    
-    /* additional sections */
-    $('input[type=checkbox].additional-section.main-config').each(function () {
-        var name = this.id.substring(0, this.id.length - 6);
-        this.checked = dict[name];
-        markHideIfNull(name, this.id);
-    });
-
-    /* additional configs */
-    $('tr.additional-config').each(function () {
-        var $this = $(this);
-        var control = $this.find('input, select, textarea, div.html');
-        
-        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];
-        }
-        else if (id.endsWith('Html')) {
-            name = id.substring(0, id.length - 4);
-            control.html(dict['_' + name]);
-        }
-        
-        markHideIfNull('_' + name, id);
-    });
-
-    updateConfigUi();
-}
-
-    
-    /* progress */
-
-function beginProgress(cameraIds) {
-    if (inProgress) {
-        return; /* already in progress */
-    }
-
-    inProgress = true;
-    
-    /* replace the main page message with a progress indicator */
-    $('div.add-camera-message').replaceWith('<img class="main-loading-progress" src="' + staticUrl + 'img/main-loading-progress.gif">');
-    
-    /* show the apply button progress indicator */
-    $('#applyButton').html('<img class="apply-progress" src="' + staticUrl + 'img/apply-progress.gif">');
-    
-    /* show the camera progress indicators */
-    if (cameraIds) {
-        cameraIds.forEach(function (cameraId) {
-            $('div.camera-frame#camera' + cameraId + ' div.camera-progress').addClass('visible');
-        });
-    }
-    else {
-        $('div.camera-progress').addClass('visible');
-    }
-    
-    /* remove the settings progress lock */
-    $('div.settings-progress').css('width', '100%').css('opacity', '0.9');
-}
-
-function endProgress() {
-    if (!inProgress) {
-        return; /* not in progress */
-    }
-    
-    inProgress = false;
-    
-    /* deal with the apply button */
-    if (Object.keys(pushConfigs).length === 0) {
-        hideApply();
-    }
-    else {
-        showApply();
-    }
-    
-    /* hide the settings progress lock */
-    $('div.settings-progress').css('opacity', '0');
-    
-    /* hide the camera progress indicator */
-    $('div.camera-progress').removeClass('visible');
-
-    setTimeout(function () {
-        $('div.settings-progress').css('width', '0px');
-    }, 500);
-}
-
-function downloadFile(uri) {
-    uri = baseUri + uri;
-
-    var url = window.location.href;
-    var parts = url.split('/');
-    url = parts.slice(0, 3).join('/') + uri;
-    url = addAuthParams('GET', url);
-    
-    /* download the file by creating a temporary iframe */
-    var frame = $('<iframe style="display: none;"></iframe>');
-    frame.attr('src', url);
-    $('body').append(frame);
-}
-
-function uploadFile(uri, input, callback) {
-    if (!window.FormData) {
-        showErrorMessage("Your browser doesn't implement this function!");s
-        callback();
-    }
-
-    var formData = new FormData();
-    var files = input[0].files;
-    formData.append('files', files[0], files[0].name);
-
-    ajax('POST', uri, formData, callback);
-}
-
-
-    /* apply button */
-
-function showApply() {
-    var applyButton = $('#applyButton');
-    
-    applyButton.html('Apply');
-    applyButton.css('display', 'inline-block');
-    applyButton.removeClass('progress');
-    setTimeout(function () {
-        applyButton.css('opacity', '1');
-    }, 10);
-}
-
-function hideApply() {
-    var applyButton = $('#applyButton');
-    
-    applyButton.css('opacity', '0');
-    applyButton.removeClass('progress');
-    
-    setTimeout(function () {
-        applyButton.css('display', 'none');
-    }, 500);
-}
-
-function isApplyVisible() {
-    var applyButton = $('#applyButton');
-    
-    return applyButton.is(':visible');
-}
-
-function doApply() {
-    if (!configUiValid()) {
-        runAlertDialog('Make sure all the configuration options are valid!');
-        return;
-    }
-    
-    function actualApply() {
-        /* gather the affected motion instances */
-        var affectedInstances = {};
-        Object.keys(pushConfigs).forEach(function (key) {
-            var config = pushConfigs[key];
-            if (key === 'main') {
-                return;
-            }
-            
-            var instance;
-            if (config.proto == 'netcam' || config.proto == 'v4l2') {
-                instance = '';
-            }
-            else if (config.proto == 'motioneye') { /* motioneye */
-                instance = config.host || '';
-                if (config.port) {
-                    instance += ':' + config.port;
-                }
-            }
-            
-            affectedInstances[instance] = true;
-        });
-        affectedInstances = Object.keys(affectedInstances);
-        
-        /* compute the affected camera ids */ 
-        var cameraIdsByInstance = getCameraIdsByInstance();
-        var affectedCameraIds = [];
-        
-        affectedInstances.forEach(function (instance) {
-            affectedCameraIds = affectedCameraIds.concat(cameraIdsByInstance[instance] || []);
-        });
-        
-        beginProgress(affectedCameraIds);
-        affectedCameraIds.forEach(function (cameraId) {
-            refreshDisabled[cameraId] |= 0;
-            refreshDisabled[cameraId]++;
-        });
-        
-        ajax('POST', baseUri + 'config/0/set/', pushConfigs, function (data) {
-            affectedCameraIds.forEach(function (cameraId) {
-                refreshDisabled[cameraId]--;
-            });
-            
-            if (data == null || data.error) {
-                endProgress();
-                showErrorMessage(data && data.error);
-                return;
-            }
-            
-            if (data.reboot) {
-                var count = 0;
-                function checkServerReboot() {
-                    ajax('GET', baseUri + 'config/0/get/', null, 
-                        function () {
-                            window.location.reload(true);
-                        },
-                        function () {
-                            if (count < 25) {
-                                count += 1;
-                                setTimeout(checkServerReboot, 2000);
-                            }
-                            else {
-                                window.location.reload(true);
-                            }
-                        }
-                    );
-                }
-                
-                setTimeout(checkServerReboot, 15000);
-                
-                return;
-            }
-            
-            if (data.reload) {
-                window.location.reload(true);
-                return;
-            }
-            
-            /* update the camera name in the device select
-             * and frame title bar */
-            Object.keys(pushConfigs).forEach(function (key) {
-                var config = pushConfigs[key];
-                if (config.key !== 'main') {
-                    $('#cameraSelect').find('option[value=' + key + ']').html(config.name);
-                }
-                
-                $('#camera' + key).find('span.camera-name').html(config.name);
-            });
-    
-            pushConfigs = {};
-            pushConfigReboot = false;
-            endProgress();
-            recreateCameraFrames(); /* a camera could have been disabled */
-        });
-    }
-    
-    if (pushConfigReboot) {
-        runConfirmDialog('This will reboot the system. Continue?', function () {
-            actualApply();
-        });
-    }
-    else {
-        actualApply();
-    }
-}
-
-function doShutDown() {
-    runConfirmDialog('Really shut down?', function () {
-        ajax('POST', baseUri + 'power/shutdown/');
-        setTimeout(function () {
-            refreshInterval = 1000000;
-            showModalDialog('<div class="modal-progress"></div>');
-            
-            function checkServer() {
-                ajax('GET', baseUri, null, 
-                    function () {
-                        setTimeout(checkServer, 1000);
-                    },
-                    function () {
-                        showModalDialog('Powered Off');
-                        setTimeout(function () {
-                            $('div.modal-glass').animate({'opacity': '1', 'background-color': '#212121'}, 200);
-                        },100);
-                    },
-                    10000 /* timeout = 10s */
-                );
-            }
-            
-            checkServer();
-        }, 10);
-    });
-}
-
-function doReboot() {
-    runConfirmDialog('Really reboot?', function () {
-        ajax('POST', baseUri + 'power/reboot/');
-        setTimeout(function () {
-            refreshInterval = 1000000;
-            showModalDialog('<div class="modal-progress"></div>');
-            var shutDown = false;
-            
-            function checkServer() {
-                ajax('GET', baseUri, null, 
-                    function () {
-                        if (!shutDown) {
-                            setTimeout(checkServer, 1000);
-                        }
-                        else {
-                            runAlertDialog('The system has been rebooted!', function () {
-                                window.location.reload(true);
-                            });
-                        }
-                    },
-                    function () {
-                        shutDown = true; /* the first error indicates the system was shut down */
-                        setTimeout(checkServer, 1000);
-                    },
-                    5 * 1000 /* timeout = 5s */
-                );
-            }
-            
-            checkServer();
-        }, 10);
-    });
-}
-
-function doRemCamera() {
-    if (Object.keys(pushConfigs).length) {
-        return runAlertDialog('Please apply the modified settings first!');
-    }
-    
-    var cameraId = $('#cameraSelect').val();
-    if (cameraId == null || cameraId === 'add') {
-        runAlertDialog('No camera to remove!');
-        return;
-    }
-
-    var deviceName = $('#cameraSelect').find('option[value=' + cameraId + ']').text();
-    
-    runConfirmDialog('Remove camera ' + deviceName + '?', function () {
-        /* disable further refreshing of this camera */
-        var img = $('div.camera-frame#camera' + cameraId).find('img.camera');
-        if (img.length) {
-            img[0].loading = 1;
-        }
-
-        beginProgress();
-        ajax('POST', baseUri + 'config/' + cameraId + '/rem/', null, function (data) {
-            if (data == null || data.error) {
-                endProgress();
-                showErrorMessage(data && data.error);
-                return;
-            }
-            
-            fetchCurrentConfig(endProgress);
-        });
-    });
-}
-
-function doUpdate() {
-    if (Object.keys(pushConfigs).length) {
-        return runAlertDialog('Please apply the modified settings first!');
-    }
-    
-    showModalDialog('<div class="modal-progress"></div>');
-    ajax('GET', baseUri + 'update/', null, function (data) {
-        if (data.update_version == null) {
-            runAlertDialog('motionEye is up to date (current version: ' + data.current_version + ')');
-        }
-        else {
-            runConfirmDialog('New version available: ' + data.update_version + '. Update?', function () {
-                refreshInterval = 1000000;
-                showModalDialog('<div style="text-align: center;"><span>Updating. This may take a few minutes.</span><div class="modal-progress"></div></div>');
-                ajax('POST', baseUri + 'update/?version=' + data.update_version, null, function () {
-                    var count = 0;
-                    function checkServer() {
-                        ajax('GET', baseUri + 'config/0/get/', null,
-                            function () {
-                                runAlertDialog('motionEye was successfully updated!', function () {
-                                    window.location.reload(true);
-                                });
-                            },
-                            function () {
-                                if (count < 60) {
-                                    count += 1;
-                                    setTimeout(checkServer, 5000);
-                                }
-                                else {
-                                    runAlertDialog('Update failed!', function () {
-                                        window.location.reload(true);
-                                    });
-                                }
-                            }
-                        );
-                    }
-                    
-                    setTimeout(checkServer, 10000);
-
-                }, function (e) { /* error */
-                    runAlertDialog('The update process has failed!', function () {
-                        window.location.reload(true);
-                    });
-                });
-
-                return false; /* prevents hiding the modal container */
-            });
-        }
-    });
-}
-
-function doBackup() {
-    downloadFile('config/backup/');
-}
-
-function doRestore() {
-    var content = 
-            $('<table class="restore-dialog">' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Backup File</span></td>' +
-                    '<td class="dialog-item-value"><form><input type="file" class="styled" id="fileInput"></form></td>' +
-                    '<td><span class="help-mark" title="the backup file you have previously downloaded">?</span></td>' +
-                '</tr>' +
-            '</table>');
-    
-    /* collect ui widgets */
-    var fileInput = content.find('#fileInput');
-    
-    /* make validators */
-    makeFileValidator(fileInput, true);
-    
-    function uiValid() {
-        /* re-validate all the validators */
-        content.find('.validator').each(function () {
-            this.validate();
-        });
-        
-        var valid = true;
-        var query = content.find('input, select');
-        query.each(function () {
-            if (this.invalid) {
-                valid = false;
-                return false;
-            }
-        });
-
-        return valid;
-    }
-
-    runModalDialog({
-        title: 'Restore Configuration',
-        closeButton: true,
-        buttons: 'okcancel',
-        content: content,
-        onOk: function () {
-            if (!uiValid(true)) {
-                return false;
-            }
-            
-            refreshInterval = 1000000;
-
-            setTimeout(function () {
-                showModalDialog('<div style="text-align: center;"><span>Restoring configuration...</span><div class="modal-progress"></div></div>');
-                uploadFile(baseUri + 'config/restore/', fileInput, function (data) {
-                    if (data && data.ok) {
-                        var count = 0;
-                        function checkServer() {
-                            ajax('GET', baseUri + 'config/0/get/', null,
-                                function () {
-                                    runAlertDialog('The configuration has been restored!', function () {
-                                        window.location.reload(true);
-                                    });
-                                },
-                                function () {
-                                    if (count < 25) {
-                                        count += 1;
-                                        setTimeout(checkServer, 2000);
-                                    }
-                                    else {
-                                        runAlertDialog('Failed to restore the configuration!', function () {
-                                            window.location.reload(true);
-                                        });
-                                    }
-                                }
-                            );
-                        }
-                        
-                        if (data.reboot) {
-                            setTimeout(checkServer, 10000);
-                        }
-                        else {
-                            setTimeout(function () {
-                                window.location.reload();
-                            }, 5000);
-                        }
-                    }
-                    else {
-                        hideModalDialog();
-                        showErrorMessage('Failed to restore the configuration!');
-                    }
-                });
-            }, 10);
-        }
-    });
-}
-
-function doDownloadZipped(cameraId, groupKey) {
-    showModalDialog('<div class="modal-progress"></div>', null, null, true);
-    ajax('GET', baseUri + 'picture/' + cameraId + '/zipped/' + groupKey + '/', null, function (data) {
-        if (data.error) {
-            hideModalDialog(); /* progress */
-            showErrorMessage(data.error);
-        }
-        else {
-            hideModalDialog(); /* progress */
-            downloadFile('picture/' + cameraId + '/zipped/' + groupKey + '/?key=' + data.key);
-        }
-    });
-}
-
-function doDeleteFile(uri, callback) {
-    var url = window.location.href;
-    var parts = url.split('/');
-    url = parts.slice(0, 3).join('/') + uri;
-    
-    runConfirmDialog('Really delete this file?', function () {
-        showModalDialog('<div class="modal-progress"></div>', null, null, true);
-        ajax('POST', url, null, function (data) {
-            hideModalDialog(); /* progress */
-            hideModalDialog(); /* confirm */
-            
-            if (data == null || data.error) {
-                showErrorMessage(data && data.error);
-                return;
-            }
-
-            if (callback) {
-                callback();
-            }
-        });
-        
-        return false;
-    }, {stack: true});
-}
-
-function doDeleteAllFiles(mediaType, cameraId, groupKey, callback) {
-    runConfirmDialog('Really delete all ' + mediaType + 's in ' + groupKey + '?', function () {
-        showModalDialog('<div class="modal-progress"></div>', null, null, true);
-        ajax('POST', baseUri + mediaType + '/' + cameraId + '/delete_all/' + groupKey + '/', null, function (data) {
-            hideModalDialog(); /* progress */
-            hideModalDialog(); /* confirm */
-            
-            if (data == null || data.error) {
-                showErrorMessage(data && data.error);
-                return;
-            }
-
-            if (callback) {
-                callback();
-            }
-        });
-        
-        return false;
-    }, {stack: true});
-}
-
-
-    /* fetch & push */
-
-function fetchCurrentConfig(onFetch) {
-    function fetchCameraList() {
-        /* fetch the camera list */
-        ajax('GET', baseUri + 'config/list/', null, function (data) {
-            if (data == null || data.error) {
-                showErrorMessage(data && data.error);
-                data = {cameras: []};
-                if (onFetch) {
-                    onFetch(null);
-                }
-            }
-            
-            var i, cameras = data.cameras;
-            
-            if (isAdmin()) {
-                var cameraSelect = $('#cameraSelect');
-                cameraSelect.html('');
-                for (i = 0; i < cameras.length; i++) {
-                    var camera = cameras[i];
-                    cameraSelect.append('<option value="' + camera['id'] + '">' + camera['name'] + '</option>');
-                }
-                cameraSelect.append('<option value="add">add camera...</option>');
-                
-                var enabledCameras = cameras.filter(function (camera) {return camera['enabled'];});
-                if (enabledCameras.length > 0) { /* prefer the first enabled camera */
-                    cameraSelect[0].selectedIndex = cameras.indexOf(enabledCameras[0]);
-                    fetchCurrentCameraConfig(onFetch);
-                }
-                else if (cameras.length) { /* only disabled cameras */
-                    cameraSelect[0].selectedIndex = 0;
-                    fetchCurrentCameraConfig(onFetch);
-                }
-                else { /* no camera at all */
-                    cameraSelect[0].selectedIndex = -1;
-
-                    if (onFetch) {
-                        onFetch(data);
-                    }
-                }
-
-                updateConfigUi();
-            }
-            else { /* normal user */
-                if (!cameras.length) {
-                    /* normal user with no cameras doesn't make too much sense - force login */
-                    doLogout();
-                }
-
-                if (onFetch) {
-                    onFetch(data);
-                }
-            }
-
-            var mainLoadingProgressImg = $('img.main-loading-progress');
-            if (mainLoadingProgressImg.length) {
-                mainLoadingProgressImg.animate({'opacity': 0}, 200, function () {
-                    recreateCameraFrames(cameras);
-                    mainLoadingProgressImg.remove();
-                });
-            }
-            else {
-                recreateCameraFrames(cameras);
-            }
-        });
-    }
-    /* add a progress indicator */
-    $('div.page-container').append('<img class="main-loading-progress" src="' + staticUrl + 'img/main-loading-progress.gif">');
-
-    if (isAdmin()) {
-        /* fetch the main configuration */
-        ajax('GET', baseUri + 'config/main/get/', null, function (data) {
-            if (data == null || data.error) {
-                showErrorMessage(data && data.error);
-                return;
-            }
-            
-            dict2MainUi(data);
-            fetchCameraList();
-        });
-    }
-    else {
-        fetchCameraList();
-    }
-}
-
-function fetchCurrentCameraConfig(onFetch) {
-    var cameraId = $('#cameraSelect').val();
-    if (cameraId != null) {
-        ajax('GET', baseUri + 'config/' + cameraId + '/get/?force=true', null, function (data) {
-            if (data == null || data.error) {
-                showErrorMessage(data && data.error);
-                dict2CameraUi(null);
-                if (onFetch) {
-                    onFetch(null);
-                }
-                
-                return;
-            }
-            
-            dict2CameraUi(data);
-            if (onFetch) {
-                onFetch(data);
-            }
-        });
-    }
-    else {
-        dict2CameraUi({});
-        if (onFetch) {
-            onFetch({});
-        }
-    }
-}
-
-function pushMainConfig(reboot) {
-    var mainConfig = mainUi2Dict();
-    
-    pushConfigReboot = pushConfigReboot || reboot;
-    pushConfigs['main'] = mainConfig;
-    if (!isApplyVisible()) {
-        showApply();
-    }
-}
-
-function pushCameraConfig(reboot) {
-    var cameraConfig = cameraUi2Dict();
-    var cameraId = $('#cameraSelect').val();
-    
-    if (!cameraId) {
-        return; /* event triggered without a selected camera */
-    }
-
-    pushConfigReboot = pushConfigReboot || reboot;
-    pushConfigs[cameraId] = cameraConfig;
-    if (!isApplyVisible()) {
-        showApply();
-    }
-    
-    /* also update the config stored in the camera frame div */
-    var cameraFrame = $('div.camera-frame#camera' + cameraId);
-    if (cameraFrame.length) {
-        Object.update(cameraFrame[0].config, cameraConfig);
-    }
-}
-
-function pushPreview(control) {
-    var cameraId = $('#cameraSelect').val();
-    
-    var brightness = $('#brightnessSlider').val();
-    var contrast= $('#contrastSlider').val();
-    var saturation = $('#saturationSlider').val();
-    var hue = $('#hueSlider').val();
-    
-    var data = {};
-    
-    if (brightness !== '' && (!control || control == 'brightness')) {
-        data.brightness = brightness;
-    }
-    
-    if (contrast !== '' && (!control || control == 'contrast')) {
-        data.contrast = contrast;
-    }
-    
-    if (saturation !== '' && (!control || control == 'saturation')) {
-        data.saturation = saturation;
-    }
-    
-    if (hue !== '' && (!control || control == 'hue')) {
-        data.hue = hue;
-    }
-    
-    refreshDisabled[cameraId] |= 0;
-    refreshDisabled[cameraId]++;
-    
-    ajax('POST', baseUri + 'config/' + cameraId + '/set_preview/', data, function (data) {
-        refreshDisabled[cameraId]--;
-        
-        if (data == null || data.error) {
-            showErrorMessage(data && data.error);
-            return;
-        }
-    });
-}
-
-function getCameraIdsByInstance() {
-    /* a motion instance is identified by the (host, port) pair;
-     * the local instance has both the host and the port set to empty string */
-    
-    var cameraIdsByInstance = {};
-    $('div.camera-frame').each(function () {
-        var instance;
-        if (this.config.proto == 'netcam' || this.config.proto == 'v4l2') {
-            instance = '';
-        }
-        else if (this.config.proto == 'motioneye') {
-            instance = this.config.host || '';
-            if (this.config.port) {
-                instance += ':' + this.config.port;
-            }
-        }
-        else { /* assuming simple mjpeg camera */
-            return;
-        }
-        
-        (cameraIdsByInstance[instance] = cameraIdsByInstance[instance] || []).push(this.config.id);
-    });
-    
-    return cameraIdsByInstance;
-}
-
-
-    /* dialogs */
-
-function runAlertDialog(message, onOk, options) {
-    var params = {
-        title: message,
-        buttons: 'ok',
-        onOk: onOk
-    };
-    
-    if (options) {
-        Object.update(params, options);
-    }
-    
-    runModalDialog(params);
-}
-
-function runConfirmDialog(message, onYes, options) {
-    var params = {
-        title: message,
-        buttons: 'yesno',
-        onYes: onYes
-    };
-    
-    if (options) {
-        Object.update(params, options);
-    }
-    
-    runModalDialog(params);
-}
-
-function runLoginDialog(retry) {
-    /* a workaround so that browsers will remember the credentials */
-    var tempFrame = $('<iframe name="temp" id="temp" style="display: none;"></iframe>');
-    $('body').append(tempFrame);
-    
-    var form = 
-            $('<form action="' + baseUri + 'login/" target="temp" method="POST"><table class="login-dialog">' +
-                '<tr>' +
-                    '<td class="login-dialog-error" colspan="100"></td>' +
-                '</tr>' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Username</span></td>' +
-                    '<td class="dialog-item-value"><input type="text" name="username" class="styled" id="usernameEntry"></td>' +
-                '</tr>' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Password</span></td>' +
-                    '<td class="dialog-item-value"><input type="password" name="password" class="styled" id="passwordEntry"></td>' +
-                    '<input type="submit" style="display: none;" name="login" value="login">' +
-                '</tr>' +
-            '</table></form>');
-
-    var usernameEntry = form.find('#usernameEntry');
-    var passwordEntry = form.find('#passwordEntry');
-    var errorTd = form.find('td.login-dialog-error');
-    
-    if (window._loginRetry) {
-        errorTd.css('display', 'table-cell');
-        errorTd.html('Invalid credentials.');
-    }
-
-    var params = {
-        title: 'Login',
-        content: form,
-        buttons: [
-            {caption: 'Cancel', isCancel: true, click: function () {
-                tempFrame.remove();
-            }},
-            {caption: 'Login', isDefault: true, click: function () {
-                window.username = usernameEntry.val();
-                window.password = passwordEntry.val();
-                window._loginDialogSubmitted = true;
-                
-                setCookie('username', window.username);
-                
-                form.submit();
-                setTimeout(function () {
-                    tempFrame.remove();
-                }, 5000);
-                
-                if (retry) {
-                    retry();
-                }
-            }}
-        ],
-    };
-    
-    runModalDialog(params);
-}
-
-function runPictureDialog(entries, pos, mediaType) {
-    var content = $('<div class="picture-dialog-content"></div>');
-    
-    var img = $('<img class="picture-dialog-content">');
-    content.append(img);
-    
-    var prevArrow = $('<div class="picture-dialog-prev-arrow button mouse-effect" title="previous picture"></div>');
-    content.append(prevArrow);
-    
-    var nextArrow = $('<div class="picture-dialog-next-arrow button mouse-effect" title="next picture"></div>');
-    content.append(nextArrow);
-    
-    var progressImg = $('<img class="picture-dialog-progress" src="' + staticUrl + 'img/modal-progress.gif">');
-    
-    function updatePicture() {
-        var entry = entries[pos];
-
-        var windowWidth = $(window).width();
-        var windowHeight = $(window).height();
-        var widthCoef = windowWidth < 1000 ? 0.8 : 0.5;
-        var heightCoef = 0.75;
-        
-        var width = parseInt(windowWidth * widthCoef);
-        var height = parseInt(windowHeight * heightCoef);        
-        
-        prevArrow.css('display', 'none');
-        nextArrow.css('display', 'none');
-        img.parent().append(progressImg);
-        updateModalDialogPosition();
-        progressImg.css('left', (img.parent().width() - progressImg.width()) / 2);
-        progressImg.css('top', (img.parent().height() - progressImg.height()) / 2);
-        
-        img.attr('src', addAuthParams('GET', baseUri + mediaType + '/' + entry.cameraId + '/preview' + entry.path));
-        img.load(function () {
-            var aspectRatio = this.naturalWidth / this.naturalHeight;
-            var sizeWidth = width * width / aspectRatio;
-            var sizeHeight = height * aspectRatio * height;
-            
-            if (sizeWidth < sizeHeight) {
-                img.width(width);
-            }
-            else {
-                img.height(height);
-            }
-            updateModalDialogPosition();
-            prevArrow.css('display', pos > 0 ? '' : 'none');
-            nextArrow.css('display', pos < entries.length - 1 ? '' : 'none');
-            progressImg.remove();
-        });
-        
-        $('div.modal-container').find('span.modal-title:last').html(entry.name);
-        updateModalDialogPosition();
-    }
-    
-    prevArrow.click(function () {
-        if (pos > 0) {
-            pos--;
-        }
-        
-        updatePicture();
-    });
-    
-    nextArrow.click(function () {
-        if (pos < entries.length - 1) {
-            pos++;
-        }
-        
-        updatePicture();
-    });
-    
-    function bodyKeyDown(e) {
-        switch (e.which) {
-            case 37:
-                if (prevArrow.is(':visible')) {
-                    prevArrow.click();
-                }
-                break;
-                
-            case 39:
-                if (nextArrow.is(':visible')) {
-                    nextArrow.click();
-                }
-                break;
-        }
-    }
-    
-    $('body').on('keydown', bodyKeyDown);
-    
-    img.load(updateModalDialogPosition);
-    
-    runModalDialog({
-        title: ' ',
-        closeButton: true,
-        buttons: [
-            {caption: 'Close'},
-            {caption: 'Download', isDefault: true, click: function () {
-                var entry = entries[pos];
-                downloadFile(mediaType + '/' + entry.cameraId + '/download' + entry.path);
-                
-                return false;
-            }}
-        ],
-        content: content,
-        stack: true,
-        onShow: updatePicture,
-        onClose: function () {
-            $('body').off('keydown', bodyKeyDown);
-        }
-    });
-}
-
-function runAddCameraDialog() {
-    if (!$('#motionEyeSwitch')[0].checked) {
-        return runAlertDialog('Please enable motionEye first!');
-    }
-    
-    if (Object.keys(pushConfigs).length) {
-        return runAlertDialog('Please apply the modified settings first!');
-    }
-    
-    var content = 
-            $('<table class="add-camera-dialog">' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Camera Type</span></td>' +
-                    '<td class="dialog-item-value"><select class="styled" id="typeSelect">' +
-                        '<option value="v4l2">Local Camera</option>' +
-                        '<option value="netcam">Network Camera</option>' +
-                        '<option value="motioneye">Remote motionEye Camera</option>' +
-                        '<option value="mjpeg">Simple MJPEG Camera</option>' +
-                    '</select></td>' +
-                    '<td><span class="help-mark" title="the type of camera you wish to add">?</span></td>' +
-                '</tr>' +
-                '<tr class="motioneye netcam mjpeg">' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">URL</span></td>' +
-                    '<td class="dialog-item-value"><input type="text" class="styled" id="urlEntry" placeholder="http://example.com:8080/cams/..."></td>' +
-                    '<td><span class="help-mark" title="the camera URL (e.g. http://example.com:8080/cam/)">?</span></td>' +
-                '</tr>' +
-                '<tr class="motioneye netcam mjpeg">' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Username</span></td>' +
-                    '<td class="dialog-item-value"><input type="text" class="styled" id="usernameEntry" placeholder="username..."></td>' +
-                    '<td><span class="help-mark" title="the username for the URL, if required (e.g. admin)">?</span></td>' +
-                '</tr>' +
-                '<tr class="motioneye netcam mjpeg">' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Password</span></td>' +
-                    '<td class="dialog-item-value"><input type="password" class="styled" id="passwordEntry" placeholder="password..."></td>' +
-                    '<td><span class="help-mark" title="the password for the URL, if required">?</span></td>' +
-                '</tr>' +
-                '<tr class="v4l2 motioneye netcam mjpeg">' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Camera</span></td>' +
-                    '<td class="dialog-item-value"><select class="styled" id="addCameraSelect"></select><span id="cameraMsgLabel"></span></td>' +
-                    '<td><span class="help-mark" title="the camera you wish to add">?</span></td>' +
-                '</tr>' +
-                '<tr class="v4l2 motioneye netcam mjpeg">' +
-                    '<td colspan="100"><div class="dialog-item-separator"></div></td>' +
-                '</tr>' +
-                '<tr class="v4l2 motioneye netcam mjpeg">' +
-                    '<td class="dialog-item-value" colspan="100"><div id="addCameraInfo"></div></td>' +
-                '</tr>' +
-            '</table>');
-    
-    /* collect ui widgets */
-    var typeSelect = content.find('#typeSelect');
-    var urlEntry = content.find('#urlEntry');
-    var usernameEntry = content.find('#usernameEntry');
-    var passwordEntry = content.find('#passwordEntry');
-    var addCameraSelect = content.find('#addCameraSelect');
-    var addCameraInfo = content.find('#addCameraInfo');
-    var cameraMsgLabel = content.find('#cameraMsgLabel');
-    
-    /* make validators */
-    makeUrlValidator(urlEntry, true);
-    makeTextValidator(usernameEntry, false);
-    makeTextValidator(typeSelect, false);
-    makeComboValidator(addCameraSelect, true);
-    
-    /* ui interaction */
-    function updateUi() {
-        content.find('tr.v4l2, tr.motioneye, tr.netcam, tr.mjpeg').css('display', 'none');
-
-        if (typeSelect.val() == 'motioneye') {
-            content.find('tr.motioneye').css('display', 'table-row');
-            usernameEntry.val('admin');
-            usernameEntry.attr('readonly', 'readonly');
-            addCameraInfo.html(
-                    'Remote motionEye cameras are cameras installed behind another motionEye server. ' +
-                    'Adding them here will allow you to view and manage them remotely.');
-        }
-        else if (typeSelect.val() == 'netcam') {
-            usernameEntry.removeAttr('readonly');
-            
-            /* make sure there is one trailing slash so that
-             * an URI can be detected */
-            var url = urlEntry.val().trim();
-            var m = url.match(new RegExp('/', 'g'));
-            if (m && m.length < 3 && !url.endsWith('/')) {
-                urlEntry.val(url + '/');
-            }
-
-            content.find('tr.netcam').css('display', 'table-row');
-            addCameraInfo.html(
-                    'Network cameras (or IP cameras) are devices that natively stream RTSP or MJPEG videos or plain JPEG images. ' +
-                    "Consult your device's manual to find out the correct RTSP, MJPEG or JPEG URL.");
-        }
-        else if (typeSelect.val() == 'mjpeg') {
-            usernameEntry.removeAttr('readonly');
-            
-            /* make sure there is one trailing slash so that
-             * an URI can be detected */
-            var url = urlEntry.val().trim();
-            var m = url.match(new RegExp('/', 'g'));
-            if (m && m.length < 3 && !url.endsWith('/')) {
-                urlEntry.val(url + '/');
-            }
-
-            content.find('tr.mjpeg').css('display', 'table-row');
-            addCameraInfo.html(
-                    'Adding your device as a simple MJPEG camera instead of as a network camera will improve the framerate, ' +
-                    'but no motion detection, picture capturing or movie recording will be available for it. ' +
-                    'The camera must be accessible to both your server and your browser. ' +
-                    'This type of camera is not compatible with Internet Explorer.');
-        }
-        else { /* assuming v4l2 */
-            content.find('tr.v4l2').css('display', 'table-row');
-            addCameraInfo.html(
-                    'Local cameras are camera devices that are connected directly to your motionEye system. ' +
-                    'These are usually USB webcams or board-specific cameras.');
-        }
-        
-        updateModalDialogPosition();
-        
-        /* re-validate all the validators */
-        content.find('.validator').each(function () {
-            this.validate();
-        });
-        
-        if (uiValid()) {
-            listCameras();
-        }
-    }
-    
-    function uiValid(includeCameraSelect) {
-        var query = content.find('input, select');
-        if (!includeCameraSelect) {
-            query = query.not('#addCameraSelect');
-        }
-        else {
-            if (cameraMsgLabel.html() || !addCameraSelect.val()) {
-                return false;
-            }
-        }
-
-        /* re-validate all the validators */
-        content.find('.validator').each(function () {
-            this.validate();
-        });
-        
-        var valid = true;
-        query.each(function () {
-            if (this.invalid) {
-                valid = false;
-                return false;
-            }
-        });
-        
-        return valid;
-    }
-    
-    function splitCameraUrl(url) {
-        var parts = url.split('://');
-        var scheme = parts[0];
-        var index = parts[1].indexOf('/');
-        var host = null;
-        var uri = '';
-        if (index >= 0) {
-            host = parts[1].substring(0, index);
-            uri = parts[1].substring(index);
-        }
-        else {
-            host = parts[1];
-        }
-        
-        var port = '';
-        parts = host.split(':');
-        if (parts.length >= 2) {
-            host = parts[0];
-            port = parts[1];
-        }
-        
-        if (uri == '') {
-            uri = '/';
-        }
-        
-        return {
-            scheme: scheme,
-            host: host,
-            port: port,
-            uri: uri
-        };
-    }
-    
-    function listCameras() {
-        var progress = $('<div style="text-align: center; margin: 2px;"><img src="' + staticUrl + 'img/small-progress.gif"></div>');
-        
-        addCameraSelect.html('');
-        addCameraSelect.hide();
-        addCameraSelect.parent().find('div').remove(); /* remove any previous progress div */
-        addCameraSelect.before(progress);
-        
-        var data = {};
-        if (urlEntry.is(':visible') && urlEntry.val()) {
-            data = splitCameraUrl(urlEntry.val());
-        }
-        data.username = usernameEntry.val();
-        data.password = passwordEntry.val();
-        data.proto = typeSelect.val();
-        
-        cameraMsgLabel.html('');
-        
-        ajax('GET', baseUri + 'config/list/', data, function (data) {
-            progress.remove();
-            
-            if (data == null || data.error) {
-                cameraMsgLabel.html(data && data.error);
-                
-                return;
-            }
-            
-            if (data.error || !data.cameras) {
-                return;
-            }
-
-            data.cameras.forEach(function (info) {
-                var option = $('<option value="' + info.id + '">' + info.name + '</option>');
-                option[0]._extra_attrs = {};
-                Object.keys(info).forEach(function (key) {
-                    if (key == 'id' || key == 'name') {
-                        return;
-                    }
-                    
-                    var value = info[key];
-                    option[0]._extra_attrs[key] = value;
-                });
-
-                addCameraSelect.append(option);
-            });
-            
-            if (!data.cameras || !data.cameras.length) {
-                addCameraSelect.append('<option value="">(no cameras)</option>');
-            }
-            
-            addCameraSelect.show();
-            addCameraSelect[0].validate();
-        });
-    }
-    
-    typeSelect.change(function () {
-        addCameraSelect.html('');
-    });
-    
-    typeSelect.change(updateUi);
-    urlEntry.change(updateUi);
-    usernameEntry.change(updateUi);
-    passwordEntry.change(updateUi);
-    updateUi();
-
-    runModalDialog({
-        title: 'Add Camera...',
-        closeButton: true,
-        buttons: 'okcancel',
-        content: content,
-        onOk: function () {
-            if (!uiValid(true)) {
-                return false;
-            }
-
-            var data = {};
-            
-            if (typeSelect.val() == 'motioneye') {
-                data = splitCameraUrl(urlEntry.val());
-                data.proto = 'motioneye';
-                data.username = usernameEntry.val();
-                data.password = passwordEntry.val();
-                data.remote_camera_id = addCameraSelect.val();
-            }
-            else if (typeSelect.val() == 'netcam') {
-                data = splitCameraUrl(urlEntry.val());
-                data.username = usernameEntry.val();
-                data.password = passwordEntry.val();
-                data.proto = 'netcam';
-                data.camera_index = addCameraSelect.val();
-            }
-            else if (typeSelect.val() == 'mjpeg') {
-                data = splitCameraUrl(urlEntry.val());
-                data.username = usernameEntry.val();
-                data.password = passwordEntry.val();
-                data.proto = 'mjpeg';
-            }
-            else { /* assuming v4l2 */
-                data.proto = 'v4l2';
-                data.uri = addCameraSelect.val();
-            }
-            
-            /* add all extra attributes */
-            var option = addCameraSelect.find('option:eq(' + addCameraSelect[0].selectedIndex + ')')[0];
-            Object.keys(option._extra_attrs).forEach(function (key) {
-                var value = option._extra_attrs[key];
-                data[key] = value;
-            });
-
-            beginProgress();
-            ajax('POST', baseUri + 'config/add/', data, function (data) {
-                endProgress();
-
-                if (data == null || data.error) {
-                    showErrorMessage(data && data.error);
-                    return;
-                }
-                
-                var cameraOption = $('#cameraSelect').find('option[value=add]');
-                cameraOption.before('<option value="' + data.id + '">' + data.name + '</option>');
-                $('#cameraSelect').val(data.id).change();
-                recreateCameraFrames();
-            });
-        }
-    });
-}
-
-function runTimelapseDialog(cameraId, groupKey, group) {
-    var content = 
-            $('<table class="timelapse-dialog">' +
-                '<tr><td colspan="2" class="timelapse-warning"></td></tr>' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Group</span></td>' +
-                    '<td class="dialog-item-value">' + groupKey + '</td>' +
-                '</tr>' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Include a picture taken every</span></td>' +
-                    '<td class="dialog-item-value">' +
-                        '<select class="styled timelapse" id="intervalSelect">' + 
-                            '<option value="1">second</option>' +
-                            '<option value="5">5 seconds</option>' +
-                            '<option value="10">10 seconds</option>' +
-                            '<option value="30">30 seconds</option>' +
-                            '<option value="60">minute</option>' +
-                            '<option value="300">5 minutes</option>' +
-                            '<option value="600">10 minutes</option>' +
-                            '<option value="1800">30 minutes</option>' +
-                            '<option value="3600">hour</option>' +
-                        '</select>' +
-                    '</td>' +
-                    '<td><span class="help-mark" title="choose the interval of time between two selected pictures">?</span></td>' +
-                '</tr>' +
-                '<tr>' +
-                    '<td class="dialog-item-label"><span class="dialog-item-label">Movie framerate</span></td>' +
-                    '<td class="dialog-item-value"><input type="text" class="styled range" id="framerateSlider"></td>' +
-                    '<td><span class="help-mark" title="choose how fast you want the timelapse playback to be">?</span></td>' +
-                '</tr>' +
-            '</table>');
-
-    var intervalSelect = content.find('#intervalSelect');
-    var framerateSlider = content.find('#framerateSlider');
-    var timelapseWarning = content.find('td.timelapse-warning');
-    
-    if (group.length > 1440) { /* one day worth of pictures, taken 1 minute apart */
-        timelapseWarning.html('Given the large number of pictures, creating your timelapse might take a while!');
-        timelapseWarning.css('display', 'table-cell');
-    }
-    
-    makeSlider(framerateSlider, 1, 100, 0, [
-        {value: 1, label: '1'},
-        {value: 20, label: '20'},
-        {value: 40, label: '40'},
-        {value: 60, label: '60'},
-        {value: 80, label: '80'},
-        {value: 100, label: '100'}
-    ], null, 0);
-    
-    intervalSelect.val(60);
-    framerateSlider.val(20).each(function () {this.update()});
-
-    runModalDialog({
-        title: 'Create Timelapse Movie',
-        closeButton: true,
-        buttons: 'okcancel',
-        content: content,
-        onOk: function () {
-            var progressBar = $('<div style=""></div>');
-            makeProgressBar(progressBar);
-            
-            runModalDialog({
-                title: 'Creating Timelapse Movie...',
-                content: progressBar,
-                stack: true,
-                noKeys: true
-            });
-            
-            var url = baseUri + 'picture/' + cameraId + '/timelapse/' + groupKey + '/';
-            var data = {interval: intervalSelect.val(), framerate: framerateSlider.val()};
-            var first = true;
-            
-            function checkTimelapse() {
-                var actualUrl = url;
-                if (!first) {
-                    actualUrl += '?check=true';
-                }
-
-                ajax('GET', actualUrl, data, function (data) {
-                    if (data == null || data.error) {
-                        hideModalDialog(); /* progress */
-                        hideModalDialog(); /* timelapse dialog */
-                        showErrorMessage(data && data.error);
-                        return;
-                    }
-                    
-                    if (data.progress != -1 && first) {
-                        showPopupMessage('A timelapse movie is already being created.');
-                    }
-                    
-                    if (data.progress == -1 && !first && !data.key) {
-                        hideModalDialog(); /* progress */
-                        hideModalDialog(); /* timelapse dialog */
-                        showErrorMessage('The timelapse movie could not be created.');
-                        return;
-                    }
-                    
-                    if (data.progress == -1) {
-                        data.progress = 0;
-                    }
-
-                    if (data.key) {
-                        progressBar[0].setProgress(100);
-                        progressBar[0].setText('100%');
-                        
-                        setTimeout(function () {
-                            hideModalDialog(); /* progress */
-                            hideModalDialog(); /* timelapse dialog */
-                            downloadFile('picture/' + cameraId + '/timelapse/' + groupKey + '/?key=' + data.key);
-                        }, 500);
-                    }
-                    else {
-                        progressBar[0].setProgress(data.progress * 100);
-                        progressBar[0].setText(parseInt(data.progress * 100) + '%');
-                        setTimeout(checkTimelapse, 1000);
-                    }
-
-                    first = false;
-                });
-            }
-            
-            checkTimelapse();
-
-            return false;
-        },
-        stack: true
-    });
-}
-
-function runMediaDialog(cameraId, mediaType) {
-    var dialogDiv = $('<div class="media-dialog"></div>');
-    var mediaListDiv = $('<div class="media-dialog-list"></div>');
-    var groupsDiv = $('<div class="media-dialog-groups"></div>');
-    var buttonsDiv = $('<div class="media-dialog-buttons"></div>');
-    
-    var groups = {};
-    var groupKey = null;
-    
-    dialogDiv.append(groupsDiv);
-    dialogDiv.append(mediaListDiv);
-    dialogDiv.append(buttonsDiv);
-    
-    /* add a temporary div to compute 3em in px */
-    var tempDiv = $('<div style="width: 3em; height: 3em;"></div>');
-    $('div.modal-container').append(tempDiv);
-    var height = tempDiv.height();
-    tempDiv.remove();
-
-    function showGroup(key) {
-        groupKey = key;
-        
-        if (mediaListDiv.find('img.media-list-progress').length) {
-            return; /* already in progress of loading */
-        }
-        
-        /* (re)set the current state of the group buttons */
-        groupsDiv.find('div.media-dialog-group-button').each(function () {
-            var $this = $(this);
-            if (this.key == key) {
-                $this.addClass('current');
-            }
-            else {
-                $this.removeClass('current');
-            }
-        });
-        
-        var mediaListByName = {};
-        var entries = groups[key];
-        
-        /* cleanup the media list */
-        mediaListDiv.children('div.media-list-entry').detach();
-        mediaListDiv.html('');
-        
-        function addEntries() {
-            /* add the entries to the media list */
-            entries.forEach(function (entry, i) {
-                var entryDiv = entry.div;
-                var detailsDiv = null;
-                
-                if (!entryDiv) {
-                    entryDiv = $('<div class="media-list-entry"></div>');
-                    
-                    var previewImg = $('<img class="media-list-preview" src="' + staticUrl + 'img/modal-progress.gif"/>');
-                    entryDiv.append(previewImg);
-                    previewImg[0]._src = addAuthParams('GET', baseUri + mediaType + '/' + cameraId + '/preview' + entry.path + '?height=' + height);
-                    
-                    var downloadButton = $('<div class="media-list-download-button button">Download</div>');
-                    entryDiv.append(downloadButton);
-                    
-                    var deleteButton = $('<div class="media-list-delete-button button">Delete</div>');
-                    if (isAdmin()) {
-                        entryDiv.append(deleteButton);
-                    }
-
-                    var nameDiv = $('<div class="media-list-entry-name">' + entry.name + '</div>');
-                    entryDiv.append(nameDiv);
-                    
-                    detailsDiv = $('<div class="media-list-entry-details"></div>');
-                    entryDiv.append(detailsDiv);
-                    
-                    downloadButton.click(function () {
-                        downloadFile(mediaType + '/' + cameraId + '/download' + entry.path);
-                        return false;
-                    });
-                    
-                    deleteButton.click(function () {
-                        doDeleteFile(baseUri + mediaType + '/' + cameraId + '/delete' + entry.path, function () {
-                            entryDiv.remove();
-                            entries.splice(i, 1); /* remove entry from group */
-
-                            /* update text on group button */
-                            groupsDiv.find('div.media-dialog-group-button').each(function () {
-                                var $this = $(this);
-                                if (this.key == groupKey) {
-                                    var text = this.innerHTML;
-                                    text = text.substring(0, text.lastIndexOf(' '));
-                                    text += ' (' + entries.length + ')';
-                                    this.innerHTML = text;
-                                }
-                            });
-                        });
-                        
-                        return false;
-                    });
-
-                    entryDiv.click(function () {
-                        var pos = entries.indexOf(entry);
-                        runPictureDialog(entries, pos, mediaType);
-                    });
-                    
-                    entry.div = entryDiv;
-                }
-                else {
-                    detailsDiv = entry.div.find('div.media-list-entry-details');
-                }                    
-                
-                var momentSpan = $('<span class="details-moment">' + entry.momentStr + ', </span>');
-                var momentShortSpan = $('<span class="details-moment-short">' + entry.momentStrShort + '</span>');
-                var sizeSpan = $('<span class="details-size">' + entry.sizeStr + '</span>');
-                detailsDiv.empty();
-                detailsDiv.append(momentSpan);
-                detailsDiv.append(momentShortSpan);
-                detailsDiv.append(sizeSpan);
-                mediaListDiv.append(entryDiv);
-            });
-
-            /* trigger a scroll event */
-            mediaListDiv.scroll();
-        }
-        
-        /* if details are already fetched, simply add the entries and return */
-        if (entries[0].timestamp) {
-            return addEntries();
-        }
-        
-        var previewImg = $('<img class="media-list-progress" src="' + staticUrl + 'img/modal-progress.gif"/>');
-        mediaListDiv.append(previewImg);
-        
-        var url = baseUri + mediaType + '/' + cameraId + '/list/?prefix=' + (key || 'ungrouped');
-        ajax('GET', url, null, function (data) {
-            previewImg.remove();
-            
-            if (data == null || data.error) {
-                hideModalDialog();
-                showErrorMessage(data && data.error);
-                return;
-            }
-            
-            /* index the media list by name */
-            data.mediaList.forEach(function (media) {
-                var path = media.path;
-                var parts = path.split('/');
-                var name = parts[parts.length - 1];
-                
-                mediaListByName[name] = media;
-            });
-            
-            /* assign details to entries */
-                entries.forEach(function (entry) {
-                    var media = mediaListByName[entry.name];
-                    if (media) {
-                        entry.momentStr = media.momentStr;
-                        entry.momentStrShort = media.momentStrShort;
-                        entry.sizeStr = media.sizeStr;
-                        entry.timestamp = media.timestamp;
-                    }
-                });
-                /* sort the entries by timestamp */
-            entries.sortKey(function (e) {return e.timestamp || e.name;}, true);
-            
-            addEntries();
-        });
-    }
-    
-    if (mediaType == 'picture') {
-        var zippedButton = $('<div class="media-dialog-button">Zipped</div>');
-        buttonsDiv.append(zippedButton);
-        
-        zippedButton.click(function () {
-            if (groupKey != null) {
-                doDownloadZipped(cameraId, groupKey);
-            }
-        });
-        
-        var timelapseButton = $('<div class="media-dialog-button">Timelapse</div>');
-        buttonsDiv.append(timelapseButton);
-        
-        timelapseButton.click(function () {
-            if (groupKey != null) {
-                runTimelapseDialog(cameraId, groupKey, groups[groupKey]);
-            }
-        });
-    }
-
-    if (isAdmin()) {
-        var deleteAllButton = $('<div class="media-dialog-button media-dialog-delete-all-button">Delete All</div>');
-        buttonsDiv.append(deleteAllButton);
-        
-        deleteAllButton.click(function () {
-            if (groupKey != null) {
-                doDeleteAllFiles(mediaType, cameraId, groupKey, function () {
-                    /* delete th group button */
-                    groupsDiv.find('div.media-dialog-group-button').each(function () {
-                        var $this = $(this);
-                        if (this.key == groupKey) {
-                            $this.remove();
-                        }
-                    });
-                    
-                    /* delete the group itself */
-                    delete groups[groupKey];
-                    
-                    /* show the first existing group, if any */
-                    var keys = Object.keys(groups);
-                    if (keys.length) {
-                        showGroup(keys[0]);
-                    }
-                    else {
-                        hideModalDialog();
-                    }
-                });
-            }
-        });
-    }
-    
-    function updateDialogSize() {
-        var windowWidth = $(window).width();
-        var windowHeight = $(window).height();
-        
-        if (Object.keys(groups).length == 0) {
-            groupsDiv.width('auto');
-            groupsDiv.height('auto');
-            groupsDiv.addClass('small-screen');
-            mediaListDiv.width('auto');
-            mediaListDiv.height('auto');
-            buttonsDiv.hide();
-
-            return;
-        }
-        
-        buttonsDiv.show();
-        
-        if (windowWidth < 1000) {
-            mediaListDiv.width(parseInt(windowWidth * 0.8));
-            mediaListDiv.height(parseInt(windowHeight * 0.7));
-            groupsDiv.width(parseInt(windowWidth * 0.8));
-            groupsDiv.height('');
-            groupsDiv.addClass('small-screen');
-        }
-        else {
-            mediaListDiv.width(parseInt(windowWidth * 0.7));
-            mediaListDiv.height(parseInt(windowHeight * 0.7));
-            groupsDiv.height(parseInt(windowHeight * 0.7));
-            groupsDiv.width('');
-            groupsDiv.removeClass('small-screen');
-        }
-    }
-    
-    function onResize() {
-        updateDialogSize();
-        updateModalDialogPosition();
-    }
-    
-    $(window).resize(onResize);
-    
-    updateDialogSize();
-    
-    showModalDialog('<div class="modal-progress"></div>');
-    
-    /* fetch the media list */
-    ajax('GET', baseUri + mediaType + '/' + cameraId + '/list/', null, function (data) {
-        if (data == null || data.error) {
-            hideModalDialog();
-            showErrorMessage(data && data.error);
-            return;
-        }
-        
-        /* group the media */
-        data.mediaList.forEach(function (media) {
-            var path = media.path;
-            var parts = path.split('/');
-            var keyParts = parts.splice(0, parts.length - 1);
-            var key = keyParts.join('/');
-            
-            if (key.indexOf('/') === 0) {
-                key = key.substring(1);
-            }
-            
-            var list = (groups[key] = groups[key] || []);
-            
-            list.push({
-                'path': path,
-                'group': key,
-                'name': parts[parts.length - 1],
-                'cameraId': cameraId
-            });
-        });
-        
-        updateDialogSize();
-        
-        var keys = Object.keys(groups);
-        keys.sort();
-        keys.reverse();
-        
-        if (keys.length) {
-            keys.forEach(function (key) {
-                var groupButton = $('<div class="media-dialog-group-button"></div>');
-                groupButton.text((key || '(ungrouped)') + ' (' + groups[key].length + ')');
-                groupButton[0].key = key;
-                
-                groupButton.click(function () {
-                    showGroup(key);
-                });
-                
-                groupsDiv.append(groupButton);
-            });
-            
-            /* add tooltips to larger group buttons */
-            setTimeout(function () {
-                groupsDiv.find('div.media-dialog-group-button').each(function () {
-                    if (this.scrollWidth > this.offsetWidth) {
-                        this.title = this.innerHTML;
-                    }
-                });
-            }, 10);
-        }
-        else {
-            groupsDiv.html('(no media files)');
-            mediaListDiv.remove();
-        }
-        
-        var title;
-        if ($(window).width() < 1000) {
-            title = data.cameraName;
-        }
-        else if (mediaType === 'picture') {
-            title = 'Pictures taken by ' + data.cameraName;
-        }
-        else {
-            title = 'Movies recorded by ' + data.cameraName;
-        }
-        
-        runModalDialog({
-            title: title,
-            closeButton: true,
-            buttons: '',
-            content: dialogDiv,
-            onShow: function () {
-                //dialogDiv.scrollTop(dialogDiv.prop('scrollHeight'));
-                if (keys.length) {
-                    showGroup(keys[0]);
-                }
-            },
-            onClose: function () {
-                $(window).unbind('resize', onResize);
-            }
-        });
-    });
-    
-    /* install the media list scroll event handler */
-    mediaListDiv.scroll(function () {
-        var height = mediaListDiv.height();
-        
-        mediaListDiv.find('img.media-list-preview').each(function () {
-            if (!this._src) {
-                return;
-            }
-            
-            var $this = $(this);
-            var entryDiv = $this.parent();
-            
-            var top1 = entryDiv.position().top;
-            var top2 = top1 + entryDiv.height();
-            
-            if ((top1 >= 0 && top1 <= height) ||
-                (top2 >= 0 && top2 <= height)) {
-                
-                this.src = this._src;
-                delete this._src;
-            }
-        });
-    });
-}
-
-
-    /* camera frames */
-
-function addCameraFrameUi(cameraConfig) {
-    var pageContainer = $('div.page-container');
-    
-    if (cameraConfig == null) {
-        var cameraFrameDivPlaceHolder = $('<div class="camera-frame-place-holder"></div>');
-        pageContainer.append(cameraFrameDivPlaceHolder);
-        
-        return;
-    }
-    
-    var cameraId = cameraConfig.id;
-    
-    var cameraFrameDiv = $(
-            '<div class="camera-frame">' +
-                '<div class="camera-top-bar">' +
-                    '<span class="camera-name"></span>' +
-                    '<div class="camera-buttons">' +
-                        '<div class="button camera-button mouse-effect full-screen" title="full-screen window"></div>' +
-                        '<div class="button camera-button mouse-effect media-pictures" title="pictures"></div>' +
-                        '<div class="button camera-button mouse-effect media-movies" title="movies"></div>' +
-                        '<div class="button camera-button mouse-effect configure" title="configure"></div>' +
-                    '</div>' +
-                '</div>' +
-                '<div class="camera-container">' +
-                    '<div class="camera-placeholder"><img class="no-camera" src="' + staticUrl + 'img/no-camera.svg"></div>' +
-                    '<img class="camera">' +
-                    '<div class="camera-progress"><img class="camera-progress"></div>' +
-                '</div>' +
-            '</div>');
-    
-    var nameSpan = cameraFrameDiv.find('span.camera-name');
-    var configureButton = cameraFrameDiv.find('div.camera-button.configure');
-    var picturesButton = cameraFrameDiv.find('div.camera-button.media-pictures');
-    var moviesButton = cameraFrameDiv.find('div.camera-button.media-movies');
-    var fullScreenButton = cameraFrameDiv.find('div.camera-button.full-screen');
-    var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
-    var cameraProgress = cameraFrameDiv.find('div.camera-progress');
-    var cameraImg = cameraFrameDiv.find('img.camera');
-    var progressImg = cameraFrameDiv.find('img.camera-progress');
-    
-    /* no camera buttons if not admin */
-    if (!isAdmin()) {
-        configureButton.hide();
-    }
-    
-    /* no media buttons for simple mjpeg cameras */
-    if (cameraConfig['proto'] == 'mjpeg') {
-        picturesButton.hide();
-        moviesButton.hide();
-    }
-    
-    cameraFrameDiv.attr('id', 'camera' + cameraId);
-    cameraFrameDiv[0].refreshDivider = 0;
-    cameraFrameDiv[0].config = cameraConfig;
-    nameSpan.html(cameraConfig.name);
-    progressImg.attr('src', staticUrl + 'img/camera-progress.gif');
-    
-    cameraProgress.click(function () {
-        doFullScreenCamera(cameraId);
-    });
-    
-    cameraProgress.addClass('visible');
-    cameraPlaceholder.css('opacity', '0');
-    
-    /* insert the new camera frame at the right position,
-     * with respect to the camera id */
-    var cameraFrames = pageContainer.find('div.camera-frame');
-    var cameraIds = cameraFrames.map(function () {return parseInt(this.id.substring(6));});
-    cameraIds.sort();
-    
-    var index = 0; /* find the first position that is greater than the current camera id */
-    while (index < cameraIds.length && cameraIds[index] < cameraId) {
-        index++;
-    }
-    
-    if (index < cameraIds.length) {
-        var beforeCameraFrame = pageContainer.find('div.camera-frame#camera' + cameraIds[index]);
-        cameraFrameDiv.insertAfter(beforeCameraFrame);
-    }
-    else  {
-        pageContainer.append(cameraFrameDiv);
-    }
-
-    /* fade in */
-    cameraFrameDiv.animate({'opacity': 1}, 100);
-    
-    /* add the button handlers */
-    configureButton.click(function () {
-        doConfigureCamera(cameraId);
-    });
-
-    picturesButton.click(function (cameraId) {
-        return function () {
-            runMediaDialog(cameraId, 'picture');
-        };
-    }(cameraId));
-    
-    moviesButton.click(function (cameraId) {
-        return function () {
-            runMediaDialog(cameraId, 'movie');
-        };
-    }(cameraId));
-    
-    fullScreenButton.click(function (cameraId) {
-        return function () {
-            var url = baseUri + 'picture/' + cameraId + '/frame/';
-            window.open(url, '_blank');
-        };
-    }(cameraId));
-    
-    /* error and load handlers */
-    cameraImg.error(function () {
-        this.error = true;
-        this.loading = 0;
-        
-        cameraImg.addClass('error').removeClass('loading');
-        cameraImg.height(Math.round(cameraImg.width() * 0.75));
-        cameraPlaceholder.css('opacity', 1);
-        cameraProgress.removeClass('visible');
-        cameraFrameDiv.removeClass('motion-detected');
-    });
-    cameraImg.load(function () {
-        if (refreshDisabled[cameraId]) {
-            return; /* refresh temporarily disabled for updating */
-        }
-        
-        this.error = false;
-        this.loading = 0;
-        
-        cameraImg.removeClass('error').removeClass('loading');
-        cameraImg.css('height', '');
-        cameraPlaceholder.css('opacity', 0);
-        cameraProgress.removeClass('visible');
-        
-        /* there's no point in looking for a cookie update more often than once every second */
-        var now = new Date().getTime();
-        if ((!this.lastCookieTime || now - this.lastCookieTime > 1000) && (cameraFrameDiv[0].config['proto'] != 'mjpeg')) {
-            if (getCookie('motion_detected_' + cameraId) == 'true') {
-                cameraFrameDiv.addClass('motion-detected');
-            }
-            else {
-                cameraFrameDiv.removeClass('motion-detected');
-            }
-            
-            this.lastCookieTime = now;
-        }
-
-        if (fullScreenCameraId) {
-            /* update the modal dialog position when image is loaded */
-            updateModalDialogPosition();
-        }
-    });
-    
-    cameraImg.addClass('loading');
-    cameraImg.height(Math.round(cameraImg.width() * 0.75));
-}
-
-function remCameraFrameUi(cameraId) {
-    var pageContainer = $('div.page-container');
-    var cameraFrameDiv = pageContainer.find('div.camera-frame#camera' + cameraId);
-    cameraFrameDiv.animate({'opacity': 0}, 100, function () {
-        cameraFrameDiv.remove();
-    });
-}
-
-function recreateCameraFrames(cameras) {
-    var pageContainer = $('div.page-container');
-    
-    function updateCameras(cameras) {
-        cameras = cameras.filter(function (camera) {return camera.enabled;});
-        var i, camera;
-
-        /* remove everything on the page */
-        pageContainer.children().remove();
-        
-        /* add camera frames */
-        for (i = 0; i < cameras.length; i++) {
-            camera = cameras[i];
-            addCameraFrameUi(camera);
-        }
-        
-        if ($('#cameraSelect').find('option').length < 2 && isAdmin() && $('#motionEyeSwitch')[0].checked) {
-            /* invite the user to add a camera */
-            var addCameraLink = $('<div class="add-camera-message">' + 
-                    '<a href="javascript:runAddCameraDialog()">You have not configured any camera yet. Click here to add one...</a></div>');
-            pageContainer.append(addCameraLink);
-        }
-    }
-    
-    if (cameras != null) {
-        updateCameras(cameras);
-    }
-    else {
-        ajax('GET', baseUri + 'config/list/', null, function (data) {
-            if (data == null || data.error) {
-                showErrorMessage(data && data.error);
-                return;
-            }
-            
-            updateCameras(data.cameras);
-        });
-    }
-    
-    /* update settings panel */
-    var cameraId = $('#cameraSelect').val();
-    if (cameraId && cameraId != 'add') {
-        openSettings(cameraId);
-    }
-}
-
-
-function doConfigureCamera(cameraId) {
-    if (inProgress) {
-        return;
-    }
-    
-    hideApply();
-    pushConfigs = {};
-    pushConfigReboot = false;
-    
-    openSettings(cameraId);
-}
-
-function doFullScreenCamera(cameraId) {
-    if (inProgress || refreshDisabled[cameraId]) {
-        return;
-    }
-    
-    if (fullScreenCameraId != null) {
-        return; /* a camera is already in full screen */
-    }
-    
-    fullScreenCameraId = -1; /* avoids successive fast toggles of fullscreen */
-    
-    var cameraFrameDiv = $('#camera' + cameraId);
-    var cameraName = cameraFrameDiv.find('span.camera-name').text();
-    var frameImg = cameraFrameDiv.find('img.camera');
-    var aspectRatio = frameImg.width() / frameImg.height();
-    var windowWidth = $(window).width();
-    var windowHeight = $(window).height();
-    var windowAspectRatio = windowWidth / windowHeight;
-    var frameIndex = cameraFrameDiv.index();
-    var pageContainer = $('div.page-container');
-    
-    if (frameImg.hasClass('error')) {
-        return; /* no full screen for erroneous cameras */
-    }
-
-    var width;
-    if (windowAspectRatio > aspectRatio) {
-        width = aspectRatio * Math.round(0.8 * windowHeight);
-    }
-    else {
-        width = Math.round(0.9 * windowWidth);
-    }
-
-    cameraFrameDiv.find('div.camera-progress').addClass('visible');
-    
-    var cameraImg = cameraFrameDiv.find('img.camera');
-    cameraImg.load(function showFullScreenCamera() {
-        cameraFrameDiv.css('width', width);
-        fullScreenCameraId = cameraId;
-        
-        runModalDialog({
-            title: cameraName,
-            closeButton: true,
-            content: cameraFrameDiv,
-            onShow: function () {
-                cameraImg.unbind('load', showFullScreenCamera);
-            },
-            onClose: function () {
-                fullScreenCameraId = null;
-                cameraFrameDiv.css('width', '');
-                var nextFrame = pageContainer.children('div:eq(' + frameIndex + ')');
-                if (nextFrame.length) {
-                    nextFrame.before(cameraFrameDiv);
-                }
-                else {
-                    pageContainer.append(cameraFrameDiv);
-                }
-            }
-        });
-    });
-    
-    if (cameraFrameDiv[0].config['proto'] == 'mjpeg') {
-        /* manually trigger the load event on simple mjpeg cameras */
-        cameraImg.load();
-    }
-}
-
-function refreshCameraFrames() {
-    function refreshCameraFrame(cameraId, img, serverSideResize) {
-        if (refreshDisabled[cameraId]) {
-            /* camera refreshing disabled, retry later */
-            
-            return;
-        }
-        
-        if (img.loading) {
-            img.loading++; /* increases each time the camera would refresh but is still loading */
-            
-            if (img.loading > 2 * 1000 / refreshInterval) { /* limits the retries to one every two seconds */
-                img.loading = 0;
-            }
-            else {
-                return; /* wait for the previous frame to finish loading */
-            }
-        }
-        
-        var timestamp = new Date().getTime();
-        var uri = baseUri + 'picture/' + cameraId + '/current/?_=' + timestamp;
-        if (serverSideResize) {
-            uri += '&width=' + img.width;
-        }
-        
-        uri = addAuthParams('GET', uri);
-        
-        img.src = uri;
-        img.loading = 1;
-    }
-
-    var cameraFrames;
-    if (fullScreenCameraId != null && fullScreenCameraId >= 0) {
-        cameraFrames = $('#camera' + fullScreenCameraId);
-    }
-    else {
-        cameraFrames = $('div.page-container').find('div.camera-frame');
-    }
-    
-    cameraFrames.each(function () {
-        if (!this.img) {
-            this.img = $(this).find('img.camera')[0];
-            if (this.config['proto'] == 'mjpeg') {
-                var url = this.config['url'].replace('127.0.0.1', window.location.host.split(':')[0]);
-                url += (url.indexOf('?') > 0 ? '&' : '?') + '_=' + new Date().getTime();
-                this.img.src = url;
-            }
-        }
-        
-        if (this.config['proto'] == 'mjpeg') {
-            return; /* no manual refresh for simple mjpeg cameras */
-        }
-        
-        /* at a refresh interval of 50ms, the refresh rate is limited to 20 fps */
-        var count = 1000 / (refreshInterval * this.config['streaming_framerate']);
-        var serverSideResize = this.config['streaming_server_resize'];
-        
-        if (count <= 2) {
-            /* skipping frames (showing the same frame twice) at this rate won't be visible,
-             * while the effective framerate will be as close as possible to the motion's one */
-            count -= 1;
-        }
-        
-        if (this.img.error) {
-            /* in case of error, decrease the refresh rate to 1 fps */
-            count = 1000 / refreshInterval;
-        }
-        
-        if (this.refreshDivider < count) {
-            this.refreshDivider++;
-        }
-        else {
-            var cameraId = this.id.substring(6);
-            refreshCameraFrame(cameraId, this.img, serverSideResize);
-            
-            this.refreshDivider = 0;
-        }
-    });
-    
-    setTimeout(refreshCameraFrames, refreshInterval);
-}
-
-function checkCameraErrors() {
-    /* properly triggers the onerror event on the cameras whose imgs were not successfully loaded,
-     * but the onerror event hasn't been triggered, for some reason (seems to happen in Chrome) */
-    var cameraFrames = $('div.page-container').find('img.camera');
-    
-    cameraFrames.each(function () {
-        if (this.complete === true && this.naturalWidth === 0 && !this.error && this.src) {
-            $(this).error();
-        }
-    });
-    
-    setTimeout(checkCameraErrors, 500);
-}
-
-
-    /* startup function */
-
-$(document).ready(function () {
-    /* detect base uri */
-    if (frame) {
-        baseUri = qualifyUri('../../../');
-
-    }
-    else {
-        baseUri = splitUrl(qualifyUri('')).baseUrl;
-
-        /* restore the username from cookie */
-        window.username = getCookie('username');
-    }
-    
-    /* open/close settings */
-    $('div.settings-button').click(function () {
-        if (isSettingsOpen()) {
-            closeSettings();
-        }
-        else {
-            openSettings();
-        }
-    });
-    
-    /* software update button */
-    $('div#updateButton').click(doUpdate);
-    
-    /* backup/restore */
-    $('div#backupButton').click(doBackup);
-    $('div#restoreButton').click(doRestore);
-    
-    /* prevent scroll events on settings div from propagating TODO this does not actually work */
-    $('div.settings').mousewheel(function (e, d) {
-        var t = $(this);
-        if (d > 0 && t.scrollTop() === 0) {
-            e.preventDefault();
-        }
-        else if (d < 0 && (t.scrollTop() === t.get(0).scrollHeight - t.innerHeight())) {
-            e.preventDefault();
-        }
-    });
-    
-    initUI();
-    beginProgress();
-    
-    ajax('GET', baseUri + 'login/', null, function () {
-        if (!frame) {
-            fetchCurrentConfig(endProgress);
-        }
-    });
-    
-    refreshCameraFrames();
-    checkCameraErrors();
-});
-
diff --git a/static/js/ui.js b/static/js/ui.js
deleted file mode 100644 (file)
index 7dfa9bb..0000000
+++ /dev/null
@@ -1,1100 +0,0 @@
-
-var _modalDialogContexts = [];
-
-
-    /* UI widgets */
-
-function makeCheckBox($input) {
-    $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');
-        }
-        
-        function setOff() {
-            text.html('OFF');
-            mainDiv.removeClass('on');
-        }
-        
-        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;
-    });
-}
-
-function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decimals, unit) {
-    unit = unit || '';
-
-    $input.each(function () {
-        var $this = $(this);
-        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;
-            }
-            
-            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) {
-            $this.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);
-            }
-        }
-        
-        function bodyMouseUp(e) {
-            bar[0]._mouseDown = false;
-    
-            $('body').unbind('mousemove', bodyMouseMove);
-            $('body').unbind('mouseup', bodyMouseUp);
-            
-            cursorLabel.css('display', 'none');
-            
-            $this.change();
-        }
-        
-        bar.mousedown(function (e) {
-            if (e.which > 1) {
-                return;
-            }
-            
-            this._mouseDown = true;
-            bodyMouseMove(e);
-    
-            $('body').mousemove(bodyMouseMove);
-            $('body').mouseup(bodyMouseUp);
-            
-            slider.focus();
-            cursorLabel.css('display', 'inline-block');
-            
-            return false;
-        });
-        
-        /* ticks */
-        var autoTicks = (ticks == null);
-        
-        function makeTicks() {
-            if (ticksNumber == null) {
-                ticksNumber = 11; 
-            }
-    
-            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});
-                }
-            }
-            
-            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>');
-                
-                labels.append(span);
-                span.css('left', (pos - 10) + '%');
-            }
-            
-            return ticks;
-        }
-        
-        makeTicks();
-    
-        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].setMaxVal = function (mv) {
-            maxVal = mv;
-    
-            makeTicks();
-            
-            input2slider();
-        };
-    });
-}
-
-function makeProgressBar($div) {
-    $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>');
-    
-        $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 (required == null) {
-        required = true;
-    }
-
-    $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;
-            }
-    
-            return true;
-        }
-        
-        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;
-            }
-        }
-        
-        $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 (required == null) {
-        required = true;
-    }
-
-    $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;
-            }
-    
-            return true;
-        }
-        
-        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;
-            }
-        }
-        
-        $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 (minVal == null) {
-        minVal = -Infinity;
-    }
-    if (maxVal == null) {
-        maxVal = Infinity;
-    }
-    if (floating == null) {
-        floating = false;
-    }
-    if (sign == null) {
-        sign = false;
-    }
-    if (required == null) {
-        required = true;
-    }
-
-    $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 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;
-            }
-            else {
-                msg += ' greater than ' + minVal;
-            }
-        }
-        else {
-            if (isFinite(maxVal)) {
-                msg += ' smaller than ' + maxVal;
-            }
-        }
-        
-        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;
-            }
-        }
-        
-        $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) {
-    $input.each(function () {
-        var $this = $(this);
-
-        function isValid(strVal) {
-            if (!$this.is(':visible')) {
-                return true; /* an invisible element is considered always valid */
-            }
-    
-            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 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;
-            }
-        }
-        
-        $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) {
-    $input.each(function () {
-        var $this = $(this);
-
-        function isValid(strVal) {
-            if (!$this.is(':visible')) {
-                return true; /* an invisible element is considered always valid */
-            }
-    
-            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 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;
-            }
-        }
-        
-        $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 makeFileValidator($input, required) {
-    if (required == null) {
-        required = true;
-    }
-
-    $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;
-            }
-    
-            return true;
-        }
-        
-        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;
-            }
-        }
-        
-        $this.keyup(validate);
-        $this.blur(validate);
-        $this.change(validate).change();
-        
-        $this.addClass('validator');
-        $this.addClass('file-validator');
-        $this.each(function () {
-            var oldValidate = this.validate;
-            this.validate = function () {
-                if (oldValidate) {
-                    oldValidate.call(this);
-                }
-                validate();
-            }
-        });
-    });
-}
-function makeCustomValidator($input, isValidFunc) {
-    $input.each(function () {
-        var $this = $(this);
-        
-        function isValid(strVal) {
-            if (!$this.is(':visible')) {
-                return true; /* an invisible element is considered always valid */
-            }
-            
-            return isValidFunc(strVal);
-        }
-        
-        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;
-            }
-        }
-    
-        $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();
-            }
-        });
-    });
-}
-
-
-    /* other input value processors */
-
-function makeStrippedInput($input) {
-    $input.change(function () {
-        this.value = $.trim(this.value);
-    });
-}
-
-function makeCharReplacer($input, oldChars, newStr) {
-    $input.change(function () {
-        this.value = this.value.replace(new RegExp('[' + oldChars + ']', 'g'), newStr);
-    });
-}
-
-
-    /* modal dialog */
-
-function showModalDialog(content, onClose, onShow, stack) {
-    var glass = $('div.modal-glass');
-    var container = $('div.modal-container');
-    
-    if (container.is(':animated')) {
-        return setTimeout(function () {
-            showModalDialog(content, onClose, onShow, stack);
-        }, 100);
-    }
-    
-    if (container.is(':visible') && stack) {
-        /* the modal dialog is already visible,
-         * we just replace the content */
-        
-        var children = container.children(':visible');
-        _modalDialogContexts.push({
-            children: children,
-            onClose: container[0]._onClose,
-        });
-        
-        children.css('display', 'none');
-        updateModalDialogPosition();
-        
-        container[0]._onClose = onClose; /* set the new onClose handler */
-        container.append(content);
-        updateModalDialogPosition();
-        
-        if (onShow) {
-            onShow();
-        }
-        
-        return;
-    }
-    
-    glass.css('display', 'block');
-    glass.animate({'opacity': '0.7'}, 200);
-    
-    container[0]._onClose = onClose; /* remember the onClose handler */
-    container.html(content);
-    
-    container.css('display', 'block');
-    updateModalDialogPosition();
-    container.animate({'opacity': '1'}, 200);
-    
-    if (onShow) {
-        onShow();
-    }
-}
-
-function hideModalDialog() {
-    var glass = $('div.modal-glass');
-    var container = $('div.modal-container');
-    
-    if (container.is(':animated')) {
-        return setTimeout(function () {
-            hideModalDialog();
-        }, 100);
-    }
-    
-    if (_modalDialogContexts.length) {
-        if (container[0]._onClose) {
-            container[0]._onClose();
-        }
-        
-        container.children(':visible').remove();
-        
-        var context = _modalDialogContexts.pop();
-        context.children.css('display', '');
-        container[0]._onClose = context.onClose;
-        updateModalDialogPosition();
-        
-        return;
-    }
-    
-    glass.animate({'opacity': '0'}, 200, function () {
-        glass.css('display', 'none');
-    });
-    
-    container.animate({'opacity': '0'}, 200, function () {
-        container.css('display', 'none');
-        container.html('');
-    });
-    
-    /* run the onClose handler, if supplied */
-    if (container[0]._onClose) {
-        container[0]._onClose();
-    }
-}
-
-function updateModalDialogPosition() {
-    var container = $('div.modal-container');
-    if (!container.is(':visible')) {
-        return;
-    }
-    
-    var windowWidth = $(window).width();
-    var windowHeight = $(window).height();
-    var modalWidth, modalHeight, i;
-    
-    /* repeat the operation multiple times, the size might change */
-    for (i = 0; i < 3; i++) {
-        modalWidth = container.outerWidth();
-        modalHeight = container.outerHeight();
-        
-        container.css('left', Math.floor((windowWidth - modalWidth) / 2));
-        container.css('top', Math.floor((windowHeight - modalHeight) / 2));
-    }
-}
-
-function makeModalDialogButtons(buttonsInfo) {
-    /* buttonsInfo is an array of:
-     * * caption: String
-     * * isDefault: Boolean
-     * * click: Function
-     */
-    
-    var buttonsContainer = $('<table class="modal-buttons-container"><tr></tr></table>');
-    var tr = buttonsContainer.find('tr');
-    
-    buttonsInfo.forEach(function (info) {
-        var buttonDiv = $('<div class="button dialog mouse-effect"></div>');
-        
-        buttonDiv.attr('tabIndex', '0'); /* make button focusable */
-        buttonDiv.html(info.caption);
-        
-        if (info.isDefault) {
-            buttonDiv.addClass('default');
-        }
-        
-        if (info.click) {
-            var oldClick = info.click;
-            info.click = function () {
-                if (oldClick() == false) {
-                    return false;
-                }
-                
-                hideModalDialog();
-                
-                return false;
-            };
-        }
-        else {
-            info.click = hideModalDialog; /* every button closes the dialog */
-        }
-        
-        buttonDiv.click(info.click);
-
-        var td = $('<td></td>');
-        td.append(buttonDiv);
-        tr.append(td);
-    });
-    
-    /* limit the size of the buttons container */
-    buttonsContainer.css('max-width', (buttonsInfo.length * 10) + 'em');
-    
-    return buttonsContainer;
-}
-
-function makeModalDialogTitleBar(options) {
-    /* available options:
-     * * title: String
-     * * closeButton: Boolean
-     */
-    
-    var titleBar = $('<div class="modal-title-bar"></div>');
-    
-    var titleSpan = $('<span class="modal-title"></span>');
-    titleSpan.html(options.title || '');
-    if (options.closeButton) {
-        titleSpan.css('margin', '0px 1.5em');
-    }
-    
-    titleBar.append(titleSpan);
-    
-    if (options.closeButton) {
-        var closeButton = $('<div class="button modal-close-button mouse-effect" title="close"></div>');
-        closeButton.click(hideModalDialog);
-        titleBar.append(closeButton);
-    }
-    
-    return titleBar;
-}
-
-function runModalDialog(options) {
-    /* available options:
-     * * title: String
-     * * closeButton: Boolean
-     * * content: any
-     * * buttons: 'ok'|'yesno'|'okcancel'|Array
-     * * onYes: Function
-     * * onNo: Function
-     * * onOk: Function
-     * * onCancel: Function
-     * * onClose: Function
-     * * onShow: Function
-     * * stack: Boolean
-     * * noKeys: Boolean
-     */
-    
-    var content = $('<div></div>');
-    var titleBar = null;
-    var buttonsDiv = null;
-    var defaultClick = null;
-    var cancelClick = null;
-    
-    /* add title bar */
-    if (options.title) {
-        titleBar = makeModalDialogTitleBar({title: options.title, closeButton: options.closeButton});
-        content.append(titleBar);
-    }
-    
-    /* add supplied content */
-    if (options.content) {
-        var contentWrapper = $('<div style="padding: 10px;"></div>');
-        contentWrapper.append(options.content);
-        content.append(contentWrapper);
-    }
-    
-    /* add buttons */
-    if (options.buttons === 'yesno') {
-        options.buttons = [
-            {caption: 'No', click: options.onNo},
-            {caption: 'Yes', isDefault: true, click: options.onYes}
-        ];
-    }
-    if (options.buttons === 'yesnocancel') {
-        options.buttons = [
-            {caption: 'Cancel', isCancel: true, click: options.onCancel},
-            {caption: 'No', click: options.onNo},
-            {caption: 'Yes', isDefault: true, click: options.onYes}
-        ];
-    }
-    else if (options.buttons === 'okcancel') {
-        options.buttons = [
-            {caption: 'Cancel', isCancel:true, click: options.onCancel},
-            {caption: 'OK', isDefault: true, click: options.onOk}
-        ];
-    }
-    else if (options.buttons === 'ok') {
-        options.buttons = [
-            {caption: 'OK', isDefault: true, click: options.onOk}
-        ];
-    }
-    
-    if (options.buttons) {
-        buttonsDiv = makeModalDialogButtons(options.buttons);
-        content.append(buttonsDiv);
-        
-        options.buttons.forEach(function (info) {
-            if (info.isDefault) {
-                defaultClick = info.click;
-            }
-            else if (info.isCancel) {
-                cancelClick = info.click;
-            }
-        });
-    }
-    
-    /* add some margins */
-    if ((buttonsDiv || options.content) && titleBar) {
-        titleBar.css('margin-bottom', '5px');
-    }
-    
-    if (buttonsDiv && options.content) {
-        buttonsDiv.css('margin-top', '5px');
-    }
-    
-    var handleKeyUp = !options.noKeys && function (e) {
-        if (!content.is(':visible')) {
-            return;
-        }
-        
-        switch (e.which) {
-            case 13:
-                if (defaultClick && defaultClick() == false) {
-                    return;
-                }
-                
-                hideModalDialog();
-                
-                break;
-           
-            case 27:
-                if (cancelClick && cancelClick() == false) {
-                    return;
-                }
-
-                hideModalDialog();
-
-                break;
-        }
-    };
-    
-    var onClose = function () {
-        if (options.onClose) {
-            options.onClose();
-        }
-        
-        /* unbind html handlers */
-        
-        $('html').unbind('keyup', handleKeyUp);
-    };
-    
-    /* bind key handlers */
-    $('html').bind('keyup', handleKeyUp);
-    
-    /* and finally, show the dialog */
-
-    showModalDialog(content, onClose, options.onShow, options.stack);
-    
-    /* focus the default button if nothing else is focused */
-    if (content.find('*:focus').length === 0) {
-        content.find('div.button.default').focus();
-    }
-}
-
-
-    /* popup message */
-
-function showPopupMessage(message, type) {
-    var container = $('div.popup-message-container');
-    var content = $('<span class="popup-message"></span>');
-    
-    if (window._popupMessageTimeout) {
-        clearTimeout(window._popupMessageTimeout);
-    }
-    
-    content.html(message);
-    content.addClass(type);
-    container.html(content);
-    
-    var windowWidth = $(window).width();
-    var messageWidth = container.width();
-    
-    container.css('display', 'block');
-    container.css('left', (windowWidth - messageWidth) / 2);
-
-    container.animate({'opacity': '1'}, 200);
-    
-    window._popupMessageTimeout = setTimeout(function () {
-        window._popupMessageTimeout = null;
-        container.animate({'opacity': '0'}, 200, function () {
-            container.css('display', 'none');
-        });
-    }, 5000);
-}
diff --git a/static/js/version.js b/static/js/version.js
deleted file mode 100644 (file)
index 7d43298..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-
-$(window).load(function () {
-    if (window.parent && window.parent.postMessage) {
-        window.parent.postMessage({'hostname': hostname, 'version': version, 'url': window.location.href.replace('version/', '')}, '*');
-    }
-});
diff --git a/templates/base.html b/templates/base.html
deleted file mode 100644 (file)
index 6679b8d..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        {% block meta %}
-            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-            <meta http-equiv="X-UA-Compatible" content="IE=Edge">
-            <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
-            <meta name="apple-mobile-web-app-capable" content="yes">
-            <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
-        {% endblock %}
-        <title>{% block title %}{% endblock %}</title>
-        {% block style %}
-            <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/jquery.timepicker.css" />
-            <link rel="shortcut icon" href="/static/favicon.ico" />
-            <link rel="apple-touch-icon" href="/static/favicon.ico" />
-        {% endblock %}
-        {% block script %}
-            <script type="text/javascript" src="{{STATIC_URL}}js/css-browser-selector.js"></script>
-            <script type="text/javascript" src="{{STATIC_URL}}js/jquery.min.js"></script>
-            <script type="text/javascript" src="{{STATIC_URL}}js/jquery.timepicker.min.js"></script>
-            <script type="text/javascript" src="{{STATIC_URL}}js/jquery.mousewheel.js"></script>
-            
-            <script type="text/javascript">
-                var staticUrl = '{{STATIC_URL}}';
-            </script>
-        {% endblock %}
-    </head>
-    
-    <body>
-        {% block body %}{% endblock %}
-    </body>
-</html>
diff --git a/templates/main.html b/templates/main.html
deleted file mode 100644 (file)
index 199e08f..0000000
+++ /dev/null
@@ -1,826 +0,0 @@
-{% extends "base.html" %}
-
-{% macro config_item(config) -%}
-    <tr class="settings-item additional-config {% if config['advanced'] %}advanced-setting{% endif %}"
-            {% if config.get('reboot') and enable_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['max']}}"{% 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 %}
-            {% if config.get('validate') %}validate="{{config['validate']}}"{% endif %}>
-        {% if config['type'] == 'separator' %}
-        <td colspan="100"><div class="settings-item-separator"></div></td>
-        {% else %}
-        <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>
-            {% elif config['type'] == 'html' %}
-                <div class="html styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Html">
-            {% endif %}
-        </td>
-        <td>{% if config.get('description') %}<span class="help-mark" title="{{config['description']}}">?</span>{% endif %}</td>
-        {% endif %}
-    </tr>
-{%- endmacro %}
-
-{% block title %}{% if title %}{{title}}{% else %}{{hostname}}{% endif %}{% endblock %}
-
-{% block style %}
-    {{super()}}
-    <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/ui.css" />
-    <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/main.css" />
-    {% if frame %}
-        <link rel="stylesheet" type="text/css" href="{{STATIC_URL}}css/frame.css" />
-    {% endif %}
-{% endblock %}
-
-{% block script %}
-    {{super()}}
-    <script type="text/javascript" src="{{STATIC_URL}}js/ui.js"></script>
-    <script type="text/javascript" src="{{STATIC_URL}}js/main.js"></script>
-    {% if frame %}
-        <script type="text/javascript" src="{{STATIC_URL}}js/frame.js"></script>
-    {% endif %}
-    <script type="text/javascript">
-        var adminUsername = '{{admin_username}}';
-        var frame = {% if frame %}true{% else %}false{% endif %};
-        var baseUri = null;
-    </script>
-{% endblock %}
-
-{% block body %}
-    {% if not frame %}
-    <div class="header">
-        <div class="header-container">
-            <div class="settings-top-bar closed">
-                <div class="button settings-button mouse-effect" title="settings"></div>
-                <div class="button logout-button mouse-effect" title="switch user"></div>
-                <select class="styled" id="cameraSelect" {% if not add_remove_cameras %}style="display: none;"{% endif %}></select>
-                <div class="button rem-camera-button mouse-effect" id="remCameraButton" title="remove camera" {% if not add_remove_cameras %}style="display: none;"{% endif %}></div>
-                <div class="button apply-button" id="applyButton">Apply</div>
-                {% if hostname %}<div class="hostname">{{hostname}}</div>{% endif %}
-            </div>
-            <div class="button logout-button mouse-effect" title="switch user"></div>
-            <div class="logo">
-                <a href="/">
-                    <span class="logo">motionEye</span>
-                    <img class="logo" src="{{STATIC_URL}}img/motioneye-logo.svg">
-                </a>
-            </div>
-        </div>
-    </div>
-    {% endif %}
-    {% if not frame %}
-    <div class="page">
-        <div class="settings closed">
-            <div class="settings-container">
-                <div class="settings-section-title">
-                    <input type="checkbox" class="styled section general main-config" id="motionEyeSwitch">
-                    <span class="help-mark" title="general settings, not related to any camera">?</span>
-                    <a class="settings-section-title">General Settings</a>
-                    <span class="minimize open"></span>
-                </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 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" 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 main-config" id="adminUsernameEntry" readonly="readonly"></td>
-                        <td><span class="help-mark" title="the username to be used for configuring the system">?</span></td>
-                    </tr>
-                    <tr class="settings-item" strip="true" {% if enable_reboot %}reboot="true"{% endif %}>
-                        <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 main-config" id="adminPasswordEntry"></td>
-                        <td><span class="help-mark" title="administrator's password">?</span></td>
-                    </tr>
-                    <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 main-config" id="normalUsernameEntry"></td>
-                        <td><span class="help-mark" title="the username to be used for video surveillance">?</span></td>
-                    </tr>
-                    <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 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>
-                    {% 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 class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Current Version</span></td>
-                        <td class="settings-item-value"><span class="settings-item-label">{{version}}</span></td>
-                    </tr>
-                    {% if enable_update %}
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Software Update</span></td>
-                        <td class="settings-item-value"><div class="button normal-button update-button" id="updateButton">Check</div></td>
-                        <td><span class="help-mark" title="checks for new versions and performs updates">?</span></td>
-                    </tr>
-                    {% endif %}
-                    <tr class="settings-item advanced-setting{% if not enable_reboot %} hidden{% endif %}">
-                        <td class="settings-item-label"><span class="settings-item-label">Power</span></td>
-                        <td class="settings-item-value"><div class="button normal-button shut-down-button" id="shutDownButton">Shut Down</div></td>
-                        <td><span class="help-mark" title="shuts down the system">?</span></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting{% if not enable_reboot %} hidden{% endif %}">
-                        <td class="settings-item-label"><span class="settings-item-label"></span></td>
-                        <td class="settings-item-value"><div class="button normal-button reboot-button" id="rebootButton">Reboot</div></td>
-                        <td><span class="help-mark" title="reboots the system">?</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">
-                        <td class="settings-item-label"><span class="settings-item-label">Configuration</span></td>
-                        <td class="settings-item-value"><div class="button normal-button backup-button" id="backupButton">Backup</div></td>
-                        <td><span class="help-mark" title="creates a file with the current configuration for you to save it locally">?</span></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label"></span></td>
-                        <td class="settings-item-value"><div class="button normal-button restore-button" id="restoreButton">Restore</div></td>
-                        <td><span class="help-mark" title="restores the configuration from a previously saved backup file">?</span></td>
-                    </tr>
-                </table>
-
-                {% 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 %}">
-                    {% if section.get('onoff') %}<input type="checkbox" class="styled section additional-section {{section['name']}} main-config" id="{{section['name']}}Switch">{% endif %}
-                    {% if section.get('description') %}<span class="help-mark" title="{{section['description']}}">?</span>{% endif %}
-                    <a class="settings-section-title">{{section['label']}}</a>
-                    <span class="minimize {% if section.get('open') %}open{% endif %}"></span>
-                </div>
-                <table class="settings {% if section.get('advanced') %}advanced-setting{% endif %}">
-                    {% for config in section['configs'] %}
-                        {{config_item(config)}}
-                    {% endfor %}
-                </table>
-                {% endif %}                    
-                {% endfor %}
-
-                <tr class="settings-item advanced-setting">
-                    <td colspan="100"><div class="settings-item-separator" style="margin: 0px 0px 1.5em 0px;"></div></td>
-                </tr>
-                
-                <div class="settings-section-title">
-                    <input type="checkbox" class="styled section device camera-config" id="videoDeviceSwitch">
-                    <span class="help-mark" title="enable this if you want to use this camera device">?</span>
-                    <a class="settings-section-title">Video Device</a>
-                    <span class="minimize open"></span>
-                </div>
-                <table class="settings">
-                    <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 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 camera-config" id="deviceUriEntry" readonly="readonly"></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Camera Type</span></td>
-                        <td class="settings-item-value"><input type="text" class="styled device camera-config" id="deviceTypeEntry" readonly="readonly"></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 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 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" 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 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" 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 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" 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 camera-config" id="saturationSlider"></td>
-                        <td><span class="help-mark" title="sets a desired saturation level for this camera">?</span></td>
-                    </tr>
-                    <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 camera-config" id="hueSlider"></td>
-                        <td><span class="help-mark" title="sets a desired hue for this camera">?</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">
-                        <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 camera-config" id="resolutionSelect">
-                            </select>
-                        </td>
-                        <td><span class="help-mark" title="the video resolution (larger values produce better quality but require more CPU power, larger storage space and bandwidth)">?</span></td>
-                    </tr>
-                    <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 camera-config" id="rotationSelect">
-                                <option value="0">0&deg;</option>
-                                <option value="90">90&deg;</option>
-                                <option value="180">180&deg;</option>
-                                <option value="270">270&deg;</option>
-                            </select>
-                        </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" 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 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 more CPU power, larger storage space and bandwidth)">?</span></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Extra Motion Options</span></td>
-                        <td class="settings-item-value"><textarea class="styled device camera-config" id="extraOptionsEntry" rows="3"></textarea></td>
-                        <td><span class="help-mark" title="you can add here any extra options for the motion daemon (use the &quot;name value&quot; format, one option per line)">?</span></td>
-                    </tr>
-                    {% for config in camera_sections.get('device', {}).get('configs', []) %}
-                        {{config_item(config)}}
-                    {% endfor %}
-                </table>
-                
-                <div class="settings-section-title advanced-setting">
-                    <span class="help-mark" title="choose where and how your media files are saved">?</span>
-                    <a class="settings-section-title">File Storage</a>
-                    <span class="minimize"></span>
-                </div>
-                <table class="settings advanced-setting">
-                    <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 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" 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 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" 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 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" 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 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" 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 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" 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 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">
-                        <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">Disk Usage</span></td>
-                        <td class="settings-item-value">
-                            <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 camera-config" id="textOverlaySwitch">
-                    <span class="help-mark" title="choose what information is displayed on the captured frames">?</span>
-                    <a class="settings-section-title">Text Overlay</a>
-                    <span class="minimize"></span>
-                </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 camera-config" id="leftTextSelect">
-                                <option value="camera-name">Camera Name</option>
-                                <option value="timestamp">Timestamp</option>
-                                <option value="custom-text">Custom Text</option>
-                                <option value="disabled">Disabled</option>
-                            </select>
-                        </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" required="true" strip="true">
-                        <td class="settings-item-label"></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 camera-config" id="rightTextSelect">
-                                <option value="camera-name">Camera Name</option>
-                                <option value="timestamp">Timestamp</option>
-                                <option value="custom-text">Custom Text</option>
-                                <option value="disabled">Disabled</option>
-                            </select>
-                        </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" required="true" strip="true">
-                        <td class="settings-item-label"></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" minimize-switch-independent="true">
-                    <input type="checkbox" class="styled section streaming camera-config" id="videoStreamingSwitch">
-                    <span class="help-mark" title="enable this if you want video streaming for this camera">?</span>
-                    <a class="settings-section-title">Video Streaming</a>
-                    <span class="minimize"></span>
-                </div>
-                <table class="settings">
-                    <tr class="settings-item advanced-setting localhost-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 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 localhost-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 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 localhost-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 camera-config" id="streamingServerResizeSwitch"></td>
-                        <td><span class="help-mark" title="when this is enabled, the images are resized before they are sent to the browser (disable when running on a slow CPU)">?</span></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting localhost-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 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" 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 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>
-                    {% if not old_motion %}
-                    <tr class="settings-item advanced-setting">
-                        <td class="settings-item-label"><span class="settings-item-label">Authentication Mode</span></td>
-                        <td class="settings-item-value">
-                            <select class="styled streaming camera-config" id="streamingAuthModeSelect">
-                                <option value="disabled">Disabled</option>
-                                <option value="basic">Basic</option>
-                                <option value="digest">Digest</option>
-                            </select>
-                        </td>
-                        <td><span class="help-mark" title="the authentication mode to use when accessing the stream (use Basic instead of Digest if you encounter issues with third party apps)">?</span></td>
-                    </tr>
-                    {% endif %}
-                    <tr class="settings-item advanced-setting localhost-streaming">
-                        <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 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">
-                        <td colspan="100"><div class="settings-item-separator"></div></td>
-                    </tr>
-                    <tr class="settings-item advanced-setting localhost-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 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 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 localhost-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 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 camera-config" id="stillImagesSwitch">
-                    <span class="help-mark" title="enable this if you want to capture still images (pictures)">?</span>
-                    <a class="settings-section-title">Still Images</a>
-                    <span class="minimize"></span>
-                </div>
-                <table class="settings">
-                    <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 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" 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 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 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>
-                            </select>
-                        </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" 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 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 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>
-                                <option value="365">For One Year</option>
-                                <option value="0">Forever</option>
-                                <option value="-1">Custom</option>
-                            </select>
-                        </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" 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 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 camera-config" id="motionDetectionSwitch">
-                    <span class="help-mark" title="enable this to use and configure the motion detection mechanism">?</span>
-                    <a class="settings-section-title">Motion Detection</a>
-                    <span class="minimize"></span>
-                </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 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" 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 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 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" 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 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" 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 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" 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 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" 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 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" 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 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 camera-config" id="motionMoviesSwitch">
-                    <span class="help-mark" title="enable this if you want to record motion movies">?</span>
-                    <a class="settings-section-title">Motion Movies</a>
-                    <span class="minimize"></span>
-                </div>
-                <table class="settings">
-                    <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 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" 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 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 advanced-setting" min="0" max="86400" required="true">
-                        <td class="settings-item-label"><span class="settings-item-label">Maximum Movie Length</span></td>
-                        <td class="settings-item-value"><input type="text" class="number styled motion-movies camera-config" id="maxMovieLengthEntry"><span class="settings-item-unit">seconds</span></td>
-                        <td><span class="help-mark" title="sets the maximum length of motion movies, in seconds; if the motion event lasts longer, a new movie file is created; use 0 for infinite length">?</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 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>
-                                <option value="365">For One Year</option>
-                                <option value="0">Forever</option>
-                                <option value="-1">Custom</option>
-                            </select>
-                        </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" 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 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">
-                    <span class="help-mark" title="enable this if you want to be notified when motion is detected">?</span>
-                    <a class="settings-section-title">Motion Notifications</a>
-                    <span class="minimize"></span>
-                </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 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" 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 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" 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 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" 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 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" 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 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" 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 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 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" 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 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); set to 0 to disable picture attachments">?</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">
-                        <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 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" 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 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 camera-config" id="webHookHttpMethodSelect">
-                                <option value="GET">GET</option>
-                                <option value="POST">POST</option>
-                            </select>
-                        </td>
-                        <td><span class="help-mark" title="the HTTP method to use when requesting the web hook URL">?</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">
-                        <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 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" 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 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 camera-config" id="workingScheduleSwitch">
-                    <span class="help-mark" title="enable this if you want to define a weekly working schedule for motion detection">?</span>
-                    <a class="settings-section-title">Working Schedule</a>
-                    <span class="minimize"></span>
-                </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 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 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 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 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 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 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 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 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 %}">
-                    {% if section.get('onoff') %}<input type="checkbox" class="styled section additional-section {{section['name']}} camera-config" id="{{section['name']}}Switch">{% endif %}
-                    {% if section.get('description') %}<span class="help-mark" title="{{section['description']}}">?</span>{% endif %}
-                    <a class="settings-section-title">{{section['label']}}</a>
-                    <span class="minimize {% if section.get('open') %}open{% endif %}"></span>
-                </div>
-                <table class="settings">
-                    {% for config in section['configs'] %}
-                        {{config_item(config)}}
-                    {% endfor %}
-                </table>
-                {% endif %}                    
-                {% endfor %}
-
-                <div class="settings-progress"></div>
-            </div>
-        </div>
-        <img class="background-logo" src="{{STATIC_URL}}img/motioneye-logo.svg" onmousedown="return false;">
-        <div class="page-container"></div>
-        <div class="footer">
-            <div class="copyright-note">copyright &copy; Calin Crisan</div>
-        </div> 
-    </div>
-    {% else %}
-        <div class="camera-frame" id="camera{{camera_id}}"
-                streaming_framerate="{{camera_config['stream_maxrate']}}" streaming_server_resize="{{camera_config['@webcam_server_resize']}}"
-                proto="{{camera_config['@proto']}}" url="{{camera_config['@url']}}">
-            
-            <div class="camera-container">
-                <div class="camera-placeholder"><img class="no-camera" src="{{STATIC_URL}}img/no-camera.svg"></div><img
-                    class="camera"><div class="camera-progress"><img class="camera-progress"></div>
-            </div>
-        </div>
-    {% endif %}
-    <div class="modal-glass"></div>
-    <div class="modal-container"></div>
-    <div class="popup-message-container"></div>
-{% endblock %}
diff --git a/templates/version.html b/templates/version.html
deleted file mode 100644 (file)
index 3c7a8f8..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "base.html" %}
-
-{% block script %}
-    {{super()}}
-    <script type="text/javascript">
-        var hostname = '{{hostname}}';
-        var version = '{{version}}';
-    </script>
-    <script type="text/javascript" src="{{STATIC_URL}}js/version.js"></script>
-{% endblock %}
-
-{% block body %}
-hostname = "{{hostname}}"<br>
-version = "{{version}}"
-{% endblock %}
diff --git a/webhook.py b/webhook.py
deleted file mode 100755 (executable)
index 016a0db..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>. 
-
-import logging
-import sys
-import urllib2
-import urlparse
-
-import settings
-
-from motioneye import _configure_settings, _configure_logging
-
-
-_configure_settings()
-_configure_logging()
-
-
-def print_usage():
-    print 'Usage: webhook.py <method> <url>'
-
-
-if __name__ == '__main__':
-    if len(sys.argv) < 3:
-        print_usage()
-        sys.exit(-1)
-    
-    method = sys.argv[1] 
-    url = sys.argv[2]
-
-    logging.debug('method = %s' % method)
-    logging.debug('url = %s' % url)
-    
-    if method == 'POST':
-        parts = urlparse.urlparse(url)
-        data = parts.query
-
-    else:
-        data = None
-
-    request = urllib2.Request(url, data)
-    try:
-        urllib2.urlopen(request, timeout=settings.REMOTE_REQUEST_TIMEOUT)
-        logging.debug('webhook successfully called')
-    
-    except Exception as e:
-        logging.error('failed to call webhook: %s' % e)