diff --git a/CHANGELOG.md b/CHANGELOG.md index 574d75ff..7e9957e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - `action:pause`: Pauses the current job in OctoPrint - `action:resume`: Resumes the current job in OctoPrint - `action:disconnect`: Disconnects OctoPrint from the printer + Plugins can add supported commands by [hooking](http://docs.octoprint.org/en/devel/plugins/hooks.html) into the + ``octoprint.comm.protocol.action`` hook * Mousing over the webcam image in the control tab enables key control mode, allowing you to quickly move the axis of your printer with your computer's keyboard ([#610](https://github.com/foosel/OctoPrint/pull/610)): - arrow keys: X and Y axes @@ -83,6 +85,14 @@ message for now. * Daemonized OctoPrint now cleans up its pidfile when receiving a TERM signal ([#711](https://github.com/foosel/OctoPrint/issues/711)) * Added serial types for OpenBSD ([#551](https://github.com/foosel/OctoPrint/pull/551)) +* Improved behaviour of terminal: + * Disabling autoscrolling now also stops cutting of the log while it's enabled, effectively preventing log lines from + being modified at all ([#735](https://github.com/foosel/OctoPrint/issues/735)) + * Applying filters displays ``[...]`` where lines where removed + * Added a link to scroll to the end of the terminal log (useful for when autoscroll is disabled) + * Added a link to select all current contents of the terminal log for easy copy-pasting + * Added a display of how many lines are displayed, how many are filtered and how many are available in total +* Frame rate for timelapses can now be configured per timelapse ([#782](https://github.com/foosel/OctoPrint/pull/782)) ### Bug Fixes @@ -126,6 +136,7 @@ * Color code successful or failed print results directly in file list, not just after a reload * Changing Timelapse post roll activates save button * Timelapse post roll is loaded properly from config + * Handling of files on the printer's SD card contained in folders now works correctly ([Commits](https://github.com/foosel/OctoPrint/compare/master...devel)) diff --git a/MANIFEST.in b/MANIFEST.in index 0226858e..82ddf50b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,5 @@ recursive-include src/octoprint/plugins * recursive-include src/octoprint/translations * include versioneer.py include src/octoprint/_version.py +include README.md +include requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 53f8a4c7..9a5f232c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,11 +27,11 @@ year_current = date.today().year # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +#needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain'] +extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain', 'sphinx.ext.autosummary', 'sphinxcontrib.napoleon'] todo_include_todos = True # Add any paths that contain templates here, relative to this directory. @@ -103,9 +103,9 @@ pygments_style = 'sphinx' on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -139,11 +139,9 @@ if not on_rtd: # only import and set the theme if we're building docs locally # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -html_context = { - 'css_files': [ - '_static/theme_overrides.css', # overrides for wide tables in RTD theme - ], -} + +def setup(app): + app.add_stylesheet("theme_overrides.css") # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/docs/development/interface/index.rst b/docs/development/interface/index.rst index 6c4866c2..5b4c28fc 100644 --- a/docs/development/interface/index.rst +++ b/docs/development/interface/index.rst @@ -1,7 +1,7 @@ .. _sec-development-interface: -Interfaces -========== +Internal API +============ .. toctree:: :maxdepth: 3 @@ -9,3 +9,4 @@ Interfaces filemanager.rst plugin.rst printer.rst + util.rst diff --git a/docs/development/interface/plugin.rst b/docs/development/interface/plugin.rst index 1b659481..e1dc2c03 100644 --- a/docs/development/interface/plugin.rst +++ b/docs/development/interface/plugin.rst @@ -1,8 +1,19 @@ .. _sec-development-interface-plugin: -``octoprint.plugin`` --------------------- +octoprint.plugin +---------------- .. automodule:: octoprint.plugin - :members: plugin_manager, plugin_settings, call_plugin, PluginSettings - :undoc-members: + +.. _sec-development-interface-plugin-core: + +octoprint.plugin.core +--------------------- + +.. automodule:: octoprint.plugin.core + +octoprint.plugin.types +---------------------- + +.. automodule:: octoprint.plugin.types + diff --git a/docs/development/interface/util.rst b/docs/development/interface/util.rst new file mode 100644 index 00000000..f855ae25 --- /dev/null +++ b/docs/development/interface/util.rst @@ -0,0 +1,9 @@ +.. _sec-development-interface-util: + +octoprint.util +-------------- + +.. automodule:: octoprint.util + :members: + + diff --git a/docs/plugins/mixins.rst b/docs/plugins/mixins.rst index 083d8bf0..a37e9775 100644 --- a/docs/plugins/mixins.rst +++ b/docs/plugins/mixins.rst @@ -3,7 +3,99 @@ Available plugin mixins ======================= -.. automodule:: octoprint.plugin - :members: AppPlugin, AssetPlugin, BlueprintPlugin, EventHandlerPlugin, ProgressPlugin, SettingsPlugin, ShutdownPlugin, SimpleApiPlugin, SlicerPlugin, StartupPlugin, TemplatePlugin - :undoc-members: +The following plugin mixins are currently available: + +.. contents:: + :local: + +Please note that all plugin mixins inherit from :class:`~octoprint.plugin.core.Plugin` and +:class:`~octoprint.plugin.OctoPrintPlugin`, which also provide attributes of interest to plugin developers. + +.. _sec-plugins-mixins-startupplugin: + +StartupPlugin +------------- + +.. autoclass:: octoprint.plugin.StartupPlugin + :members: + +.. _sec-plugins-mixins-shutdownplugin: + +ShutdownPlugin +-------------- + +.. autoclass:: octoprint.plugin.ShutdownPlugin + :members: + +.. _sec-plugins-mixins-settingsplugin: + +SettingsPlugin +-------------- + +.. autoclass:: octoprint.plugin.SettingsPlugin + :members: + +.. _sec-plugins-mixins-assetplugin: + +AssetPlugin +----------- + +.. autoclass:: octoprint.plugin.AssetPlugin + :members: + +.. _sec-plugins-mixins-templateplugin: + +TemplatePlugin +-------------- + +.. autoclass:: octoprint.plugin.TemplatePlugin + :members: + +.. _sec-plugins-mixins-simpleapiplugin: + +SimpleApiPlugin +--------------- + +.. autoclass:: octoprint.plugin.SimpleApiPlugin + :members: + +.. _sec-plugins-mixins-blueprintplugin: + +BlueprintPlugin +--------------- + +.. autoclass:: octoprint.plugin.BlueprintPlugin + :members: + +.. _sec-plugins-mixins-eventhandlerplugin: + +EventHandlerPlugin +------------------ + +.. autoclass:: octoprint.plugin.EventHandlerPlugin + :members: + +.. _sec-plugins-mixins-progressplugin: + +ProgressPlugin +-------------- + +.. autoclass:: octoprint.plugin.ProgressPlugin + :members: + +.. _sec-plugins-mixins-slicerplugin: + +SlicerPlugin +------------ + +.. autoclass:: octoprint.plugin.SlicerPlugin + :members: + +.. _sec-plugins-mixins-appplugin: + +AppPlugin +--------- + +.. autoclass:: octoprint.plugin.AppPlugin + :members: diff --git a/docs/requirements.txt b/docs/requirements.txt index 28a38e6d..42469fd9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ sphinxcontrib-httpdomain +sphinxcontrib-napoleon sphinx_rtd_theme diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 331446ce..5dc8695f 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -13,7 +13,7 @@ import tempfile import octoprint.filemanager -from octoprint.util import safeRename +from octoprint.util import safe_rename class StorageInterface(object): """ @@ -1003,7 +1003,7 @@ class LocalFileStorage(StorageInterface): with open(metadata_temporary_path, "w") as f: import yaml yaml.safe_dump(metadata, stream=f, default_flow_style=False, indent=" ", allow_unicode=True) - safeRename(metadata_temporary_path, metadata_path, throw_error=True) + safe_rename(metadata_temporary_path, metadata_path, throw_error=True) except: self._logger.exception("Error while writing .metadata.yaml to {path}".format(**locals())) else: diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 90859e1b..377e6229 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -1,4 +1,19 @@ # coding=utf-8 +""" +This module represents OctoPrint's plugin subsystem. This includes management and helper methods as well as the +registered plugin types. + +.. autofunction:: plugin_manager + +.. autofunction:: plugin_settings + +.. autofunction:: call_plugin + +.. autoclass:: PluginSettings + :members: + +""" + from __future__ import absolute_import __author__ = "Gina Häußge " @@ -12,13 +27,47 @@ from octoprint.settings import settings from octoprint.plugin.core import (PluginInfo, PluginManager, Plugin) from octoprint.plugin.types import * +from octoprint.util import deprecated + # singleton _instance = None def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None): + """ + Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager` + singleton. + + Arguments: + init (boolean): A flag indicating whether this is the initial call to construct the singleton (True) or not + (False, default). If this is set to True and the plugin manager has already been initialized, a :class:`ValueError` + will be raised. The same will happen if the plugin manager has not yet been initialized and this is set to + False. + plugin_folders (list): A list of folders (as strings containing the absolute path to them) in which to look for + potential plugin modules. If not provided this defaults to the configured ``plugins`` base folder and + ``src/plugins`` within OctoPrint's code base. + plugin_types (list): A list of recognized plugin types for which to look for provided implementations. If not + provided this defaults to the plugin types found in :mod:`octoprint.plugin.types` without + :class:`~octoprint.plugin.OctoPrintPlugin`. + plugin_entry_points (list): A list of entry points pointing to modules which to load as plugins. If not provided + this defaults to the entry point ``octoprint.plugin``. + plugin_disabled_list (list): A list of plugin identifiers that are currently disabled. If not provided this + defaults to all plugins for which ``enabled`` is set to ``False`` in the settings. + + Returns: + PluginManager: A fully initialized :class:`~octoprint.plugin.core.PluginManager` instance to be used for plugin + management tasks. + + Raises: + ValueError: ``init`` was True although the plugin manager was already initialized, or it was False although + the plugin manager was not yet initialized. + """ + global _instance if _instance is None: if init: + if _instance is not None: + raise ValueError("Plugin Manager already initialized") + if plugin_folders is None: plugin_folders = (settings().getBaseFolder("plugins"), os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins"))) if plugin_types is None: @@ -49,10 +98,59 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en def plugin_settings(plugin_key, defaults=None): + """ + Factory method for creating a :class:`PluginSettings` instance. + + Arguments: + plugin_key (string): The plugin identifier for which to create the settings instance. + defaults (dict): The default settings for the plugin. + + Returns: + PluginSettings: A fully initialized :class:`PluginSettings` instance to be used to access the plugin's + settings + """ return PluginSettings(settings(), plugin_key, defaults=defaults) def call_plugin(types, method, args=None, kwargs=None, callback=None, error_callback=None): + """ + Helper method to invoke the indicated ``method`` on all registered plugin implementations implementing the + indicated ``types``. Allows providing method arguments and registering callbacks to call in case of success + and/or failure of each call which can be used to return individual results to the calling code. + + Example: + + .. sourcecode:: python + + def my_success_callback(name, plugin, result): + print("{name} was called successfully and returned {result!r}".format(**locals())) + + def my_error_callback(name, plugin, exc): + print("{name} raised an exception: {exc!s}".format(**locals())) + + octoprint.plugin.call_plugin( + [octoprint.plugin.StartupPlugin], + "on_startup", + args=(my_host, my_port), + callback=my_success_callback, + error_callback=my_error_callback + ) + + Arguments: + types (list): A list of plugin implementation types to match against. + method (string): Name of the method to call on all matching implementations. + args (tuple): A tuple containing the arguments to supply to the called ``method``. Optional. + kwargs (dict): A dictionary containing the keyword arguments to supply to the called ``method``. Optional. + callback (function): A callback to invoke after an implementation has been called successfully. Will be called + with the three arguments ``name``, ``plugin`` and ``result``. ``name`` will be the plugin identifier, + ``plugin`` the plugin implementation instance itself and ``result`` the result returned from the + ``method`` invocation. + error_callback (function): A callback to invoke after the call of an implementation resulted in an exception. + Will be called with the three arguments ``name``, ``plugin`` and ``exc``. ``name`` will be the plugin + identifier, ``plugin`` the plugin implementation instance itself and ``exc`` the caught exception. + + """ + if not isinstance(types, (list, tuple)): types = [types] if args is None: @@ -67,14 +165,53 @@ def call_plugin(types, method, args=None, kwargs=None, callback=None, error_call result = getattr(plugin, method)(*args, **kwargs) if callback: callback(name, plugin, result) - except Exception as e: + except Exception as exc: logging.getLogger(__name__).exception("Error while calling plugin %s" % name) if error_callback: - error_callback(name, plugin, e) + error_callback(name, plugin, exc) class PluginSettings(object): + """ + The :class:`PluginSettings` class is the interface for plugins to their own or globally defined settings. + + It provides a couple of convenience methods for directly accessing plugin settings via the regular + :class:`octoprint.settings.Settings` interfaces as well as means to access plugin specific folder locations. + + .. method:: get(path, merged=False, asdict=False) + + Retrieves a raw key from the settings for ``path``, optionally merging the raw value with the default settings + if ``merged`` is set to True. + + :param list path: a list of path elements to navigate to the settings value + :param boolean merged: whether to merge the returned result with the default settings (True) or not (False, default) + :return: the retrieved settings value + + .. method:: get_int(path) + + .. method:: get_float(path) + + .. method:: get_boolean(path) + + .. method:: set(path, value, force=False) + + .. method:: set_int(path, value, force=False) + + .. method:: set_float(path, value, force=False) + + .. method:: set_boolean(path, value, force=False) + """ + def __init__(self, settings, plugin_key, defaults=None): + """ + Initializes the object with the provided :class:`octoprint.settings.Settings` manager as ``settings``, using + the ``plugin_key`` and optional ``defaults``. + + :param settings: + :param plugin_key: + :param defaults: + :return: + """ self.settings = settings self.plugin_key = plugin_key @@ -103,45 +240,46 @@ class PluginSettings(object): kwargs.update(dict(defaults=self.defaults)) return kwargs - self.access_methods = { - 'get': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'getInt': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'getFloat': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'getBoolean': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'set': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'setInt': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'setFloat': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), - 'setBoolean': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)) - } + self.access_methods = dict( + get=("get", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + get_int=("getInt", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + get_float=("getFloat", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + get_boolean=("getBoolean", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + set=("set", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + set_int=("setInt", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + set_float=("setFloat", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)), + set_boolean=("setBoolean", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)) + ) + self.deprecated_access_methods = dict(getInt="get_int", getFloat="get_float", getBoolean="get_boolean", setInt="set_int", setFloat="set_float", setBoolean="set_boolean") - def globalGet(self, path): - return self.settings.get(path) + def global_get(self, path, **kwargs): + return self.settings.get(path, **kwargs) - def globalGetInt(self, path): - return self.settings.getInt(path) + def global_get_int(self, path, **kwargs): + return self.settings.getInt(path, **kwargs) - def globalGetFloat(self, path): - return self.settings.getFloat(path) + def global_get_float(self, path, **kwargs): + return self.settings.getFloat(path, **kwargs) - def globalGetBoolean(self, path): - return self.settings.getBoolean(path) + def global_get_boolean(self, path, **kwargs): + return self.settings.getBoolean(path, **kwargs) - def globalSet(self, path, value): - self.settings.set(path, value) + def global_set(self, path, value, **kwargs): + self.settings.set(path, value, **kwargs) - def globalSetInt(self, path, value): - self.settings.setInt(path, value) + def global_set_int(self, path, value, **kwargs): + self.settings.setInt(path, value, **kwargs) - def globalSetFloat(self, path, value): - self.settings.setFloat(path, value) + def global_set_float(self, path, value, **kwargs): + self.settings.setFloat(path, value, **kwargs) - def globalSetBoolean(self, path, value): - self.settings.setBoolean(path, value) + def global_set_boolean(self, path, value, **kwargs): + self.settings.setBoolean(path, value, **kwargs) - def globalGetBaseFolder(self, folder_type): - return self.settings.getBaseFolder(folder_type) + def global_get_basefolder(self, folder_type, **kwargs): + return self.settings.getBaseFolder(folder_type, **kwargs) - def getPluginLogfilePath(self, postfix=None): + def get_plugin_logfile_path(self, postfix=None): filename = "plugin_" + self.plugin_key if postfix is not None: filename += "_" + postfix @@ -149,10 +287,60 @@ class PluginSettings(object): return os.path.join(self.settings.getBaseFolder("logs"), filename) def __getattr__(self, item): - if item in self.access_methods and hasattr(self.settings, item) and callable(getattr(self.settings, item)): - orig_item = getattr(self.settings, item) - args_mapper, kwargs_mapper = self.access_methods[item] + all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys() + if item in all_access_methods: + decorator = None + if item in self.deprecated_access_methods: + new = self.deprecated_access_methods[item] + decorator = deprecated("{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2) + item = new - return lambda *args, **kwargs: orig_item(*args_mapper(args), **kwargs_mapper(kwargs)) - else: - return getattr(self.settings, item) + settings_name, args_mapper, kwargs_mapper = self.access_methods[item] + if hasattr(self.settings, settings_name) and callable(getattr(self.settings, settings_name)): + orig_func = getattr(self.settings, settings_name) + if decorator is not None: + orig_func = decorator(orig_func) + + def _func(*args, **kwargs): + return orig_func(*args_mapper(args), **kwargs_mapper(kwargs)) + _func.__name__ = item + _func.__doc__ = orig_func.__doc__ if "__doc__" in dir(orig_func) else None + + return _func + + return getattr(self.settings, item) + + ##~~ deprecated methods follow + + # TODO: Remove with release of 1.2.0 + + globalGet = deprecated("globalGet has been renamed to global_get", + includedoc="Replaced by :func:`global_get`", + since="1.2.0-dev-546")(global_get) + globalGetInt = deprecated("globalGetInt has been renamed to global_get_int", + includedoc="Replaced by :func:`global_get_int`", + since="1.2.0-dev-546")(global_get_int) + globalGetFloat = deprecated("globalGetFloat has been renamed to global_get_float", + includedoc="Replaced by :func:`global_get_float`", + since="1.2.0-dev-546")(global_get_float) + globalGetBoolean = deprecated("globalGetBoolean has been renamed to global_get_boolean", + includedoc="Replaced by :func:`global_get_boolean`", + since="1.2.0-dev-546")(global_get_boolean) + globalSet = deprecated("globalSet has been renamed to global_set", + includedoc="Replaced by :func:`global_set`", + since="1.2.0-dev-546")(global_set) + globalSetInt = deprecated("globalSetInt has been renamed to global_set_int", + includedoc="Replaced by :func:`global_set_int`", + since="1.2.0-dev-546")(global_set_int) + globalSetFloat = deprecated("globalSetFloat has been renamed to global_set_float", + includedoc="Replaced by :func:`global_set_float`", + since="1.2.0-dev-546")(global_set_float) + globalSetBoolean = deprecated("globalSetBoolean has been renamed to global_set_boolean", + includedoc="Replaced by :func:`global_set_boolean`", + since="1.2.0-dev-546")(global_set_boolean) + globalGetBaseFolder = deprecated("globalGetBaseFolder has been renamed to global_get_basefolder", + includedoc="Replaced by :func:`global_get_basefolder`", + since="1.2.0-dev-546")(global_get_basefolder) + getPluginLogfilePath = deprecated("getPluginLogfilePath has been renamed to get_plugin_logfile_path", + includedoc="Replaced by :func:`get_plugin_logfile_path`", + since="1.2.0-dev-546")(get_plugin_logfile_path) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 6af26056..d418483d 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -1,4 +1,16 @@ # coding=utf-8 +""" +In this module resides the core data structures and logic of the plugin system. It is implemented in an OctoPrint-agnostic +way and could be extracted into a separate Python module in the future. + +.. autoclass:: PluginManager + +.. autoclass:: PluginInfo + +.. autoclass:: Plugin + +""" + from __future__ import absolute_import __author__ = "Gina Häußge " @@ -13,6 +25,15 @@ import logging class PluginInfo(object): + """ + The :class:`PluginInfo` class wraps all available information about a registered plugin. + + This includes its meta data (like name, description, version, etc) as well as the actual plugin extensions like + implementations, hooks and helpers. + + It works on Python module objects and extracts the relevant data from those via accessing the + :ref:`control properties `. + """ attr_name = '__plugin_name__' @@ -125,6 +146,12 @@ class PluginInfo(object): class PluginManager(object): + """ + The :class:`PluginManager` is the central component for finding, loading and accessing plugins provided to the + system. + + It is able to discover plugins both through possible file system locations as well as customizable entry points. + """ def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None): self.logger = logging.getLogger(__name__) @@ -405,5 +432,34 @@ class PluginManager(object): class Plugin(object): + """ + The parent class of all plugin implementations. + + .. attribute:: _identifier + + The identifier of the plugin. Injected by the plugin core system upon initialization of the implementation. + + .. attribute:: _plugin_name + + The name of the plugin. Injected by the plugin core system upon initialization of the implementation. + + .. attribute:: _plugin_version + + The version of the plugin. Injected by the plugin core system upon initialization of the implementation. + + .. attribute:: _basefolder + + The base folder of the plugin. Injected by the plugin core system upon initialization of the implementation. + + .. attribute:: _logger + + The logger instance to use, with the logging name set to the :attr:`PluginManager.logging_prefix` of the + :class:`PluginManager` concatenated with :attr:`_identifier`. Injected by the plugin core system upon + initialization of the implementation. + """ + def initialize(self): + """ + Called by the plugin core after performing all injections. + """ pass diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 48d2b36a..71c9ed39 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -1,4 +1,15 @@ # coding=utf-8 +""" +This module bundles all of OctoPrint's supported plugin implementation types as well as their common parent +class, :class:`OctoPrintPlugin`. + +Please note that the plugin implementation types are documented in the section +:ref:`Available plugin mixins `. + +.. autoclass: OctoPrintPlugin + +""" + from __future__ import absolute_import __author__ = "Gina Häußge " @@ -9,7 +20,29 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms from .core import Plugin -class StartupPlugin(Plugin): +class OctoPrintPlugin(Plugin): + """ + .. attribute:: _plugin_manager + + .. attribute:: _printer_profile_manager + + .. attribute:: _event_bus + + .. attribute:: _analysis_queue + + .. attribute:: _slicing_manager + + .. attribute:: _file_manager + + .. attribute:: _printer + + .. attribute:: _app_session_manager + """ + + pass + + +class StartupPlugin(OctoPrintPlugin): """ The ``StartupPlugin`` allows hooking into the startup of OctoPrint. It can be used to start up additional services on or just after the startup of the server. @@ -37,7 +70,7 @@ class StartupPlugin(Plugin): pass -class ShutdownPlugin(Plugin): +class ShutdownPlugin(OctoPrintPlugin): """ The ``ShutdownPlugin`` allows hooking into the shutdown of OctoPrint. It's usually used in conjunction with the :class:`StartupPlugin` mixin, to cleanly shut down additional services again that where started by the :class:`StartupPlugin` @@ -51,14 +84,14 @@ class ShutdownPlugin(Plugin): pass -class AssetPlugin(Plugin): +class AssetPlugin(OctoPrintPlugin): """ The ``AssetPlugin`` mixin allows plugins to define additional static assets such as Javascript or CSS files to be automatically embedded into the pages delivered by the server to be used within the client sided part of the plugin. A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used by templates injected - through :class:`TemplatePlugin`s. + through a :class:`TemplatePlugin`. """ def get_asset_folder(self): @@ -104,7 +137,7 @@ class AssetPlugin(Plugin): return dict() -class TemplatePlugin(Plugin): +class TemplatePlugin(OctoPrintPlugin): """ Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface. @@ -408,7 +441,7 @@ class TemplatePlugin(Plugin): return os.path.join(self._basefolder, "templates") -class SimpleApiPlugin(Plugin): +class SimpleApiPlugin(OctoPrintPlugin): """ Utilizing the ``SimpleApiPlugin`` mixin plugins may implement a simple API based around one GET resource and one resource accepting JSON commands POSTed to it. This is the easy alternative for plugin's which don't need the @@ -543,7 +576,7 @@ class SimpleApiPlugin(Plugin): return None -class BlueprintPlugin(Plugin): +class BlueprintPlugin(OctoPrintPlugin): """ The ``BlueprintPlugin`` mixin allows plugins to define their own full fledged endpoints for whatever purpose, be it a more sophisticated API than what is possible via the :class:`SimpleApiPlugin` or a custom web frontend. @@ -646,7 +679,7 @@ class BlueprintPlugin(Plugin): return True -class SettingsPlugin(Plugin): +class SettingsPlugin(OctoPrintPlugin): """ Including the ``SettingsPlugin`` mixin allows plugins to store and retrieve their own settings within OctoPrint's configuration. @@ -697,6 +730,11 @@ class SettingsPlugin(Plugin): Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the default implementations do not fit your requirements. + + .. attribute:: _settings + + The :class:`~octoprint.plugin.PluginSettings` instance to use for accessing the plugin's settings. Injected by + the plugin core system upon initialization of the implementation. """ def on_settings_load(self): @@ -751,7 +789,7 @@ class SettingsPlugin(Plugin): return dict() -class EventHandlerPlugin(Plugin): +class EventHandlerPlugin(OctoPrintPlugin): """ The ``EventHandlerPlugin`` mixin allows OctoPrint plugins to react to any of :ref:`OctoPrint's events `. OctoPrint will call the :func:`on_event` method for any event fired on its internal event bus, supplying the @@ -774,7 +812,7 @@ class EventHandlerPlugin(Plugin): pass -class SlicerPlugin(Plugin): +class SlicerPlugin(OctoPrintPlugin): """ Via the ``SlicerPlugin`` mixin plugins can add support for slicing engines to be used by OctoPrint. @@ -835,10 +873,10 @@ class SlicerPlugin(Plugin): ``False`` (defaults to ``True``), an ``IOError`` should be raised. :param string path: the path to which to save the profile - :param :class:`SlicingProfile` profile: the profile to save + :param SlicingProfile profile: the profile to save :param bool allow_overwrite: whether to allow to overwrite an existing profile at the indicated path (``True``, default) - or not (``False``) - if a profile already exists on the path and this is ``False`` - and :class:`IOError` should be raised + or not (``False``) - if a profile already exists on the path and this is ``False`` + and :class:`IOError` should be raised :param dict overrides: profile overrides to apply to the ``profile`` before saving it """ pass @@ -881,7 +919,7 @@ class SlicerPlugin(Plugin): pass -class ProgressPlugin(Plugin): +class ProgressPlugin(OctoPrintPlugin): """ Via the ``ProgressPlugin`` mixing plugins can let themselves be called upon progress in print jobs or slicing jobs, limited to minimally 1% steps. @@ -911,7 +949,7 @@ class ProgressPlugin(Plugin): pass -class AppPlugin(Plugin): +class AppPlugin(OctoPrintPlugin): def get_additional_apps(self): return [] diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index b0509df4..909e4285 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -39,26 +39,26 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, def on_startup(self, host, port): # setup our custom logger - cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.getPluginLogfilePath(postfix="engine"), maxBytes=2*1024*1024) + cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="engine"), maxBytes=2*1024*1024) cura_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) cura_logging_handler.setLevel(logging.DEBUG) self._cura_logger.addHandler(cura_logging_handler) - self._cura_logger.setLevel(logging.DEBUG if self._settings.getBoolean(["debug_logging"]) else logging.CRITICAL) + self._cura_logger.setLevel(logging.DEBUG if self._settings.get_boolean(["debug_logging"]) else logging.CRITICAL) self._cura_logger.propagate = False ##~~ BlueprintPlugin API @octoprint.plugin.BlueprintPlugin.route("/import", methods=["POST"]) - def importCuraProfile(self): + def import_cura_profile(self): import datetime import tempfile from octoprint.server import slicingManager input_name = "file" - input_upload_name = input_name + "." + self._settings.globalGet(["server", "uploads", "nameSuffix"]) - input_upload_path = input_name + "." + self._settings.globalGet(["server", "uploads", "pathSuffix"]) + input_upload_name = input_name + "." + self._settings.global_get(["server", "uploads", "nameSuffix"]) + input_upload_path = input_name + "." + self._settings.global_get(["server", "uploads", "pathSuffix"]) if input_upload_name in flask.request.values and input_upload_path in flask.request.values: filename = flask.request.values[input_upload_name] @@ -92,7 +92,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, # default values for name, display name and description profile_name = _sanitize_name(name) profile_display_name = name - profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.getFormattedDateTime(datetime.datetime.now())) + profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.get_formatted_datetime(datetime.datetime.now())) profile_allow_overwrite = False # overrides @@ -134,11 +134,11 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, ##~~ SettingsPlugin API def on_settings_save(self, data): - old_debug_logging = self._settings.getBoolean(["debug_logging"]) + old_debug_logging = self._settings.get_boolean(["debug_logging"]) super(CuraPlugin, self).on_settings_save(data) - new_debug_logging = self._settings.getBoolean(["debug_logging"]) + new_debug_logging = self._settings.get_boolean(["debug_logging"]) if old_debug_logging != new_debug_logging: if new_debug_logging: self._cura_logger.setLevel(logging.DEBUG) diff --git a/src/octoprint/plugins/cura/templates/cura_settings.jinja2 b/src/octoprint/plugins/cura/templates/cura_settings.jinja2 index 35a27d38..943586ac 100644 --- a/src/octoprint/plugins/cura/templates/cura_settings.jinja2 +++ b/src/octoprint/plugins/cura/templates/cura_settings.jinja2 @@ -66,7 +66,7 @@
{{ _('Browse...') }} - +
diff --git a/src/octoprint/plugins/discovery/__init__.py b/src/octoprint/plugins/discovery/__init__.py index 5a7e1dd6..7516940d 100644 --- a/src/octoprint/plugins/discovery/__init__.py +++ b/src/octoprint/plugins/discovery/__init__.py @@ -467,7 +467,7 @@ class DiscoveryPlugin(octoprint.plugin.StartupPlugin, if self._settings.get(["pathPrefix"]): path = self._settings.get(["pathPrefix"]) else: - prefix = self._settings.globalGet(["server", "reverseProxy", "prefixFallback"]) + prefix = self._settings.global_get(["server", "reverseProxy", "prefixFallback"]) if prefix: path = prefix @@ -676,7 +676,7 @@ class DiscoveryPlugin(octoprint.plugin.StartupPlugin, return upnpUuid def get_instance_name(self): - name = self._settings.globalGet(["appearance", "name"]) + name = self._settings.global_get(["appearance", "name"]) if name: return u"OctoPrint instance \"{}\"".format(name) else: diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index c373a0bb..3ab01b17 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -30,7 +30,7 @@ def getConnectionOptions(): "autoconnect": settings().getBoolean(["serial", "autoconnect"]) } -class Printer(): +class Printer(comm.MachineComPrintCallback): def __init__(self, fileManager, analysisQueue, printerProfileManager): from collections import deque @@ -329,7 +329,7 @@ class Printer(): return self._printAfterSelect = printAfterSelect - self._comm.selectFile(filename, sd) + self._comm.selectFile("/" + filename if sd else filename, sd) self._setProgressData(0, None, None, None) self._setCurrentZ(None) @@ -491,8 +491,14 @@ class Printer(): def _setJobData(self, filename, filesize, sd): if filename is not None: + if sd: + path_in_storage = filename[1:] + path_on_disk = None + else: + path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename) + path_on_disk = self._fileManager.path_on_disk(FileDestinations.LOCAL, filename) self._selectedFile = { - "filename": filename, + "filename": path_in_storage, "filesize": filesize, "sd": sd, "estimatedPrintTime": None @@ -518,14 +524,14 @@ class Printer(): averagePrintTime = None date = None filament = None - if filename: + if path_on_disk: # Use a string for mtime because it could be float and the # javascript needs to exact match if not sd: - date = int(os.stat(filename).st_ctime) + date = int(os.stat(path_on_disk).st_ctime) try: - fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, filename) + fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk) except: fileData = None if fileData is not None: @@ -549,7 +555,7 @@ class Printer(): self._stateMonitor.setJobData({ "file": { - "name": os.path.basename(filename) if filename is not None else None, + "name": path_in_storage, "origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL, "size": filesize, "date": date @@ -691,7 +697,7 @@ class Printer(): def getSdFiles(self): if self._comm is None or not self._comm.isSdReady(): return [] - return self._comm.getSdFiles() + return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles()) def addSdFile(self, filename, absolutePath, streamingFinishedCallback): if not self._comm or self._comm.isBusy() or not self._comm.isSdReady(): @@ -703,16 +709,16 @@ class Printer(): self.refreshSdFiles(blocking=True) existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles()) - remoteName = util.getDosFilename(filename, existingSdFiles) + remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco") self._timeEstimationData = TimeEstimationHelper() - self._comm.startFileTransfer(absolutePath, filename, remoteName) + self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName) return remoteName def deleteSdFile(self, filename): if not self._comm or not self._comm.isSdReady(): return - self._comm.deleteSdFile(filename) + self._comm.deleteSdFile("/" + filename) def initSdCard(self): if not self._comm or self._comm.isSdReady(): @@ -728,7 +734,7 @@ class Printer(): """ Refreshs the list of file stored on the SD card attached to printer (if available and printer communication available). Optional blocking parameter allows making the method block (max 10s) until the file list has been - received (and can be accessed via self._comm.getSdFiles()). Defaults to a asynchronous operation. + received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation. """ if not self._comm or not self._comm.isSdReady(): return diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index dc59063d..981cafcb 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -534,23 +534,33 @@ class Server(): printer = Printer(fileManager, analysisQueue, printerProfileManager) appSessionManager = util.flask.AppSessionManager() - def plugin_settings_factory(name, implementation): + def octoprint_plugin_inject_factory(name, implementation): + if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin): + return None + return dict( + plugin_manager=pluginManager, + printer_profile_manager=printerProfileManager, + event_bus=eventManager, + analysis_queue=analysisQueue, + slicing_manager=slicingManager, + file_manager=fileManager, + printer=printer, + app_session_manager=appSessionManager + ) + + def settings_plugin_inject_factory(name, implementation): if not isinstance(implementation, octoprint.plugin.SettingsPlugin): return None default_settings = implementation.get_settings_defaults() plugin_settings = octoprint.plugin.plugin_settings(name, defaults=default_settings) return dict(settings=plugin_settings) - pluginManager.initialize_implementations(additional_injects=dict( - plugin_manager=pluginManager, - printer_profile_manager=printerProfileManager, - event_bus=eventManager, - analysis_queue=analysisQueue, - slicing_manager=slicingManager, - file_manager=fileManager, - printer=printer, - app_session_manager=appSessionManager - ), additional_inject_factories=[plugin_settings_factory]) + pluginManager.initialize_implementations( + additional_inject_factories=[ + octoprint_plugin_inject_factory, + settings_plugin_inject_factory + ] + ) slicingManager.initialize() # configure additional template folders for jinja2 @@ -581,7 +591,7 @@ class Server(): if settings().getBoolean(["accessControl", "enabled"]): userManagerName = settings().get(["accessControl", "userManager"]) try: - clazz = octoprint.util.getClass(userManagerName) + clazz = octoprint.util.get_class(userManagerName) userManager = clazz() except AttributeError, e: logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) @@ -784,6 +794,10 @@ class Server(): config = octoprint.util.dict_merge(defaultConfig, configFromFile) logging.config.dictConfig(config) + logging.captureWarnings(True) + + import warnings + warnings.simplefilter("always") if settings().getBoolean(["serial", "log"]): # enable debug logging to serial.log diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index a4a4ece4..5c4a1fc9 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -20,7 +20,7 @@ import octoprint.plugin from octoprint.server import admin_permission, NO_CONTENT from octoprint.settings import settings as s, valid_boolean_trues from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler -from octoprint.server.util.flask import restricted_access +from octoprint.server.util.flask import restricted_access, get_remote_address, get_json_command_from_request #~~ init api blueprint, including sub modules @@ -73,7 +73,7 @@ def pluginCommand(name): if valid_commands is None: return make_response("Method not allowed", 405) - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response @@ -204,7 +204,7 @@ def login(): localNetworks.add(ip) try: - remoteAddr = util.getRemoteAddress(request) + remoteAddr = get_remote_address(request) if netaddr.IPAddress(remoteAddr) in localNetworks: user = octoprint.server.userManager.findUser(autologinAs) if user is not None: diff --git a/src/octoprint/server/api/connection.py b/src/octoprint/server/api/connection.py index b562062d..911ffe4b 100644 --- a/src/octoprint/server/api/connection.py +++ b/src/octoprint/server/api/connection.py @@ -11,7 +11,7 @@ from octoprint.settings import settings from octoprint.printer import getConnectionOptions from octoprint.server import printer, printerProfileManager, NO_CONTENT from octoprint.server.api import api -from octoprint.server.util.flask import restricted_access +from octoprint.server.util.flask import restricted_access, get_json_command_from_request import octoprint.util as util @@ -36,7 +36,7 @@ def connectionCommand(): "disconnect": [] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index b2e1cc24..bb14b9ed 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -11,7 +11,7 @@ import octoprint.util as util from octoprint.filemanager.destinations import FileDestinations from octoprint.settings import settings, valid_boolean_trues from octoprint.server import printer, fileManager, slicingManager, eventManager, NO_CONTENT -from octoprint.server.util.flask import restricted_access +from octoprint.server.util.flask import restricted_access, get_json_command_from_request from octoprint.server.api import api from octoprint.events import Events import octoprint.filemanager @@ -27,7 +27,7 @@ def readGcodeFiles(): filter = request.values["filter"] files = _getFileList(FileDestinations.LOCAL, filter=filter) files.extend(_getFileList(FileDestinations.SDCARD)) - return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads"))) + return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads"))) @api.route("/files/", methods=["GET"]) @@ -38,7 +38,7 @@ def readGcodeFilesForOrigin(origin): files = _getFileList(origin) if origin == FileDestinations.LOCAL: - return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads"))) + return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads"))) else: return jsonify(files=files) @@ -276,7 +276,7 @@ def gcodeFileCommand(filename, target): "slice": [] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response diff --git a/src/octoprint/server/api/job.py b/src/octoprint/server/api/job.py index 84eacd8a..ae57039f 100644 --- a/src/octoprint/server/api/job.py +++ b/src/octoprint/server/api/job.py @@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms from flask import request, make_response, jsonify from octoprint.server import printer, NO_CONTENT -from octoprint.server.util.flask import restricted_access +from octoprint.server.util.flask import restricted_access, get_json_command_from_request from octoprint.server.api import api import octoprint.util as util @@ -26,7 +26,7 @@ def controlJob(): "cancel": [] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py index b1131b32..ccfeb01a 100644 --- a/src/octoprint/server/api/log.py +++ b/src/octoprint/server/api/log.py @@ -15,7 +15,7 @@ from octoprint.settings import settings from octoprint.server import NO_CONTENT, admin_permission from octoprint.server.util.flask import redirect_to_tornado, restricted_access from octoprint.server.api import api -from octoprint.util import getFreeBytes +from octoprint.util import get_free_bytes @api.route("/logs", methods=["GET"]) @@ -23,7 +23,7 @@ from octoprint.util import getFreeBytes @admin_permission.require(403) def getLogFiles(): files = _getLogFiles() - return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs"))) + return jsonify(files=files, free=get_free_bytes(settings().getBaseFolder("logs"))) @api.route("/logs/", methods=["GET"]) diff --git a/src/octoprint/server/api/printer.py b/src/octoprint/server/api/printer.py index 4fdd1ef1..20e22b32 100644 --- a/src/octoprint/server/api/printer.py +++ b/src/octoprint/server/api/printer.py @@ -12,7 +12,7 @@ import re from octoprint.settings import settings, valid_boolean_trues from octoprint.server import printer, NO_CONTENT from octoprint.server.api import api -from octoprint.server.util.flask import restricted_access +from octoprint.server.util.flask import restricted_access, get_json_command_from_request import octoprint.util as util #~~ Printer @@ -64,7 +64,7 @@ def printerToolCommand(): "extrude": ["amount"], "flowrate": ["factor"] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response @@ -166,7 +166,7 @@ def printerBedCommand(): "target": ["target"], "offset": ["offset"] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response @@ -232,7 +232,7 @@ def printerPrintheadCommand(): "home": ["axes"], "feedrate": ["factor"] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response @@ -293,7 +293,7 @@ def printerSdCommand(): "refresh": [], "release": [] } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + command, data, response = get_json_command_from_request(request, valid_commands) if response is not None: return response diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 79da21f3..44720adb 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -48,6 +48,7 @@ def getSettings(): "snapshotUrl": s.get(["webcam", "snapshot"]), "ffmpegPath": s.get(["webcam", "ffmpeg"]), "bitrate": s.get(["webcam", "bitrate"]), + "ffmpegThreads": s.get(["webcam", "ffmpegThreads"]), "watermark": s.getBoolean(["webcam", "watermark"]), "flipH": s.getBoolean(["webcam", "flipH"]), "flipV": s.getBoolean(["webcam", "flipV"]) @@ -139,6 +140,7 @@ def setSettings(): if "snapshotUrl" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"]) if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"]) if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"]) + if "ffmpegThreads" in data["webcam"].keys(): s.setInt(["webcam", "ffmpegThreads"], data["webcam"]["ffmpegThreads"]) if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"]) if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"]) if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"]) diff --git a/src/octoprint/server/api/timelapse.py b/src/octoprint/server/api/timelapse.py index f3c7a2c1..bef85536 100644 --- a/src/octoprint/server/api/timelapse.py +++ b/src/octoprint/server/api/timelapse.py @@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import os -from flask import request, jsonify, url_for +from flask import request, jsonify, url_for, make_response from werkzeug.utils import secure_filename import octoprint.timelapse @@ -30,9 +30,11 @@ def getTimelapseData(): if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse): config["type"] = "zchange" config["postRoll"] = timelapse.postRoll() + config["fps"] = timelapse.fps() elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse): config["type"] = "timed" config["postRoll"] = timelapse.postRoll() + config["fps"] = timelapse.fps() config.update({ "interval": timelapse.interval() }) @@ -55,7 +57,7 @@ def downloadTimelapse(filename): @api.route("/timelapse/", methods=["DELETE"]) @restricted_access def deleteTimelapse(filename): - if util.isAllowedFile(filename, {"mpg"}): + if util.is_allowed_file(filename, {"mpg"}): timelapse_folder = settings().getBaseFolder("timelapse") full_path = os.path.realpath(os.path.join(timelapse_folder, filename)) if full_path.startswith(timelapse_folder) and os.path.exists(full_path): @@ -70,26 +72,46 @@ def setTimelapseConfig(): config = { "type": request.values["type"], "postRoll": 0, + "fps": 25, "options": {} } - if "postRoll" in request.values: try: - config["postRoll"] = int(request.values["postRoll"]) + postRoll = int(request.values["postRoll"]) except ValueError: - pass + return make_response("Invalid value for postRoll: %r" % request.values["postRoll"], 400) + else: + if postRoll >= 0: + config["postRoll"] = postRoll + else: + return make_response("Invalid value for postRoll: %d" % postRoll, 400) + + if "fps" in request.values: + try: + fps = int(request.values["fps"]) + except ValueError: + return make_response("Invalid value for fps: %r" % request.values["fps"], 400) + else: + if fps > 0: + config["fps"] = fps + else: + return make_response("Invalid value for fps: %d" % fps, 400) if "interval" in request.values: - interval = 10 + config["options"] = { + "interval": 10 + } + try: interval = int(request.values["interval"]) except ValueError: - pass - - config["options"] = { - "interval": interval - } + return make_response("Invalid value for interval: %r" % request.values["interval"]) + else: + if interval > 0: + config["options"]["interval"] = interval + else: + return make_response("Invalid value for interval: %d" % interval) if admin_permission.can() and "save" in request.values and request.values["save"] in valid_boolean_trues: octoprint.timelapse.configureTimelapse(config, True) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 3f6408ec..c52f4174 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -1,5 +1,6 @@ # coding=utf-8 from __future__ import absolute_import +from flask import make_response __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' @@ -245,3 +246,25 @@ class AppSessionManager(object): self._logger.debug("App sessions after cleanup: %r" % self._sessions) + +def get_remote_address(request): + forwardedFor = request.headers.get("X-Forwarded-For", None) + if forwardedFor is not None: + return forwardedFor.split(",")[0] + return request.remote_addr + + +def get_json_command_from_request(request, valid_commands): + if not "application/json" in request.headers["Content-Type"]: + return None, None, make_response("Expected content-type JSON", 400) + + data = request.json + if not "command" in data.keys() or not data["command"] in valid_commands.keys(): + return None, None, make_response("Expected valid command", 400) + + command = data["command"] + for parameter in valid_commands[command]: + if not parameter in data: + return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400) + + return command, data, None \ No newline at end of file diff --git a/src/octoprint/server/util/tornado.py b/src/octoprint/server/util/tornado.py index bc070121..18e7281a 100644 --- a/src/octoprint/server/util/tornado.py +++ b/src/octoprint/server/util/tornado.py @@ -373,7 +373,7 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler): finally: # make sure the temporary files are removed again for f in self._files: - octoprint.util.silentRemove(f) + octoprint.util.silent_remove(f) # make all http methods trigger _handle_method get = _handle_method diff --git a/src/octoprint/server/util/watchdog.py b/src/octoprint/server/util/watchdog.py index 4b40ba30..4d4641e2 100644 --- a/src/octoprint/server/util/watchdog.py +++ b/src/octoprint/server/util/watchdog.py @@ -36,7 +36,7 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler): self.filename = os.path.basename(self._path) def save(self, target): - octoprint.util.safeRename(self._path, target) + octoprint.util.safe_rename(self._path, target) file_wrapper = WatchdogFileWrapper(path) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 8d45976a..8ed1bc6a 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -61,6 +61,7 @@ default_settings = { "stream": None, "snapshot": None, "ffmpeg": None, + "ffmpegThreads": 1, "bitrate": "5000k", "watermark": True, "flipH": False, @@ -68,7 +69,8 @@ default_settings = { "timelapse": { "type": "off", "options": {}, - "postRoll": 0 + "postRoll": 0, + "fps": 25 } }, "gcodeViewer": { diff --git a/src/octoprint/static/css/octoprint.css b/src/octoprint/static/css/octoprint.css index 3c506d64..1e6eebbf 100644 --- a/src/octoprint/static/css/octoprint.css +++ b/src/octoprint/static/css/octoprint.css @@ -1 +1 @@ -.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;margin-bottom:0;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{border-color:transparent;cursor:pointer;color:#08c;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.nowrap{white-space:nowrap}.actioncol{text-align:center;white-space:nowrap}.actioncol a{text-decoration:none;color:#000}.actioncol a.disabled{color:#ccc;cursor:default}#navbar .navbar-inner{background-color:#ebebeb;background-image:-moz-linear-gradient(top,#fff,#ccc);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ccc));background-image:-webkit-linear-gradient(top,#fff,#ccc);background-image:-o-linear-gradient(top,#fff,#ccc);background-image:linear-gradient(to bottom,#fff,#ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffcccccc',GradientType=0)}#navbar .navbar-inner .brand,#navbar .navbar-inner .nav>li>a{text-shadow:0 1px 0 #fff;color:#333}#navbar .navbar-inner .brand .caret,#navbar .navbar-inner .nav>li>a .caret{border-bottom-color:#939393;border-top-color:#939393}#navbar .navbar-inner .brand:hover .caret,#navbar .navbar-inner .nav>li>a:hover .caret,#navbar .navbar-inner .brand:focus .caret,#navbar .navbar-inner .nav>li>a:focus .caret{border-bottom-color:#636363;border-top-color:#636363}#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e0e0e0;background-image:-moz-linear-gradient(top,#ccc,#fff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ccc),to(#fff));background-image:-webkit-linear-gradient(top,#ccc,#fff);background-image:-o-linear-gradient(top,#ccc,#fff);background-image:linear-gradient(to bottom,#ccc,#fff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffcccccc',endColorstr='#ffffffff',GradientType=0)}#navbar .navbar-inner.red{background-color:#e36565;background-image:-moz-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9a0a0),to(#c20c0c));background-image:-webkit-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-o-linear-gradient(top,#f9a0a0,#c20c0c);background-image:linear-gradient(to bottom,#f9a0a0,#c20c0c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9a0a0',endColorstr='#ffc20c0c',GradientType=0)}#navbar .navbar-inner.red .brand,#navbar .navbar-inner.red .nav>li>a{text-shadow:0 1px 0 #c20c0c;color:#f2f2f2}#navbar .navbar-inner.red .brand .caret,#navbar .navbar-inner.red .nav>li>a .caret{border-bottom-color:#f28d8d;border-top-color:#f28d8d}#navbar .navbar-inner.red .brand:hover .caret,#navbar .navbar-inner.red .nav>li>a:hover .caret,#navbar .navbar-inner.red .brand:focus .caret,#navbar .navbar-inner.red .nav>li>a:focus .caret{border-bottom-color:#f2c0c0;border-top-color:#f2c0c0}#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.red .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d84747;background-image:-moz-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c20c0c),to(#f9a0a0));background-image:-webkit-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-o-linear-gradient(top,#c20c0c,#f9a0a0);background-image:linear-gradient(to bottom,#c20c0c,#f9a0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc20c0c',endColorstr='#fff9a0a0',GradientType=0)}#navbar .navbar-inner.orange{background-color:#e39665;background-image:-moz-linear-gradient(top,#f9c3a0,#c2530c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9c3a0),to(#c2530c));background-image:-webkit-linear-gradient(top,#f9c3a0,#c2530c);background-image:-o-linear-gradient(top,#f9c3a0,#c2530c);background-image:linear-gradient(to bottom,#f9c3a0,#c2530c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9c3a0',endColorstr='#ffc2530c',GradientType=0)}#navbar .navbar-inner.orange .brand,#navbar .navbar-inner.orange .nav>li>a{text-shadow:0 1px 0 #f6a570;color:#333}#navbar .navbar-inner.orange .brand .caret,#navbar .navbar-inner.orange .nav>li>a .caret{border-bottom-color:#93552e;border-top-color:#93552e}#navbar .navbar-inner.orange .brand:hover .caret,#navbar .navbar-inner.orange .nav>li>a:hover .caret,#navbar .navbar-inner.orange .brand:focus .caret,#navbar .navbar-inner.orange .nav>li>a:focus .caret{border-bottom-color:#634430;border-top-color:#634430}#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.orange .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d88047;background-image:-moz-linear-gradient(top,#c2530c,#f9c3a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2530c),to(#f9c3a0));background-image:-webkit-linear-gradient(top,#c2530c,#f9c3a0);background-image:-o-linear-gradient(top,#c2530c,#f9c3a0);background-image:linear-gradient(to bottom,#c2530c,#f9c3a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2530c',endColorstr='#fff9c3a0',GradientType=0)}#navbar .navbar-inner.yellow{background-color:#e3d765;background-image:-moz-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9f0a0),to(#c2b00c));background-image:-webkit-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-o-linear-gradient(top,#f9f0a0,#c2b00c);background-image:linear-gradient(to bottom,#f9f0a0,#c2b00c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f0a0',endColorstr='#ffc2b00c',GradientType=0)}#navbar .navbar-inner.yellow .brand,#navbar .navbar-inner.yellow .nav>li>a{text-shadow:0 1px 0 #f6e970;color:#333}#navbar .navbar-inner.yellow .brand .caret,#navbar .navbar-inner.yellow .nav>li>a .caret{border-bottom-color:#93892e;border-top-color:#93892e}#navbar .navbar-inner.yellow .brand:hover .caret,#navbar .navbar-inner.yellow .nav>li>a:hover .caret,#navbar .navbar-inner.yellow .brand:focus .caret,#navbar .navbar-inner.yellow .nav>li>a:focus .caret{border-bottom-color:#635e30;border-top-color:#635e30}#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.yellow .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d8ca47;background-image:-moz-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2b00c),to(#f9f0a0));background-image:-webkit-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-o-linear-gradient(top,#c2b00c,#f9f0a0);background-image:linear-gradient(to bottom,#c2b00c,#f9f0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2b00c',endColorstr='#fff9f0a0',GradientType=0)}#navbar .navbar-inner.green{background-color:#98f064;background-image:-moz-linear-gradient(top,#c8ffa7,#50da00);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8ffa7),to(#50da00));background-image:-webkit-linear-gradient(top,#c8ffa7,#50da00);background-image:-o-linear-gradient(top,#c8ffa7,#50da00);background-image:linear-gradient(to bottom,#c8ffa7,#50da00);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8ffa7',endColorstr='#ff50da00',GradientType=0)}#navbar .navbar-inner.green .brand,#navbar .navbar-inner.green .nav>li>a{text-shadow:0 1px 0 #a7ff74;color:#333}#navbar .navbar-inner.green .brand .caret,#navbar .navbar-inner.green .nav>li>a .caret{border-bottom-color:#55992e;border-top-color:#55992e}#navbar .navbar-inner.green .brand:hover .caret,#navbar .navbar-inner.green .nav>li>a:hover .caret,#navbar .navbar-inner.green .brand:focus .caret,#navbar .navbar-inner.green .nav>li>a:focus .caret{border-bottom-color:#446630;border-top-color:#446630}#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.green .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.open.active>.dropdown-toggle{background-color:#80e943;background-image:-moz-linear-gradient(top,#50da00,#c8ffa7);background-image:-webkit-gradient(linear,0 0,0 100%,from(#50da00),to(#c8ffa7));background-image:-webkit-linear-gradient(top,#50da00,#c8ffa7);background-image:-o-linear-gradient(top,#50da00,#c8ffa7);background-image:linear-gradient(to bottom,#50da00,#c8ffa7);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff50da00',endColorstr='#ffc8ffa7',GradientType=0)}#navbar .navbar-inner.blue{background-color:#6498f0;background-image:-moz-linear-gradient(top,#a7c8ff,#0050da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#a7c8ff),to(#0050da));background-image:-webkit-linear-gradient(top,#a7c8ff,#0050da);background-image:-o-linear-gradient(top,#a7c8ff,#0050da);background-image:linear-gradient(to bottom,#a7c8ff,#0050da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa7c8ff',endColorstr='#ff0050da',GradientType=0)}#navbar .navbar-inner.blue .brand,#navbar .navbar-inner.blue .nav>li>a{text-shadow:0 1px 0 #74a7ff;color:#333}#navbar .navbar-inner.blue .brand .caret,#navbar .navbar-inner.blue .nav>li>a .caret{border-bottom-color:#2e5599;border-top-color:#2e5599}#navbar .navbar-inner.blue .brand:hover .caret,#navbar .navbar-inner.blue .nav>li>a:hover .caret,#navbar .navbar-inner.blue .brand:focus .caret,#navbar .navbar-inner.blue .nav>li>a:focus .caret{border-bottom-color:#304466;border-top-color:#304466}#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.blue .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.open.active>.dropdown-toggle{background-color:#4380e9;background-image:-moz-linear-gradient(top,#0050da,#a7c8ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0050da),to(#a7c8ff));background-image:-webkit-linear-gradient(top,#0050da,#a7c8ff);background-image:-o-linear-gradient(top,#0050da,#a7c8ff);background-image:linear-gradient(to bottom,#0050da,#a7c8ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0050da',endColorstr='#ffa7c8ff',GradientType=0)}#navbar .navbar-inner.violet{background-color:#9864f0;background-image:-moz-linear-gradient(top,#c8a7ff,#5000da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8a7ff),to(#5000da));background-image:-webkit-linear-gradient(top,#c8a7ff,#5000da);background-image:-o-linear-gradient(top,#c8a7ff,#5000da);background-image:linear-gradient(to bottom,#c8a7ff,#5000da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8a7ff',endColorstr='#ff5000da',GradientType=0)}#navbar .navbar-inner.violet .brand,#navbar .navbar-inner.violet .nav>li>a{text-shadow:0 1px 0 #5000da;color:#f2f2f2}#navbar .navbar-inner.violet .brand .caret,#navbar .navbar-inner.violet .nav>li>a .caret{border-bottom-color:#b58df9;border-top-color:#b58df9}#navbar .navbar-inner.violet .brand:hover .caret,#navbar .navbar-inner.violet .nav>li>a:hover .caret,#navbar .navbar-inner.violet .brand:focus .caret,#navbar .navbar-inner.violet .nav>li>a:focus .caret{border-bottom-color:#d3c0f5;border-top-color:#d3c0f5}#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.violet .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.open.active>.dropdown-toggle{background-color:#8043e9;background-image:-moz-linear-gradient(top,#5000da,#c8a7ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5000da),to(#c8a7ff));background-image:-webkit-linear-gradient(top,#5000da,#c8a7ff);background-image:-o-linear-gradient(top,#5000da,#c8a7ff);background-image:linear-gradient(to bottom,#5000da,#c8a7ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5000da',endColorstr='#ffc8a7ff',GradientType=0)}#navbar .navbar-inner.black{background-color:#4f4f4f;background-image:-moz-linear-gradient(top,#787878,#121212);background-image:-webkit-gradient(linear,0 0,0 100%,from(#787878),to(#121212));background-image:-webkit-linear-gradient(top,#787878,#121212);background-image:-o-linear-gradient(top,#787878,#121212);background-image:linear-gradient(to bottom,#787878,#121212);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff787878',endColorstr='#ff121212',GradientType=0)}#navbar .navbar-inner.black .brand,#navbar .navbar-inner.black .nav>li>a{text-shadow:0 1px 0 #121212;color:#f2f2f2}#navbar .navbar-inner.black .brand .caret,#navbar .navbar-inner.black .nav>li>a .caret{border-bottom-color:#959595;border-top-color:#959595}#navbar .navbar-inner.black .brand:hover .caret,#navbar .navbar-inner.black .nav>li>a:hover .caret,#navbar .navbar-inner.black .brand:focus .caret,#navbar .navbar-inner.black .nav>li>a:focus .caret{border-bottom-color:#c4c4c4;border-top-color:#c4c4c4}#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.black .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.open.active>.dropdown-toggle{background-color:#3b3b3b;background-image:-moz-linear-gradient(top,#121212,#787878);background-image:-webkit-gradient(linear,0 0,0 100%,from(#121212),to(#787878));background-image:-webkit-linear-gradient(top,#121212,#787878);background-image:-o-linear-gradient(top,#121212,#787878);background-image:linear-gradient(to bottom,#121212,#787878);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff121212',endColorstr='#ff787878',GradientType=0)}#navbar .navbar-inner .brand span{background-size:20px 20px;background-position:left center;padding-left:24px;background-repeat:no-repeat}.octoprint-container{margin-top:20px}.octoprint-container .tab-content{padding:9px 15px;border-left:1px solid #DDD;border-right:1px solid #DDD;border-bottom:1px solid #DDD;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.octoprint-container .nav{margin-bottom:0}.octoprint-container .tab-content h1{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5;font-weight:normal}.octoprint-container .accordion-heading .accordion-heading-button{float:right}.octoprint-container .accordion-heading .accordion-heading-button a{display:inline-block;padding:8px 15px;font-size:14px;line-height:20px;color:#000;text-decoration:none;background:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.octoprint-container .accordion-heading a.accordion-toggle{display:inline-block}.octoprint-container .accordion-heading [class^="icon-"],.octoprint-container .accordion-heading [class*=" icon-"]{color:#000}.print-control .btn{padding-left:4px;padding-right:4px}.upload-buttons .btn{margin-right:0}table{table-layout:fixed}table .popover-title{text-overflow:ellipsis;word-break:break-all}table th,table td{overflow:hidden}table th.gcode_files_name,table td.gcode_files_name{text-overflow:ellipsis;text-align:left;white-space:nowrap}table th.gcode_files_action,table td.gcode_files_action{width:90px;text-align:center;white-space:nowrap}table th.gcode_files_action a,table td.gcode_files_action a{text-decoration:none;color:#000}table th.gcode_files_action a.disabled,table td.gcode_files_action a.disabled{color:#ccc;cursor:default}table th.timelapse_files_name,table td.timelapse_files_name{text-overflow:ellipsis;text-align:left}table th.timelapse_files_size,table td.timelapse_files_size{text-align:right;width:55px}table th.timelapse_files_action,table td.timelapse_files_action{width:45px;text-align:center;white-space:nowrap}table th.timelapse_files_action a,table td.timelapse_files_action a{text-decoration:none;color:#000}table th.timelapse_files_action a.disabled,table td.timelapse_files_action a.disabled{color:#ccc;cursor:default}table th.settings_users_name,table td.settings_users_name{text-overflow:ellipsis;text-align:left}table th.settings_users_active,table td.settings_users_active,table th.settings_users_admin,table td.settings_users_admin{text-align:center;width:55px}table th.settings_users_actions,table td.settings_users_actions{width:60px;text-align:center;white-space:nowrap}table th.settings_users_actions a,table td.settings_users_actions a{text-decoration:none;color:#000}table th.settings_users_actions a.disabled,table td.settings_users_actions a.disabled{color:#ccc;cursor:default}table th.settings_logs_name,table td.settings_logs_name{text-overflow:ellipsis;text-align:left}table th.settings_logs_size,table td.settings_logs_size{text-align:right;width:70px}table th.settings_logs_date,table td.settings_logs_date{text-align:left;width:130px}table th.settings_logs_action,table td.settings_logs_action{width:70px;text-align:center;white-space:nowrap}table th.settings_logs_action a,table td.settings_logs_action a{text-decoration:none;color:#000}table th.settings_logs_action a.disabled,table td.settings_logs_action a.disabled{color:#ccc;cursor:default}table th.settings_printerProfiles_profiles_name,table td.settings_printerProfiles_profiles_name{text-overflow:ellipsis;text-align:left}table th.settings_printerProfiles_profiles_model,table td.settings_printerProfiles_profiles_model{text-align:left;width:250px}table th.settings_printerProfiles_profiles_action,table td.settings_printerProfiles_profiles_action{width:80px;text-align:center;white-space:nowrap}table th.settings_printerProfiles_profiles_action a,table td.settings_printerProfiles_profiles_action a{text-decoration:none;color:#000}table th.settings_printerProfiles_profiles_action a.disabled,table td.settings_printerProfiles_profiles_action a.disabled{color:#ccc;cursor:default}#temperature-graph{height:350px;width:100%;background-image:url("../img/graph-background.png");background-position:center;background-repeat:no-repeat}.tab-content,.tab-pane{overflow:visible}.tempInput{width:50px}#temp_newTemp,#temp_newBedTemp,#speed_innerWall,#speed_outerWall,#speed_fill,#speed_support,#webcam_timelapse_interval,#webcam_timelapse_postRoll{text-align:right}ul.dropdown-menu li a{cursor:pointer}#connection_ports,#connection_baudrates,#connection_printers{width:100%}#offline_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#offline_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#offline_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#offline_overlay_wrapper .container{margin:auto}#webcam_container{width:100%;position:relative;outline:0}#webcam_container .flipH{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1)}#webcam_container .flipV{-webkit-transform:scaleY(-1);-moz-transform:scaleY(-1)}#webcam_container .flipH.flipV{-webkit-transform:scaleX(-1) scaleY(-1);-moz-transform:scaleX(-1) scaleY(-1)}#webcam_container .keycontrol_overlay{position:absolute;left:10px;right:10px;bottom:10px;background:rgba(0,0,0,0.5);font-size:85%;color:white;padding:0}#webcam_container .keycontrol_overlay kbd{border:1px solid #eee;border-radius:3px;margin-left:2px;margin-right:2px;font-size:90%;padding:2px;min-width:1em}#webcam_container .keycontrol_overlay .keycontrol_overlay_heading{position:relative;padding:10px;font-weight:bold}#webcam_container .keycontrol_overlay .keycontrol_overlay_column{position:relative;width:45%;padding:10px;float:left}#files .gcode_files{padding-right:7px}#files .gcode_files .entry{padding:5px;line-height:20px;border-bottom:1px solid #ddd;position:relative}#files .gcode_files .entry:hover{background-color:#f5f5f5}#files .gcode_files .entry .title{text-overflow:ellipsis;word-break:break-all}#files .gcode_files .entry .uploaded,#files .gcode_files .entry .size,#files .gcode_files .entry .additionalInfo{font-size:85%;color:#999}#files .gcode_files .entry .action-buttons{position:absolute;bottom:5px;right:5px}#files .gcode_files .entry .additionalInfo{padding-bottom:22px}#files .upload-buttons{margin-top:10px}#files .form-search{text-align:center;margin-bottom:5px!important}#control{overflow:hidden}#control .jog-panel{float:left;margin-right:19px}#control h1{text-align:left}#control .jog-panel>div{text-align:center}#control .jog-panel>div.distance{text-align:left}#control .box{width:30px;height:30px;margin-right:10px;margin-bottom:10px;padding-left:8px}#control .control-box{display:block;height:30px;margin-bottom:10px}#control .btn-group{margin-bottom:10px}#control .btn-group.distance>.btn{width:43px;padding:3px 0;height:30px}#gcode .progress{width:582px}#gcode .progress .bar{-webkit-transition:width 0s linear;-moz-transition:width 0s linear;-o-transition:width 0s linear;transition:width 0s linear}#gcode #gcode_layer_slider{height:568px;float:right}#gcode #gcode_layer_slider .slider-handle{width:14px;height:14px;margin-left:-3px;margin-top:-7px}#gcode #gcode_command_slider .slider-handle{width:14px;height:14px;margin-left:-7px;margin-top:-3px}#term #terminal-output{min-height:340px}.footer ul{margin:0}.footer ul li{display:inline;margin-left:1em;font-size:85%}.footer ul li:first-child{margin-left:0}.footer ul li a{color:#555}.ui-pnotify .alert a{color:#c09853}.ui-pnotify .alert-error a,.ui-pnotify .alert-danger a{color:#b94a48}.ui-pnotify .alert-success a{color:#468847}.ui-pnotify .alert-info a{color:#3a87ad}.pnotify_additional_info .pnotify_more{font-size:85%}.text-right{text-align:right}.overflow_visible{overflow:visible!important}.border_box{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none}textarea.block{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:100%}#drop_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#drop_overlay.in{display:block}#drop_overlay #drop_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#drop_overlay #drop_overlay_wrapper #drop,#drop_overlay #drop_overlay_wrapper #drop_background{position:absolute;top:50%;left:50%;margin-left:-200px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_locally,#drop_overlay #drop_overlay_wrapper #drop_locally_background{position:absolute;top:50%;left:50%;margin-left:-425px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_sd,#drop_overlay #drop_overlay_wrapper #drop_sd_background{position:absolute;top:50%;left:50%;margin-left:25px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper .dropzone{width:404px;height:404px;z-index:10001;color:#fff;font-size:30px}#drop_overlay #drop_overlay_wrapper .dropzone i{font-size:50px}#drop_overlay #drop_overlay_wrapper .dropzone .centered{display:table-cell;text-align:center;vertical-align:middle;width:400px;height:400px;line-height:40px;filter:alpha(opacity=100);-moz-opacity:1.0;-khtml-opacity:1.0;opacity:1.0}#drop_overlay #drop_overlay_wrapper .dropzone_background{width:400px;height:400px;border:2px dashed #eee;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;background-color:#000;filter:alpha(opacity=25);-moz-opacity:.25;-khtml-opacity:.25;opacity:.25}#drop_overlay #drop_overlay_wrapper .dropzone_background.hover{background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper .dropzone_background.fade{-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out;opacity:1}.icon-sd-black-14{background:url("../img/icon-sd-black-14.png") 0 3px no-repeat;width:11px;height:17px;display:inline-block!important}.center{float:none;margin-left:auto;margin-right:auto}.slider .slider-selection{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.slider .slider-selection:hover,.slider .slider-selection:focus,.slider .slider-selection:active,.slider .slider-selection.active,.slider .slider-selection.disabled,.slider .slider-selection[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.slider .slider-selection:active,.slider .slider-selection.active{background-color:#039 \9}.slider.slider-disabled .slider-selection{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-track{background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.slider.slider-disabled .slider-track{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);padding:0;margin-bottom:0;opacity:1;filter:alpha(opacity=100)}.slider .slider-handle:hover,.slider .slider-handle:focus,.slider .slider-handle:active,.slider .slider-handle.active,.slider .slider-handle.disabled,.slider .slider-handle[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.slider .slider-handle:active,.slider .slider-handle.active{background-color:#ccc \9}.slider .slider-handle:first-child{*margin-left:0}.slider .slider-handle:hover,.slider .slider-handle:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.slider .slider-handle:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.slider .slider-handle.active,.slider .slider-handle:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.slider .slider-handle.disabled,.slider .slider-handle[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle.hide{display:none}.slider .slider-handle.round{-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%} \ No newline at end of file +.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;margin-bottom:0;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{border-color:transparent;cursor:pointer;color:#08c;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.nowrap{white-space:nowrap}.actioncol{text-align:center;white-space:nowrap}.actioncol a{text-decoration:none;color:#000}.actioncol a.disabled{color:#ccc;cursor:default}#navbar .navbar-inner{background-color:#ebebeb;background-image:-moz-linear-gradient(top,#fff,#ccc);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ccc));background-image:-webkit-linear-gradient(top,#fff,#ccc);background-image:-o-linear-gradient(top,#fff,#ccc);background-image:linear-gradient(to bottom,#fff,#ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffcccccc',GradientType=0)}#navbar .navbar-inner .brand,#navbar .navbar-inner .nav>li>a{text-shadow:0 1px 0 #fff;color:#333}#navbar .navbar-inner .brand .caret,#navbar .navbar-inner .nav>li>a .caret{border-bottom-color:#939393;border-top-color:#939393}#navbar .navbar-inner .brand:hover .caret,#navbar .navbar-inner .nav>li>a:hover .caret,#navbar .navbar-inner .brand:focus .caret,#navbar .navbar-inner .nav>li>a:focus .caret{border-bottom-color:#636363;border-top-color:#636363}#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e0e0e0;background-image:-moz-linear-gradient(top,#ccc,#fff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ccc),to(#fff));background-image:-webkit-linear-gradient(top,#ccc,#fff);background-image:-o-linear-gradient(top,#ccc,#fff);background-image:linear-gradient(to bottom,#ccc,#fff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffcccccc',endColorstr='#ffffffff',GradientType=0)}#navbar .navbar-inner.red{background-color:#e36565;background-image:-moz-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9a0a0),to(#c20c0c));background-image:-webkit-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-o-linear-gradient(top,#f9a0a0,#c20c0c);background-image:linear-gradient(to bottom,#f9a0a0,#c20c0c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9a0a0',endColorstr='#ffc20c0c',GradientType=0)}#navbar .navbar-inner.red .brand,#navbar .navbar-inner.red .nav>li>a{text-shadow:0 1px 0 #c20c0c;color:#f2f2f2}#navbar .navbar-inner.red .brand .caret,#navbar .navbar-inner.red .nav>li>a .caret{border-bottom-color:#f28d8d;border-top-color:#f28d8d}#navbar .navbar-inner.red .brand:hover .caret,#navbar .navbar-inner.red .nav>li>a:hover .caret,#navbar .navbar-inner.red .brand:focus .caret,#navbar .navbar-inner.red .nav>li>a:focus .caret{border-bottom-color:#f2c0c0;border-top-color:#f2c0c0}#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.red .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d84747;background-image:-moz-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c20c0c),to(#f9a0a0));background-image:-webkit-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-o-linear-gradient(top,#c20c0c,#f9a0a0);background-image:linear-gradient(to bottom,#c20c0c,#f9a0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc20c0c',endColorstr='#fff9a0a0',GradientType=0)}#navbar .navbar-inner.orange{background-color:#e39665;background-image:-moz-linear-gradient(top,#f9c3a0,#c2530c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9c3a0),to(#c2530c));background-image:-webkit-linear-gradient(top,#f9c3a0,#c2530c);background-image:-o-linear-gradient(top,#f9c3a0,#c2530c);background-image:linear-gradient(to bottom,#f9c3a0,#c2530c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9c3a0',endColorstr='#ffc2530c',GradientType=0)}#navbar .navbar-inner.orange .brand,#navbar .navbar-inner.orange .nav>li>a{text-shadow:0 1px 0 #f6a570;color:#333}#navbar .navbar-inner.orange .brand .caret,#navbar .navbar-inner.orange .nav>li>a .caret{border-bottom-color:#93552e;border-top-color:#93552e}#navbar .navbar-inner.orange .brand:hover .caret,#navbar .navbar-inner.orange .nav>li>a:hover .caret,#navbar .navbar-inner.orange .brand:focus .caret,#navbar .navbar-inner.orange .nav>li>a:focus .caret{border-bottom-color:#634430;border-top-color:#634430}#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.orange .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d88047;background-image:-moz-linear-gradient(top,#c2530c,#f9c3a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2530c),to(#f9c3a0));background-image:-webkit-linear-gradient(top,#c2530c,#f9c3a0);background-image:-o-linear-gradient(top,#c2530c,#f9c3a0);background-image:linear-gradient(to bottom,#c2530c,#f9c3a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2530c',endColorstr='#fff9c3a0',GradientType=0)}#navbar .navbar-inner.yellow{background-color:#e3d765;background-image:-moz-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9f0a0),to(#c2b00c));background-image:-webkit-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-o-linear-gradient(top,#f9f0a0,#c2b00c);background-image:linear-gradient(to bottom,#f9f0a0,#c2b00c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f0a0',endColorstr='#ffc2b00c',GradientType=0)}#navbar .navbar-inner.yellow .brand,#navbar .navbar-inner.yellow .nav>li>a{text-shadow:0 1px 0 #f6e970;color:#333}#navbar .navbar-inner.yellow .brand .caret,#navbar .navbar-inner.yellow .nav>li>a .caret{border-bottom-color:#93892e;border-top-color:#93892e}#navbar .navbar-inner.yellow .brand:hover .caret,#navbar .navbar-inner.yellow .nav>li>a:hover .caret,#navbar .navbar-inner.yellow .brand:focus .caret,#navbar .navbar-inner.yellow .nav>li>a:focus .caret{border-bottom-color:#635e30;border-top-color:#635e30}#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.yellow .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d8ca47;background-image:-moz-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2b00c),to(#f9f0a0));background-image:-webkit-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-o-linear-gradient(top,#c2b00c,#f9f0a0);background-image:linear-gradient(to bottom,#c2b00c,#f9f0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2b00c',endColorstr='#fff9f0a0',GradientType=0)}#navbar .navbar-inner.green{background-color:#98f064;background-image:-moz-linear-gradient(top,#c8ffa7,#50da00);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8ffa7),to(#50da00));background-image:-webkit-linear-gradient(top,#c8ffa7,#50da00);background-image:-o-linear-gradient(top,#c8ffa7,#50da00);background-image:linear-gradient(to bottom,#c8ffa7,#50da00);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8ffa7',endColorstr='#ff50da00',GradientType=0)}#navbar .navbar-inner.green .brand,#navbar .navbar-inner.green .nav>li>a{text-shadow:0 1px 0 #a7ff74;color:#333}#navbar .navbar-inner.green .brand .caret,#navbar .navbar-inner.green .nav>li>a .caret{border-bottom-color:#55992e;border-top-color:#55992e}#navbar .navbar-inner.green .brand:hover .caret,#navbar .navbar-inner.green .nav>li>a:hover .caret,#navbar .navbar-inner.green .brand:focus .caret,#navbar .navbar-inner.green .nav>li>a:focus .caret{border-bottom-color:#446630;border-top-color:#446630}#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.green .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.open.active>.dropdown-toggle{background-color:#80e943;background-image:-moz-linear-gradient(top,#50da00,#c8ffa7);background-image:-webkit-gradient(linear,0 0,0 100%,from(#50da00),to(#c8ffa7));background-image:-webkit-linear-gradient(top,#50da00,#c8ffa7);background-image:-o-linear-gradient(top,#50da00,#c8ffa7);background-image:linear-gradient(to bottom,#50da00,#c8ffa7);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff50da00',endColorstr='#ffc8ffa7',GradientType=0)}#navbar .navbar-inner.blue{background-color:#6498f0;background-image:-moz-linear-gradient(top,#a7c8ff,#0050da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#a7c8ff),to(#0050da));background-image:-webkit-linear-gradient(top,#a7c8ff,#0050da);background-image:-o-linear-gradient(top,#a7c8ff,#0050da);background-image:linear-gradient(to bottom,#a7c8ff,#0050da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa7c8ff',endColorstr='#ff0050da',GradientType=0)}#navbar .navbar-inner.blue .brand,#navbar .navbar-inner.blue .nav>li>a{text-shadow:0 1px 0 #74a7ff;color:#333}#navbar .navbar-inner.blue .brand .caret,#navbar .navbar-inner.blue .nav>li>a .caret{border-bottom-color:#2e5599;border-top-color:#2e5599}#navbar .navbar-inner.blue .brand:hover .caret,#navbar .navbar-inner.blue .nav>li>a:hover .caret,#navbar .navbar-inner.blue .brand:focus .caret,#navbar .navbar-inner.blue .nav>li>a:focus .caret{border-bottom-color:#304466;border-top-color:#304466}#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.blue .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.open.active>.dropdown-toggle{background-color:#4380e9;background-image:-moz-linear-gradient(top,#0050da,#a7c8ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0050da),to(#a7c8ff));background-image:-webkit-linear-gradient(top,#0050da,#a7c8ff);background-image:-o-linear-gradient(top,#0050da,#a7c8ff);background-image:linear-gradient(to bottom,#0050da,#a7c8ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0050da',endColorstr='#ffa7c8ff',GradientType=0)}#navbar .navbar-inner.violet{background-color:#9864f0;background-image:-moz-linear-gradient(top,#c8a7ff,#5000da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8a7ff),to(#5000da));background-image:-webkit-linear-gradient(top,#c8a7ff,#5000da);background-image:-o-linear-gradient(top,#c8a7ff,#5000da);background-image:linear-gradient(to bottom,#c8a7ff,#5000da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8a7ff',endColorstr='#ff5000da',GradientType=0)}#navbar .navbar-inner.violet .brand,#navbar .navbar-inner.violet .nav>li>a{text-shadow:0 1px 0 #5000da;color:#f2f2f2}#navbar .navbar-inner.violet .brand .caret,#navbar .navbar-inner.violet .nav>li>a .caret{border-bottom-color:#b58df9;border-top-color:#b58df9}#navbar .navbar-inner.violet .brand:hover .caret,#navbar .navbar-inner.violet .nav>li>a:hover .caret,#navbar .navbar-inner.violet .brand:focus .caret,#navbar .navbar-inner.violet .nav>li>a:focus .caret{border-bottom-color:#d3c0f5;border-top-color:#d3c0f5}#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.violet .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.open.active>.dropdown-toggle{background-color:#8043e9;background-image:-moz-linear-gradient(top,#5000da,#c8a7ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5000da),to(#c8a7ff));background-image:-webkit-linear-gradient(top,#5000da,#c8a7ff);background-image:-o-linear-gradient(top,#5000da,#c8a7ff);background-image:linear-gradient(to bottom,#5000da,#c8a7ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5000da',endColorstr='#ffc8a7ff',GradientType=0)}#navbar .navbar-inner.black{background-color:#4f4f4f;background-image:-moz-linear-gradient(top,#787878,#121212);background-image:-webkit-gradient(linear,0 0,0 100%,from(#787878),to(#121212));background-image:-webkit-linear-gradient(top,#787878,#121212);background-image:-o-linear-gradient(top,#787878,#121212);background-image:linear-gradient(to bottom,#787878,#121212);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff787878',endColorstr='#ff121212',GradientType=0)}#navbar .navbar-inner.black .brand,#navbar .navbar-inner.black .nav>li>a{text-shadow:0 1px 0 #121212;color:#f2f2f2}#navbar .navbar-inner.black .brand .caret,#navbar .navbar-inner.black .nav>li>a .caret{border-bottom-color:#959595;border-top-color:#959595}#navbar .navbar-inner.black .brand:hover .caret,#navbar .navbar-inner.black .nav>li>a:hover .caret,#navbar .navbar-inner.black .brand:focus .caret,#navbar .navbar-inner.black .nav>li>a:focus .caret{border-bottom-color:#c4c4c4;border-top-color:#c4c4c4}#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.black .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.open.active>.dropdown-toggle{background-color:#3b3b3b;background-image:-moz-linear-gradient(top,#121212,#787878);background-image:-webkit-gradient(linear,0 0,0 100%,from(#121212),to(#787878));background-image:-webkit-linear-gradient(top,#121212,#787878);background-image:-o-linear-gradient(top,#121212,#787878);background-image:linear-gradient(to bottom,#121212,#787878);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff121212',endColorstr='#ff787878',GradientType=0)}#navbar .navbar-inner .brand span{background-size:20px 20px;background-position:left center;padding-left:24px;background-repeat:no-repeat}.octoprint-container{margin-top:20px}.octoprint-container .tab-content{padding:9px 15px;border-left:1px solid #DDD;border-right:1px solid #DDD;border-bottom:1px solid #DDD;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.octoprint-container .nav{margin-bottom:0}.octoprint-container .tab-content h1{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5;font-weight:normal}.octoprint-container .accordion-heading .accordion-heading-button{float:right}.octoprint-container .accordion-heading .accordion-heading-button a{display:inline-block;padding:8px 15px;font-size:14px;line-height:20px;color:#000;text-decoration:none;background:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.octoprint-container .accordion-heading a.accordion-toggle{display:inline-block}.octoprint-container .accordion-heading [class^="icon-"],.octoprint-container .accordion-heading [class*=" icon-"]{color:#000}.print-control .btn{padding-left:4px;padding-right:4px}.upload-buttons .btn{margin-right:0}table{table-layout:fixed}table .popover-title{text-overflow:ellipsis;word-break:break-all}table th,table td{overflow:hidden}table th.gcode_files_name,table td.gcode_files_name{text-overflow:ellipsis;text-align:left;white-space:nowrap}table th.gcode_files_action,table td.gcode_files_action{width:90px;text-align:center;white-space:nowrap}table th.gcode_files_action a,table td.gcode_files_action a{text-decoration:none;color:#000}table th.gcode_files_action a.disabled,table td.gcode_files_action a.disabled{color:#ccc;cursor:default}table th.timelapse_files_name,table td.timelapse_files_name{text-overflow:ellipsis;text-align:left}table th.timelapse_files_size,table td.timelapse_files_size{text-align:right;width:55px}table th.timelapse_files_action,table td.timelapse_files_action{width:45px;text-align:center;white-space:nowrap}table th.timelapse_files_action a,table td.timelapse_files_action a{text-decoration:none;color:#000}table th.timelapse_files_action a.disabled,table td.timelapse_files_action a.disabled{color:#ccc;cursor:default}table th.settings_users_name,table td.settings_users_name{text-overflow:ellipsis;text-align:left}table th.settings_users_active,table td.settings_users_active,table th.settings_users_admin,table td.settings_users_admin{text-align:center;width:55px}table th.settings_users_actions,table td.settings_users_actions{width:60px;text-align:center;white-space:nowrap}table th.settings_users_actions a,table td.settings_users_actions a{text-decoration:none;color:#000}table th.settings_users_actions a.disabled,table td.settings_users_actions a.disabled{color:#ccc;cursor:default}table th.settings_logs_name,table td.settings_logs_name{text-overflow:ellipsis;text-align:left}table th.settings_logs_size,table td.settings_logs_size{text-align:right;width:70px}table th.settings_logs_date,table td.settings_logs_date{text-align:left;width:130px}table th.settings_logs_action,table td.settings_logs_action{width:70px;text-align:center;white-space:nowrap}table th.settings_logs_action a,table td.settings_logs_action a{text-decoration:none;color:#000}table th.settings_logs_action a.disabled,table td.settings_logs_action a.disabled{color:#ccc;cursor:default}table th.settings_printerProfiles_profiles_name,table td.settings_printerProfiles_profiles_name{text-overflow:ellipsis;text-align:left}table th.settings_printerProfiles_profiles_model,table td.settings_printerProfiles_profiles_model{text-align:left;width:250px}table th.settings_printerProfiles_profiles_action,table td.settings_printerProfiles_profiles_action{width:80px;text-align:center;white-space:nowrap}table th.settings_printerProfiles_profiles_action a,table td.settings_printerProfiles_profiles_action a{text-decoration:none;color:#000}table th.settings_printerProfiles_profiles_action a.disabled,table td.settings_printerProfiles_profiles_action a.disabled{color:#ccc;cursor:default}#temperature-graph{height:350px;width:100%;background-image:url("../img/graph-background.png");background-position:center;background-repeat:no-repeat}.tab-content,.tab-pane{overflow:visible}.tempInput{width:50px}#temp_newTemp,#temp_newBedTemp,#speed_innerWall,#speed_outerWall,#speed_fill,#speed_support,#webcam_timelapse_interval,#webcam_timelapse_postRoll,#webcam_timelapse_fps{text-align:right}ul.dropdown-menu li a{cursor:pointer}#connection_ports,#connection_baudrates,#connection_printers{width:100%}#offline_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#offline_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#offline_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#offline_overlay_wrapper .container{margin:auto}#webcam_container{width:100%;position:relative;outline:0}#webcam_container .flipH{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1)}#webcam_container .flipV{-webkit-transform:scaleY(-1);-moz-transform:scaleY(-1)}#webcam_container .flipH.flipV{-webkit-transform:scaleX(-1) scaleY(-1);-moz-transform:scaleX(-1) scaleY(-1)}#webcam_container .keycontrol_overlay{position:absolute;left:10px;right:10px;bottom:10px;background:rgba(0,0,0,0.5);font-size:85%;color:white;padding:0}#webcam_container .keycontrol_overlay kbd{border:1px solid #eee;border-radius:3px;margin-left:2px;margin-right:2px;font-size:90%;padding:2px;min-width:1em}#webcam_container .keycontrol_overlay .keycontrol_overlay_heading{position:relative;padding:10px;font-weight:bold}#webcam_container .keycontrol_overlay .keycontrol_overlay_column{position:relative;width:45%;padding:10px;float:left}#files .gcode_files{padding-right:7px}#files .gcode_files .entry{padding:5px;line-height:20px;border-bottom:1px solid #ddd;position:relative}#files .gcode_files .entry:hover{background-color:#f5f5f5}#files .gcode_files .entry .title{text-overflow:ellipsis;word-break:break-all}#files .gcode_files .entry .uploaded,#files .gcode_files .entry .size,#files .gcode_files .entry .additionalInfo{font-size:85%;color:#999}#files .gcode_files .entry .action-buttons{position:absolute;bottom:5px;right:5px}#files .gcode_files .entry .additionalInfo{padding-bottom:22px}#files .upload-buttons{margin-top:10px}#files .form-search{text-align:center;margin-bottom:5px!important}#control{overflow:hidden}#control .jog-panel{float:left;margin-right:19px}#control h1{text-align:left}#control .jog-panel>div{text-align:center}#control .jog-panel>div.distance{text-align:left}#control .jog-panel .slider{margin-bottom:10px}#control .box{width:30px;height:30px;margin-right:10px;margin-bottom:10px;padding-left:8px}#control .control-box{display:block;height:30px;margin-bottom:10px}#control .btn-group{margin-bottom:10px}#control .btn-group.distance>.btn{width:43px;padding:3px 0;height:30px}#control .slider-handle{width:14px;height:14px;margin-left:-7px;margin-top:-3px}#control .custom_parametric_command .slider{margin-left:10px;margin-right:10px;margin-bottom:2px}#gcode .progress{width:582px}#gcode .progress .bar{-webkit-transition:width 0s linear;-moz-transition:width 0s linear;-o-transition:width 0s linear;transition:width 0s linear}#gcode #gcode_layer_slider{height:568px;float:right}#gcode #gcode_layer_slider .slider-handle{width:14px;height:14px;margin-left:-3px;margin-top:-7px}#gcode #gcode_command_slider .slider-handle{width:14px;height:14px;margin-left:-7px;margin-top:-3px}#term .terminal{margin-bottom:30px}#term .terminal #terminal-output{min-height:340px;margin-bottom:5px}#term #terminal-sendpanel{text-align:right}.footer ul{margin:0}.footer ul li{display:inline;margin-left:1em;font-size:85%}.footer ul li:first-child{margin-left:0}.footer ul li a{color:#555}.ui-pnotify .alert a{color:#c09853}.ui-pnotify .alert-error a,.ui-pnotify .alert-danger a{color:#b94a48}.ui-pnotify .alert-success a{color:#468847}.ui-pnotify .alert-info a{color:#3a87ad}.pnotify_additional_info .pnotify_more{font-size:85%}.text-right{text-align:right}.overflow_visible{overflow:visible!important}.hidden{display:none}#drop_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#drop_overlay.in{display:block}#drop_overlay #drop_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#drop_overlay #drop_overlay_wrapper #drop,#drop_overlay #drop_overlay_wrapper #drop_background{position:absolute;top:50%;left:50%;margin-left:-200px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_locally,#drop_overlay #drop_overlay_wrapper #drop_locally_background{position:absolute;top:50%;left:50%;margin-left:-425px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_sd,#drop_overlay #drop_overlay_wrapper #drop_sd_background{position:absolute;top:50%;left:50%;margin-left:25px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper .dropzone{width:404px;height:404px;z-index:10001;color:#fff;font-size:30px}#drop_overlay #drop_overlay_wrapper .dropzone i{font-size:50px}#drop_overlay #drop_overlay_wrapper .dropzone .centered{display:table-cell;text-align:center;vertical-align:middle;width:400px;height:400px;line-height:40px;filter:alpha(opacity=100);-moz-opacity:1.0;-khtml-opacity:1.0;opacity:1.0}#drop_overlay #drop_overlay_wrapper .dropzone_background{width:400px;height:400px;border:2px dashed #eee;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;background-color:#000;filter:alpha(opacity=25);-moz-opacity:.25;-khtml-opacity:.25;opacity:.25}#drop_overlay #drop_overlay_wrapper .dropzone_background.hover{background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper .dropzone_background.fade{-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out;opacity:1}.icon-sd-black-14{background:url("../img/icon-sd-black-14.png") 0 3px no-repeat;width:11px;height:17px;display:inline-block!important}.center{float:none;margin-left:auto;margin-right:auto}.slider .slider-selection{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.slider .slider-selection:hover,.slider .slider-selection:focus,.slider .slider-selection:active,.slider .slider-selection.active,.slider .slider-selection.disabled,.slider .slider-selection[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.slider .slider-selection:active,.slider .slider-selection.active{background-color:#039 \9}.slider.slider-disabled .slider-selection{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-track{background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.slider.slider-disabled .slider-track{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);padding:0;margin-bottom:0;opacity:1;filter:alpha(opacity=100)}.slider .slider-handle:hover,.slider .slider-handle:focus,.slider .slider-handle:active,.slider .slider-handle.active,.slider .slider-handle.disabled,.slider .slider-handle[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.slider .slider-handle:active,.slider .slider-handle.active{background-color:#ccc \9}.slider .slider-handle:first-child{*margin-left:0}.slider .slider-handle:hover,.slider .slider-handle:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.slider .slider-handle:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.slider .slider-handle.active,.slider .slider-handle:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.slider .slider-handle.disabled,.slider .slider-handle[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle.hide{display:none}.slider .slider-handle.round{-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%} \ No newline at end of file diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index 342e4f4d..f1c79de6 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -236,6 +236,26 @@ $(function() { return $(window).height() - 165; }; + // jquery plugin to select all text in an element + // originally from: http://stackoverflow.com/a/987376 + $.fn.selectText = function() { + var doc = document; + var element = this[0]; + var range, selection; + + if (doc.body.createTextRange) { + range = document.body.createTextRange(); + range.moveToElementText(element); + range.select(); + } else if (window.getSelection) { + selection = window.getSelection(); + range = document.createRange(); + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + } + }; + // Use bootstrap tabdrop for tabs and pills $('.nav-pills, .nav-tabs').tabdrop(); diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index c2e2e403..660ccf4c 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -55,6 +55,7 @@ $(function() { self.webcam_snapshotUrl = ko.observable(undefined); self.webcam_ffmpegPath = ko.observable(undefined); self.webcam_bitrate = ko.observable(undefined); + self.webcam_ffmpegThreads = ko.observable(undefined); self.webcam_watermark = ko.observable(undefined); self.webcam_flipH = ko.observable(undefined); self.webcam_flipV = ko.observable(undefined); @@ -188,6 +189,7 @@ $(function() { self.webcam_snapshotUrl(response.webcam.snapshotUrl); self.webcam_ffmpegPath(response.webcam.ffmpegPath); self.webcam_bitrate(response.webcam.bitrate); + self.webcam_ffmpegThreads(response.webcam.ffmpegThreads); self.webcam_watermark(response.webcam.watermark); self.webcam_flipH(response.webcam.flipH); self.webcam_flipV(response.webcam.flipV); @@ -255,6 +257,7 @@ $(function() { "snapshotUrl": self.webcam_snapshotUrl(), "ffmpegPath": self.webcam_ffmpegPath(), "bitrate": self.webcam_bitrate(), + "ffmpegThreads": self.webcam_ffmpegThreads(), "watermark": self.webcam_watermark(), "flipH": self.webcam_flipH(), "flipV": self.webcam_flipV() @@ -326,4 +329,4 @@ $(function() { ["loginStateViewModel", "usersViewModel", "printerProfilesViewModel"], ["#settings_dialog", "#navbar_settings"] ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index 9a97474e..235dd0c8 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -5,7 +5,8 @@ $(function() { self.loginState = parameters[0]; self.settings = parameters[1]; - self.log = []; + self.log = ko.observableArray([]); + self.buffer = ko.observable(300); self.command = ko.observable(undefined); @@ -20,15 +21,56 @@ $(function() { self.autoscrollEnabled = ko.observable(true); self.filters = self.settings.terminalFilters; - self.filterRegex = undefined; + self.filterRegex = ko.observable(); self.cmdHistory = []; self.cmdHistoryIdx = -1; + self.displayedLines = ko.computed(function() { + var regex = self.filterRegex(); + var lineVisible = function(entry) { + return regex == undefined || !entry.line.match(regex); + }; + + var filtered = false; + var result = []; + _.each(self.log(), function(entry) { + if (lineVisible(entry)) { + result.push(entry); + filtered = false; + } else if (!filtered) { + result.push(self._toInternalFormat("[...]", "filtered")); + filtered = true; + } + }); + + return result; + }); + self.displayedLines.subscribe(function() { + self.updateOutput(); + }); + + self.lineCount = ko.computed(function() { + var total = self.log().length; + var displayed = _.filter(self.displayedLines(), function(entry) { return entry.type == "line" }).length; + var filtered = total - displayed; + + if (total == displayed) { + return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed}); + } else { + return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered}); + } + }); + + self.autoscrollEnabled.subscribe(function(newValue) { + if (newValue) { + self.log(self.log.slice(-self.buffer())); + } + }); + self.activeFilters = ko.observableArray([]); self.activeFilters.subscribe(function(e) { self.updateFilterRegex(); - self.updateOutput(); }); self.fromCurrentData = function(data) { @@ -42,16 +84,21 @@ $(function() { }; self._processCurrentLogData = function(data) { - if (!self.log) - self.log = []; - self.log = self.log.concat(data); - self.log = self.log.slice(-300); - self.updateOutput(); + self.log(self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) }))); + if (self.autoscrollEnabled()) { + self.log(self.log.slice(-300)); + } }; self._processHistoryLogData = function(data) { - self.log = data; - self.updateOutput(); + self.log(_.map(data, function(line) { return self._toInternalFormat(line) })); + }; + + self._toInternalFormat = function(line, type) { + if (type == undefined) { + type = "line"; + } + return {line: line, type: type} }; self._processStateData = function(data) { @@ -67,29 +114,34 @@ $(function() { self.updateFilterRegex = function() { var filterRegexStr = self.activeFilters().join("|").trim(); if (filterRegexStr == "") { - self.filterRegex = undefined; + self.filterRegex(undefined); } else { - self.filterRegex = new RegExp(filterRegexStr); + self.filterRegex(new RegExp(filterRegexStr)); } + self.updateOutput(); }; self.updateOutput = function() { - if (!self.log) - return; - - var output = ""; - for (var i = 0; i < self.log.length; i++) { - if (self.filterRegex !== undefined && self.log[i].match(self.filterRegex)) continue; - output += self.log[i] + "\n"; + if (self.autoscrollEnabled()) { + self.scrollToEnd(); } + }; + self.toggleAutoscroll = function() { + self.autoscrollEnabled(!self.autoscrollEnabled()); + }; + + self.selectAll = function() { var container = $("#terminal-output"); if (container.length) { - container.text(output); + container.selectText(); + } + }; - if (self.autoscrollEnabled()) { - container.scrollTop(container[0].scrollHeight - container.height()) - } + self.scrollToEnd = function() { + var container = $("#terminal-output"); + if (container.length) { + container.scrollTop(container[0].scrollHeight - container.height()) } }; @@ -155,10 +207,12 @@ $(function() { }; self.onAfterTabChange = function(current, previous) { - if (current != "#terminal") { + if (current != "#term") { return; } - self.updateOutput(); + if (self.autoscrollEnabled()) { + self.scrollToEnd(); + } }; } diff --git a/src/octoprint/static/js/app/viewmodels/timelapse.js b/src/octoprint/static/js/app/viewmodels/timelapse.js index f902fa19..f3f8abcc 100644 --- a/src/octoprint/static/js/app/viewmodels/timelapse.js +++ b/src/octoprint/static/js/app/viewmodels/timelapse.js @@ -4,9 +4,14 @@ $(function() { self.loginState = parameters[0]; + self.defaultFps = 25; + self.defaultPostRoll = 0; + self.defaultInterval = 10; + self.timelapseType = ko.observable(undefined); - self.timelapseTimedInterval = ko.observable(undefined); - self.timelapsePostRoll = ko.observable(undefined); + self.timelapseTimedInterval = ko.observable(self.defaultInterval); + self.timelapsePostRoll = ko.observable(self.defaultPostRoll); + self.timelapseFps = ko.observable(self.defaultFps); self.persist = ko.observable(false); self.isDirty = ko.observable(false); @@ -42,6 +47,9 @@ $(function() { self.timelapsePostRoll.subscribe(function(newValue) { self.isDirty(true); }); + self.timelapseFps.subscribe(function(newValue) { + self.isDirty(true); + }); // initialize list helper self.listHelper = new ItemListHelper( @@ -91,15 +99,23 @@ $(function() { self.listHelper.updateItems(response.files); if (config.type == "timed") { - if (response.config.interval != undefined && response.config.interval > 0) { - self.timelapseTimedInterval(response.config.interval); - } - if (response.config.postRoll != undefined && response.config.postRoll >= 0) { - self.timelapsePostRoll(response.config.postRoll); + if (config.interval != undefined && config.interval > 0) { + self.timelapseTimedInterval(config.interval); } } else { - self.timelapseTimedInterval(undefined); - self.timelapsePostRoll(undefined); + self.timelapseTimedInterval(self.defaultInterval); + } + + if (config.postRoll != undefined && config.postRoll >= 0) { + self.timelapsePostRoll(config.postRoll); + } else { + self.timelapsePostRoll(self.defaultPostRoll); + } + + if (config.fps != undefined && config.fps > 0) { + self.timelapseFps(config.fps); + } else { + self.timelapseFps(self.defaultFps); } self.persist(false); @@ -137,6 +153,7 @@ $(function() { var payload = { "type": self.timelapseType(), "postRoll": self.timelapsePostRoll(), + "fps": self.timelapseFps(), "save": self.persist() }; @@ -167,4 +184,4 @@ $(function() { ["loginStateViewModel"], "#timelapse" ]); -}); \ No newline at end of file +}); diff --git a/src/octoprint/static/less/octoprint.less b/src/octoprint/static/less/octoprint.less index f1a2799b..d3e50fc1 100644 --- a/src/octoprint/static/less/octoprint.less +++ b/src/octoprint/static/less/octoprint.less @@ -323,7 +323,7 @@ table { } #temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support, -#webcam_timelapse_interval, #webcam_timelapse_postRoll { +#webcam_timelapse_interval, #webcam_timelapse_postRoll, #webcam_timelapse_fps { text-align: right; } @@ -590,8 +590,17 @@ ul.dropdown-menu li a { /** Terminal output */ #term { - #terminal-output { - min-height: 340px; + .terminal { + #terminal-output { + min-height: 340px; + margin-bottom: 5px; + } + + margin-bottom: 30px; + } + + #terminal-sendpanel { + text-align: right; } } diff --git a/src/octoprint/templates/dialogs/settings/webcam.jinja2 b/src/octoprint/templates/dialogs/settings/webcam.jinja2 index a2328c65..d334fa9b 100644 --- a/src/octoprint/templates/dialogs/settings/webcam.jinja2 +++ b/src/octoprint/templates/dialogs/settings/webcam.jinja2 @@ -1,28 +1,34 @@
-
+
-
- +
+
-
- +
+
-
+
+
+ +
+ +
+
- {{ _('Hint: You can also drag and drop files on this page to upload them.') }} + {{ _('Hint: You can also drag and drop files on this page to upload them.') }}
diff --git a/src/octoprint/templates/tabs/control.jinja2 b/src/octoprint/templates/tabs/control.jinja2 index ff360ef7..1c589e57 100644 --- a/src/octoprint/templates/tabs/control.jinja2 +++ b/src/octoprint/templates/tabs/control.jinja2 @@ -18,7 +18,7 @@
- {{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }} + {{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}
{% endif %} diff --git a/src/octoprint/templates/tabs/terminal.jinja2 b/src/octoprint/templates/tabs/terminal.jinja2 index 1cc87677..13a1cc3e 100644 --- a/src/octoprint/templates/tabs/terminal.jinja2 +++ b/src/octoprint/templates/tabs/terminal.jinja2 @@ -1,14 +1,22 @@ -

-
-
- +
+

+ + {{ _("Scroll to end") }} | {{ _("Select all") }}
-