From: Calin Crisan Date: Sat, 5 Oct 2013 19:54:13 +0000 (+0300) Subject: added a remote module X-Git-Url: http://www.vanbest.org/gitweb/?a=commitdiff_plain;h=741e84cc5eee2be9f58933f39e79344a4d78b9db;p=motioneye-debian added a remote module --- diff --git a/doc/todo.txt b/doc/todo.txt index e243170..46666f8 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -1,3 +1,7 @@ +-> move/copy list available resolutions code to get_config +-> bug: adding a remote device does not provide the available resolutions + +-> bug: if updating a remote camera config, local motion will get restarted -> make camera frames positions configurable -> hide horrible 404 image on cameras -> prevent Request closed errors by stopping mjpg clients before stopping motion diff --git a/src/config.py b/src/config.py index ad7921e..89f7f09 100644 --- a/src/config.py +++ b/src/config.py @@ -179,12 +179,13 @@ def get_camera(camera_id, as_lines=False): data = _conf_to_dict(lines) # determine the enabled status - main_config = get_main() - threads = main_config.get('thread', []) - data['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads - data['@id'] = camera_id - - _set_default_motion_camera(data) + if data['@proto'] == 'v4l2': + main_config = get_main() + threads = main_config.get('thread', []) + data['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads + data['@id'] = camera_id + + _set_default_motion_camera(data) return data @@ -192,24 +193,23 @@ def get_camera(camera_id, as_lines=False): def set_camera(camera_id, data): # TODO use a cache - _set_default_motion_camera(data) - - # 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 data['@enabled'] and config_file_name not in threads: - threads.append(config_file_name) - - elif not data['@enabled']: - threads = [t for t in threads if t != config_file_name] - - main_config['thread'] = threads - - set_main(main_config) + if data.get('@proto') == 'v4l2': + _set_default_motion_camera(data) + + # 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 data['@enabled'] and config_file_name not in threads: + threads.append(config_file_name) + + elif not data['@enabled']: + threads = [t for t in threads if t != config_file_name] + + main_config['thread'] = threads + + set_main(main_config) - del data['@enabled'] - # read the actual configuration from file config_file_path = _CAMERA_CONFIG_FILE_PATH % {'id': camera_id} if os.path.isfile(config_file_path): @@ -248,9 +248,12 @@ def set_camera(camera_id, data): return data -def add_camera(device): +def add_camera(device_details): # TODO use a cache + device = device_details.get('device') + proto = device_details.get('proto') + # determine the last camera id camera_ids = get_camera_ids() @@ -260,25 +263,24 @@ def add_camera(device): logging.info('adding new camera with id %(id)s...' % {'id': camera_id}) - # get device type - proto = None - if device.count('://'): - proto, device = device.split('://', 1) - # add the default camera config data = OrderedDict() data['@name'] = 'Camera' + str(camera_id) data['@proto'] = proto data['@enabled'] = True - data['videodevice'] = device - - # find a suitable resolution - for (w, h) in v4l2ctl.list_resolutions(device): - if w > 300: - data['width'] = w - data['height'] = h - break + for k, v in device_details.items(): + data['@' + k] = v + + if proto == 'v4l2': + data['videodevice'] = device + # find a suitable resolution + for (w, h) in v4l2ctl.list_resolutions(device): # TODO move/copy this code to handler/get_config + if w > 300: + data['width'] = w + data['height'] = h + break + # write the configuration to file set_camera(camera_id, data) @@ -527,7 +529,6 @@ def _set_default_motion_camera(data): data.setdefault('quality', 75) data.setdefault('@preserve_images', 0) - data.setdefault('motion_movies', False) data.setdefault('ffmpeg_variable_bitrate', 14) data.setdefault('movie_filename', '%Y-%m-%d-%H-%M-%S') data.setdefault('ffmpeg_cap_new', False) @@ -537,4 +538,3 @@ def _set_default_motion_camera(data): data.setdefault('@motion_notifications_emails', '') data.setdefault('@working_schedule', '') - diff --git a/src/handlers.py b/src/handlers.py index 117154a..a650469 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -7,6 +7,7 @@ from tornado.web import RequestHandler, HTTPError import config import mjpgclient import motionctl +import remote import template import v4l2ctl @@ -90,11 +91,29 @@ class ConfigHandler(BaseHandler): raise HTTPError(404, 'no such camera') camera_config = config.get_camera(camera_id) + if camera_config['@proto'] != 'v4l2': + try: + remote_data = remote.get_config( + camera_config.get('@host'), + camera_config.get('@port'), + camera_config.get('@username'), + camera_config.get('@password'), + camera_config.get('@remote_camera_id')) + + except Exception as e: + return self.finish_json({'error': unicode(e)}) + + remote_data = self._camera_ui_to_dict(remote_data) + + camera_config.update(remote_data) + ui_config = self._camera_dict_to_ui(camera_config) - resolutions = v4l2ctl.list_resolutions(camera_config['videodevice']) - resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions] - ui_config['available_resolutions'] = resolutions + if camera_config['@proto'] == 'v4l2': + resolutions = v4l2ctl.list_resolutions(camera_config['videodevice']) + resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions] + ui_config['available_resolutions'] = resolutions + self.finish_json(ui_config) else: @@ -112,7 +131,7 @@ class ConfigHandler(BaseHandler): raise - restart = bool(data.get('restart')) + restart = bool(data.get('last')) if restart and motionctl.running(): motionctl.stop() @@ -123,9 +142,20 @@ class ConfigHandler(BaseHandler): camera_ids = config.get_camera_ids() if camera_id not in camera_ids: raise HTTPError(404, 'no such camera') - - data = self._camera_ui_to_dict(data) - config.set_camera(camera_id, data) + + camera_config = config.get_camera(camera_id) + if camera_config['@proto'] == 'v4l2': + data = self._camera_ui_to_dict(data) + config.set_camera(camera_id, data) + + else: + remote.set_config( + camera_config.get('@host'), + camera_config.get('@port'), + camera_config.get('@username'), + camera_config.get('@password'), + camera_config.get('@remote_camera_id'), + data) else: logging.debug('setting main config') @@ -187,13 +217,33 @@ class ConfigHandler(BaseHandler): def list_cameras(self): logging.debug('listing cameras') + + host = self.get_argument('host', None) + port = self.get_argument('port', None) + username = self.get_argument('username', None) + password = self.get_argument('password', None) + + if host: # remote + try: + cameras = remote.list_cameras(host, port, username, password) + + except Exception as e: + return self.finish_json({'error': unicode(e)}) - cameras = [] - for camera_id in config.get_camera_ids(): - data = config.get_camera(camera_id) - data = self._camera_dict_to_ui(data) - data['id'] = camera_id - cameras.append(data) + else: + cameras = [] + for camera_id in config.get_camera_ids(): + data = config.get_camera(camera_id) + if data['@proto'] == 'v4l2': + data = self._camera_dict_to_ui(data) + + else: + data = { + 'name': data['@name'] + } + + data['id'] = camera_id + cameras.append(data) self.finish_json({'cameras': cameras}) @@ -213,29 +263,50 @@ class ConfigHandler(BaseHandler): def add_camera(self): logging.debug('adding new camera') - device = self.get_argument('device') - camera_id, data = config.add_camera(device) + 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_id, data = config.add_camera(device_details) data['@id'] = camera_id - data['@enabled'] = True - if motionctl.running(): + if motionctl.running() and data['@proto'] == 'v4l2': motionctl.stop() - if config.has_enabled_cameras(): + if config.has_enabled_cameras() and data['@proto'] == 'v4l2': motionctl.start() + + try: + remote_data = remote.get_config( + device_details.get('host'), + device_details.get('port'), + device_details.get('username'), + device_details.get('password'), + device_details.get('remote_camera_id')) + + except Exception as e: + return self.finish_json({'error': unicode(e)}) + + remote_data = self._camera_ui_to_dict(remote_data) + remote_data.update(data) - self.finish_json(self._camera_dict_to_ui(data)) + self.finish_json(self._camera_dict_to_ui(remote_data)) def rem_camera(self, camera_id): logging.debug('removing camera %(id)s' % {'id': camera_id}) + local = config.get_camera(camera_id).get('@proto') == 'v4l2' config.rem_camera(camera_id) - if motionctl.running(): + if motionctl.running() and local: motionctl.stop() - if config.has_enabled_cameras(): + if config.has_enabled_cameras() and local: motionctl.start() def _main_ui_to_dict(self, ui): @@ -259,10 +330,6 @@ class ConfigHandler(BaseHandler): } def _camera_ui_to_dict(self, ui): - video_device = ui.get('device', '') - if video_device.count('://'): - video_device = video_device.split('://')[-1] - if not ui.get('resolution'): # avoid errors for empty resolution setting ui['resolution'] = '352x288' @@ -270,7 +337,6 @@ class ConfigHandler(BaseHandler): # device '@name': ui.get('name', ''), '@enabled': ui.get('enabled', False), - 'videodevice': video_device, 'lightswitch': int(ui.get('light_switch_detect', False) * 5), 'auto_brightness': ui.get('auto_brightness', False), 'brightness': max(1, int(round(int(ui.get('brightness', 0)) * 2.55))), @@ -396,17 +462,17 @@ class ConfigHandler(BaseHandler): # device 'name': data['@name'], 'enabled': data['@enabled'], - 'id': data['@id'], - 'device': data['@proto'] + '://' + data['videodevice'], - 'light_switch_detect': data['lightswitch'] > 0, - 'auto_brightness': data['auto_brightness'], - 'brightness': int(round(int(data['brightness']) / 2.55)), - 'contrast': int(round(int(data['contrast']) / 2.55)), - 'saturation': int(round(int(data['saturation']) / 2.55)), - 'hue': int(round(int(data['hue']) / 2.55)), - 'resolution': str(data['width']) + 'x' + str(data['height']), - 'framerate': int(data['framerate']), - 'rotation': int(data['rotate']), + 'id': data.get('@id'), + 'proto': data['@proto'], + 'light_switch_detect': data.get('lightswitch') > 0, + 'auto_brightness': data.get('auto_brightness'), + 'brightness': int(round(int(data.get('brightness')) / 2.55)), + 'contrast': int(round(int(data.get('contrast')) / 2.55)), + 'saturation': int(round(int(data.get('saturation')) / 2.55)), + 'hue': int(round(int(data.get('hue')) / 2.55)), + 'resolution': str(data.get('width')) + 'x' + str(data.get('height')), + 'framerate': int(data.get('framerate')), + 'rotation': int(data.get('rotate')), # file storage 'storage_device': data['@storage_device'], @@ -414,7 +480,7 @@ class ConfigHandler(BaseHandler): 'network_share_name': data['@network_share_name'], 'network_username': data['@network_username'], 'network_password': data['@network_password'], - 'root_directory': data['target_dir'], + 'root_directory': data.get('target_dir'), # text overlay 'text_overlay': False, @@ -424,11 +490,11 @@ class ConfigHandler(BaseHandler): 'custom_right_text': '', # streaming - 'vudeo_streaming': not data['webcam_localhost'], - 'streaming_port': int(data['webcam_port']), - 'streaming_framerate': int(data['webcam_maxrate']), - 'streaming_quality': int(data['webcam_quality']), - 'streaming_motion': int(data['webcam_motion']), + 'vudeo_streaming': not data.get('webcam_localhost'), + 'streaming_port': int(data.get('webcam_port')), + 'streaming_framerate': int(data.get('webcam_maxrate')), + 'streaming_quality': int(data.get('webcam_quality')), + 'streaming_motion': int(data.get('webcam_motion')), # still images 'still_images': False, @@ -439,19 +505,19 @@ class ConfigHandler(BaseHandler): 'preserve_images': data['@preserve_images'], # motion movies - 'motion_movies': data['motion_movies'], - 'movie_quality': int((max(2, data['ffmpeg_variable_bitrate']) - 2) / 0.29), - 'movie_file_name': data['movie_filename'], + 'motion_movies': data.get('ffmpeg_cap_new'), + 'movie_quality': int((max(2, data.get('ffmpeg_variable_bitrate')) - 2) / 0.29), + 'movie_file_name': data.get('movie_filename'), 'preserve_movies': data['@preserve_movies'], # motion detection 'show_frame_changes': data.get('text_changes') or data.get('locate'), - 'frame_change_threshold': data['threshold'], - 'auto_noise_detect': data['noise_tune'], - 'noise_level': int(int(data['noise_level']) / 2.55), - 'gap': int(data['gap']), - 'pre_capture': int(data['pre_capture']), - 'post_capture': int(data['post_capture']), + 'frame_change_threshold': data.get('threshold'), + 'auto_noise_detect': data.get('noise_tune'), + 'noise_level': int(int(data.get('noise_level')) / 2.55), + 'gap': int(data.get('gap')), + 'pre_capture': int(data.get('pre_capture')), + 'post_capture': int(data.get('post_capture')), # motion notifications 'motion_notifications': data['@motion_notifications'], @@ -468,8 +534,8 @@ class ConfigHandler(BaseHandler): 'sunday_from': '09:00', 'sunday_to': '17:00' } - text_left = data['text_left'] - text_right = data['text_right'] + text_left = data.get('text_left') + text_right = data.get('text_right') if text_left or text_right: ui['text_overlay'] = True diff --git a/src/remote.py b/src/remote.py new file mode 100644 index 0000000..a3ec679 --- /dev/null +++ b/src/remote.py @@ -0,0 +1,110 @@ + +import json +import logging +import urllib2 + + +def _compose_url(host, port, username, password, uri, query=None): + url = '%(scheme)s://%(host)s:%(port)s%(uri)s' % { + 'scheme': 'http', + 'host': host, + 'port': port, + 'uri': uri} + + if query: + url += '?' + '='.join(query.items()) + + return url + + +def list_cameras(host, port, username, password): + logging.debug('listing remote cameras on %(host)s:%(port)s' % { + 'host': host, + 'port': port}) + + url = _compose_url(host, port, username, password, '/config/list/') + request = urllib2.Request(url) + + try: + response = urllib2.urlopen(request) + + except Exception as e: + logging.error('failed to list remote cameras on %(host)s:%(port)s: %(msg)s' % { + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + + try: + response = json.load(response) + + except Exception as e: + logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % { + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + + return response['cameras'] + + +def get_config(host, port, username, password, camera_id): + logging.debug('getting config for remote camera %(id)s on %(host)s:%(port)s' % { + 'id': camera_id, + 'host': host, + 'port': port}) + + url = _compose_url(host, port, username, password, '/config/%(id)s/get/' % {'id': camera_id}) + request = urllib2.Request(url) + + try: + response = urllib2.urlopen(request) + + except Exception as e: + logging.error('failed to get config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + 'id': camera_id, + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + + try: + response = json.load(response) + + except Exception as e: + logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % { + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + + return response + + +def set_config(host, port, username, password, camera_id, data): + logging.debug('setting config for remote camera %(id)s on %(host)s:%(port)s' % { + 'id': camera_id, + 'host': host, + 'port': port}) + + data = json.dumps(data) + + url = _compose_url(host, port, username, password, '/config/%(id)s/set/' % {'id': camera_id}) + request = urllib2.Request(url, data=data) + + try: + urllib2.urlopen(request) + + except Exception as e: + logging.error('failed to set config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % { + 'id': camera_id, + 'host': host, + 'port': port, + 'msg': unicode(e)}) + + raise + diff --git a/static/img/apply-progress.gif b/static/img/apply-progress.gif new file mode 100644 index 0000000..610f733 Binary files /dev/null and b/static/img/apply-progress.gif differ diff --git a/static/img/progress.gif b/static/img/progress.gif deleted file mode 100644 index 610f733..0000000 Binary files a/static/img/progress.gif and /dev/null differ diff --git a/static/img/small-progress.gif b/static/img/small-progress.gif new file mode 100644 index 0000000..e0be29f Binary files /dev/null and b/static/img/small-progress.gif differ diff --git a/static/js/main.js b/static/js/main.js index bea6c3d..989c341 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -19,7 +19,7 @@ function ajax(method, url, data, callback) { } }; - if (data && typeof data === 'object') { + if (data && method === 'POST' && typeof data === 'object') { options['contentType'] = 'application/json'; options['data'] = JSON.stringify(options['data']); } @@ -266,7 +266,11 @@ function initUI() { runConfirmDialog('Remove device ' + deviceName + '?', function () { showProgress(); - ajax('POST', '/config/' + cameraId + '/rem/', null, function () { + ajax('POST', '/config/' + cameraId + '/rem/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + hideApply(); fetchCurrentConfig(); }); @@ -406,8 +410,8 @@ function updateConfigUi() { } }); - /* re-validate all the input validators */ - $('div.settings').find('input.validator').each(function () { + /* re-validate all the validators */ + $('div.settings').find('.validator').each(function () { this.validate(); }); @@ -425,6 +429,11 @@ function updateConfigUi() { } 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) { @@ -659,7 +668,7 @@ function showProgress() { return; /* progress already visible */ } - applyButton.html(''); + applyButton.html(''); applyButton.css('display', 'inline-block'); applyButton.animate({'opacity': '1'}, 100); applyButton.addClass('progress'); @@ -737,9 +746,13 @@ function doApply() { for (var i = 0; i < configs.length; i++) { var config = configs[i]; if (i === configs.length - 1) { - config.config['restart'] = true; + config.config['last'] = true; } - ajax('POST', '/config/' + config.key + '/set/', config.config, function () { + ajax('POST', '/config/' + config.key + '/set/', config.config, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + finishedCount++; testReady(); }); @@ -759,10 +772,18 @@ function doApply() { function fetchCurrentConfig() { /* fetch the main configuration */ ajax('GET', '/config/main/get/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + dict2MainUi(data); /* fetch the camera list */ ajax('GET', '/config/list/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + var i, cameras = data.cameras; var videoDeviceSelect = $('#videoDeviceSelect'); videoDeviceSelect.html(''); @@ -791,6 +812,10 @@ function fetchCurrentCameraConfig() { var cameraId = $('#videoDeviceSelect').val(); if (cameraId != null) { ajax('GET', '/config/' + cameraId + '/get/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + dict2CameraUi(data); }); } @@ -832,7 +857,11 @@ function pushPreview() { 'hue': hue }; - ajax('POST', '/config/' + cameraId + '/set_preview/', data); + ajax('POST', '/config/' + cameraId + '/set_preview/', data, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + }); } @@ -874,6 +903,11 @@ function runAddCameraDialog() { '' + '?' + '' + + '' + + 'Camera' + + '' + + '?' + + '' + ''); /* collect ui widgets */ @@ -882,15 +916,19 @@ function runAddCameraDialog() { var portEntry = content.find('#portEntry'); var usernameEntry = content.find('#usernameEntry'); var passwordEntry = content.find('#passwordEntry'); + var cameraSelect = content.find('#cameraSelect'); /* make validators */ makeTextValidator(hostEntry, true); makeNumberValidator(portEntry, 1, 65535, false, false, true); makeTextValidator(usernameEntry, true); + makeTextValidator(deviceSelect, true); + makeComboValidator(cameraSelect, true); /* ui interaction */ content.find('tr.remote').css('display', 'none'); - var updateUi = function () { + + function updateUi() { if (deviceSelect.val() === 'remote') { content.find('tr.remote').css('display', 'table-row'); } @@ -899,14 +937,88 @@ function runAddCameraDialog() { } updateModalDialogPosition(); - }; + cameraSelect.html(''); + + /* re-validate all the validators */ + content.find('.validator').each(function () { + this.validate(); + }); + + if (uiValid() && deviceSelect.val() == 'remote') { + fetchRemoteCameras(); + } + } - deviceSelect.change(updateUi).change(); + function uiValid(includeCameraSelect) { + /* re-validate all the validators */ + content.find('.validator').each(function () { + this.validate(); + }); + + var valid = true; + var query = content.find('input, select'); + if (!includeCameraSelect) { + query = query.not('#cameraSelect'); + } + query.each(function () { + if (this.invalid) { + valid = false; + return false; + } + }); + + return valid; + } + + function fetchRemoteCameras() { + var progress = $('
'); + + cameraSelect.hide(); + cameraSelect.before(progress); + cameraSelect.parent().find('div').remove(); /* remove any previous progress div */ + + var data = { + host: hostEntry.val(), + port: portEntry.val(), + username: usernameEntry.val(), + password: passwordEntry.val() + }; + + ajax('GET', '/config/list/', data, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + + cameraSelect.html(''); + progress.remove(); + + if (data.error || !data.cameras) { + return; + } + + data.cameras.forEach(function (info) { + cameraSelect.append(''); + }); + + cameraSelect.show(); + }); + } + + deviceSelect.change(updateUi); + hostEntry.change(updateUi); + portEntry.change(updateUi); + usernameEntry.change(updateUi); + passwordEntry.change(updateUi); + updateUi(); showModalDialog(''); /* fetch the available devices */ ajax('GET', '/config/list_devices/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + /* add available devices */ data.devices.forEach(function (device) { if (!device.configured) { @@ -916,23 +1028,39 @@ function runAddCameraDialog() { deviceSelect.append(''); + updateUi(); + runModalDialog({ title: 'Add Camera...', closeButton: true, buttons: 'okcancel', content: content, onOk: function () { - var fullDevice; + if (!uiValid(true)) { + return false; + } + + var data = {}; if (deviceSelect.val() == 'remote') { - fullDevice = 'http://' + usernameEntry.val() + ':' + passwordEntry.val() + '@' + hostEntry + ':' + portEntry; + data.proto = 'http'; + data.host = hostEntry.val(); + data.port = portEntry.val(); + data.username = usernameEntry.val(); + data.password = passwordEntry.val(); + data.remote_camera_id = cameraSelect.val(); } else { - fullDevice = 'v4l2://' + deviceSelect.val(); + data.proto = 'v4l2'; + data.device = deviceSelect.val(); } - + showProgress(); - - ajax('POST', '/config/add/?device=' + fullDevice, null, function (data) { + + ajax('POST', '/config/add/', data, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + hideApply(); var addCameraOption = $('#videoDeviceSelect').find('option[value=add]'); addCameraOption.before(''); @@ -1068,6 +1196,10 @@ function recreateCameraFrames(cameras) { } else { ajax('GET', '/config/list/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + updateCameras(data.cameras); }); } @@ -1081,9 +1213,17 @@ function doCloseCamera(cameraId) { remCameraFrameUi(cameraId); showProgress(); ajax('GET', '/config/' + cameraId + '/get/', null, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + data['enabled'] = false; - data['restart'] = true; - ajax('POST', '/config/' + cameraId + '/set/', data, function () { + data['last'] = true; + ajax('POST', '/config/' + cameraId + '/set/', data, function (data) { + if (data == null || data.error) { + return; // TODO handle error + } + endProgress(); /* if the current camera in the settings panel is the closed camera, diff --git a/static/js/ui.js b/static/js/ui.js index 2b28bf6..1b7ffc6 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -271,6 +271,48 @@ function makeTextValidator($input, required) { $input[0].validate = validate; } +function makeComboValidator($select, required) { + if (required == null) { + required = true; + } + + function isValid(strVal) { + if (!$select.parents('tr:eq(0)').is(':visible')) { + return true; /* an invisible element is considered always valid */ + } + + if (strVal.length === 0 && required) { + return false; + } + + return true; + } + + var msg = 'this field is required'; + + function validate() { + var strVal = $select.val() || ''; + if (isValid(strVal)) { + $select.attr('title', ''); + $select.removeClass('error'); + $select[0].invalid = false; + } + else { + $select.attr('title', msg); + $select.addClass('error'); + $select[0].invalid = true; + } + } + + $select.keyup(validate); + $select.blur(validate); + $select.change(validate).change(); + + $select.addClass('validator'); + $select.addClass('combo-validator'); + $select[0].validate = validate; +} + function makeNumberValidator($input, minVal, maxVal, floating, sign, required) { if (minVal == null) { minVal = -Infinity; @@ -524,7 +566,6 @@ function makeModalDialogButtons(buttonsInfo) { buttonsInfo.forEach(function (info) { var buttonDiv = $('
'); - buttonDiv.click(hideModalDialog); /* every button closes the dialog */ buttonDiv.attr('tabIndex', '0'); /* make button focusable */ buttonDiv.html(info.caption); @@ -533,9 +574,21 @@ function makeModalDialogButtons(buttonsInfo) { } if (info.click) { - buttonDiv.click(info.click); + var oldClick = info.click; + info.click = function () { + if (oldClick() == false) { + return; + } + + hideModalDialog(); + }; + } + else { + info.click = hideModalDialog; /* every button closes the dialog */ } + buttonDiv.click(info.click); + var td = $(''); td.append(buttonDiv); tr.append(td); @@ -650,7 +703,9 @@ function runModalDialog(options) { switch (e.which) { case 13: if (defaultClick) { - defaultClick(); + if (defaultClick() == false) { + return; + }; } /* intentionally no break */