More work on the template refactoring and new template plugin types

This commit is contained in:
Gina Häußge 2015-01-15 11:06:59 +01:00
parent 73466bb513
commit 23387d6e48
25 changed files with 971 additions and 746 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -37,6 +37,9 @@ class PluginInfo(object):
self._version = version
if self.name is None:
raise ValueError("Plugin {key} doesn't have a name".format(key=key))
def __str__(self):
return "{name} ({version})".format(name=self.name, version=self.version if self.version else "unknown")
@ -109,7 +112,8 @@ class PluginManager(object):
self.plugins = dict()
self.plugin_hooks = defaultdict(list)
self.plugin_implementations = defaultdict(list)
self.plugin_implementations = defaultdict(set)
self.plugin_implementations_by_type = defaultdict(list)
self.registered_clients = []
@ -223,10 +227,31 @@ class PluginManager(object):
# evaluate registered implementations
for plugin_type in self.plugin_types:
implementations = plugin.get_implementations(plugin_type)
self.plugin_implementations[plugin_type] += ( (name, implementation) for implementation in implementations )
self.plugin_implementations_by_type[plugin_type] += ( (name, implementation) for implementation in implementations )
self.plugin_implementations[name].update(plugin.get_implementations())
self.log_registered_plugins()
def initialize_implementations(self, additional_injects=None):
if additional_injects is None:
additional_injects = dict()
for name, implementations in self.plugin_implementations.items():
plugin = self.plugins[name]
for implementation in implementations:
kwargs = dict(additional_injects)
kwargs.update(dict(
identifier=name,
basefolder=os.path.realpath(plugin.location),
logger=logging.getLogger("octoprint.plugins." + name),
))
try:
implementation.pre_initialize(**kwargs)
except:
self.logger.exception("Exception while initializing plugin")
self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations)))
def log_registered_plugins(self):
if len(self.plugins) <= 0:
self.logger.info("No plugins found")
@ -247,7 +272,7 @@ class PluginManager(object):
result = None
for t in types:
implementations = self.plugin_implementations[t]
implementations = self.plugin_implementations_by_type[t]
if result is None:
result = set(implementations)
else:
@ -283,4 +308,10 @@ class PluginManager(object):
class Plugin(object):
pass
def pre_initialize(self, **kwargs):
for arg, value in kwargs.items():
setattr(self, "_" + arg, value)
self.initialize()
def initialize(self):
pass

View file

@ -63,20 +63,13 @@ class AssetPlugin(Plugin):
def get_asset_folder(self):
"""
Defines the folder where the plugin stores its static assets as defined in ``get_assets``. Usually an
implementation such as
.. code-block:: python
def get_asset_folder(self):
import os
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "static")
should be sufficient here. This way, assets can be put into a sub folder ``static`` in the plugin directory.
Defines the folder where the plugin stores its static assets as defined in ``get_assets``. Override this if
your plugin stores its assets at some other place than the ``static`` sub folder in the plugin base directory.
:return: the absolute path to the folder where the plugin stores its static assets
"""
return None
import os
return os.path.join(self._basefolder, "static")
def get_assets(self):
"""
@ -108,15 +101,168 @@ class AssetPlugin(Plugin):
:return: a dictionary describing the static assets to publish for the plugin
"""
return []
return dict()
class TemplatePlugin(Plugin):
"""
Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface.
Currently OctoPrint supports the following types of injections:
Navbar
The right part of the navigation bar located at the top of the UI can be enriched with additional links. Note that
with the current implementation, plugins will always be located *to the left* of the existing links.
Sidebar
The left side bar containing Connection, State and Files sections can be enriched with additional sections. Note
that with the current implementations, plugins will always be located *beneath* the existing sections.
Tabs
The available tabs of the main part of the interface may be extended with additional tabs originating from within
plugins. Note that with the current implementation, plugins will always be located *to the right* of the existing
tabs.
Settings
Plugins may inject a dialog into the existing settings view. Note that with the current implementations, plugins
will always be listed beneath the "Plugins" header in the settings link list, ordered alphabetically after
their displayed name.
.. figure:: ../images/template-plugin-types-main.png
:align: center
:alt: Template injection types in the main part of the interface
Template injection types in the main part of the interface
.. figure:: ../images/template-plugin-types-settings.png
:align: center
:alt: Template injection types in the settings
Template injection types in the settings
Which components a plugin supplies is controlled by a number of special template variables returned by the overridden
``get_template_vars`` method. The following special keys are supported:
_settings
Configures the settings component to inject. The value must be a dictionary, supported values are the following:
name
The name under which to include the settings pane, will be visible in the navigation tree on the left of the
settings dialog
custom_bindings
A boolean value indicating whether the default settings view model should be bound to the settings pane (``false``, default)
or if a custom binding will be used by the plugin (``true``)
data_bind
Additional knockout data bindings to apply to the template container, can be used to add further behaviour to
the container based on internal state if necessary. Note that if you include this and set ``custom_bindings``
to ``True``, you need to also supply ``allowBindings: true`` as part of your custom data binding, otherwise
it won't work.
The included settings dialog template file must be called ``<plugin name>_settings.jinja2`` (e.g. ``myplugin_settings.jinja2``).
The template will be already included in the necessary wrapper divs, you just need to supply the pure content.
_tab
Configures the tab component to inject. The value must be a dictionary, supported values are the following:
name
The name under which to include the tab, will be visible in the list of tabs on the top of the main view
data_bind
Additional knockout data bindings to apply to the template container, can be used to add further behaviour to
the container based on internal state if necessary. Note that if you include this and set ``custom_bindings``
to ``True``, you need to also supply ``allowBindings: true`` as part of your custom data binding, otherwise
it won't work.
The template included into the tab pane section must be called ``<plugin name>_tab.jinja2`` (e.g. ``myplugin_tab.jinja2``).
The template will be already included in the necessary wrapper divs, you just need to supply the pure content.
_sidebar
Configures the sidebar component to inject. The value must be a dictionary, supported values are the following:
name
The name of the sidebar entry
icon
Icon to use for the sidebar header, should be the name of a Font Awesome icon without the leading ``icon-`` part
header_addon
Additional template to include in the head section of the sidebar item. For an example of this, see the additional
options included in the "Files" section
data_bind
Additional knockout data bindings to apply to the template container, can be used to add further behaviour to
the container based on internal state if necessary. Note that if you include this and set ``custom_bindings``
to ``True``, you need to also supply ``allowBindings: true`` as part of your custom data binding, otherwise
it won't work.
The template included into the sidebar section must be called ``<plugin name>_sidebar.jinja2`` (e.g. ``myplugin_sidebar.jinja2``).
The template will be already included in the necessary wrapper divs, you just need to supply the pure content.
_navbar
Configures the navbar component to inject. The value must be an empty dictionary.
The template included into the sidebar section must be called ``<plugin name>_navbar.jinja2``
The following is an example for a simple plugin which injects all four types of templates into the interfaces:
``helloworld/__init__.py``
.. code-block:: python
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.TemplatePlugin, octoprint.plugin.AssetPlugin):
def get_template_vars(self):
return dict(
_settings=dict(name="Hello World"),
_tab=dict(name="Hello World", custom_bindings=True, data_bind="allowBindings: true, visible: self.loginState.isUser()"),
_sidebar=dict(name="Hello World", icon="gear"),
_navbar=dict()
)
def get_assets(self):
return dict(
js=["js/helloworld.js"]
)
__plugin_name__ = "Hello World"
__plugin_version__ = "1.0"
__plugin_implementations__ = [HelloWorldPlugin()]
``helloworld/templates/helloworld_settings.jinja2``
.. code-block:: html+jinja
<h1>Hello World!</h1>
This is a custom settings pane.
``helloworld/templates/helloworld_tab.jinja2``
.. code-block:: html+jinja
<h1>Hello World!</h1>
This is a custom tab.
``helloworld/templates/helloworld_sidebar.jinja2``
.. code-block:: html+jinja
Hello World! This is a custom side bar.
``helloworld/templates/helloworld_navbar.jinja2``
.. code-block:: html+jinja
<li>
<a class="pull-right" href="http://www.example.com"><i class="icon-gear"></i> {{ _('Hello World!') }}</a>
</li>
"""
def get_template_vars(self):
return dict()
def get_template_folder(self):
return None
"""
Defines the folder where the plugin stores its templates. Override this if your plugin stores its templates at
some other place than the ``templates`` sub folder in the plugin base directory.
:return: the absolute path to the folder where the plugin stores its jinja2 templates
"""
import os
return os.path.join(self._basefolder, "templates")
class SimpleApiPlugin(Plugin):

View file

@ -111,41 +111,78 @@ def index():
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
plugin_vars = dict()
plugin_includes = dict()
plugin_includes_navbar = []
plugin_includes_sidebar = []
plugin_includes_tabs = []
plugin_includes_settings = []
plugin_names = template_plugins.keys()
for name, implementation in template_plugins.items():
vars = implementation.get_template_vars()
if not isinstance(vars, dict):
continue
plugin_data = dict()
if "_navbar" in vars and isinstance(vars["_navbar"], dict):
data = dict(vars["_navbar"])
data["_div"] = "navbar_plugin_" + name
data["_template"] = name + "_navbar.jinja2"
plugin_includes_navbar.append(data)
del vars["_navbar"]
if "_settings" in vars and "name" in vars["_settings"]:
plugin_data["_settings"] = dict(vars["_settings"])
plugin_data["_settings"]["_div"] = "settings_plugin_" + name
plugin_data["_settings"]["_template"] = name + "_settings.jinja2"
del vars["_settings"]
if "_tab" in vars and "name" in vars["_tab"]:
plugin_data["_tab"] = dict(vars["_tab"])
plugin_data["_tab"]["_div"] = "tab_plugin_" + name
plugin_data["_tab"]["_template"] = name + "_tab.jinja2"
del vars["_tab"]
if "_sidebar" in vars and "name" in vars["_sidebar"]:
plugin_data["_sidebar"] = vars["_sidebar"]
plugin_data["_sidebar"]["_div"] = "sidebar_plugin_" + name
plugin_data["_sidebar"]["_template"] = name + "_sidebar.jinja2"
if "_sidebar" in vars and isinstance(vars["_sidebar"], dict) and "name" in vars["_sidebar"]:
data = dict(vars["_sidebar"])
data["_div"] = "sidebar_plugin_" + name
data["_template"] = name + "_sidebar.jinja2"
plugin_includes_sidebar.append((data["name"], data))
del vars["_sidebar"]
if len(plugin_data):
plugin_includes[name] = plugin_data
if "_tab" in vars and isinstance(vars["_tab"], dict) and "name" in vars["_tab"]:
data = dict(vars["_tab"])
data["_div"] = "tab_plugin_" + name
data["_template"] = name + "_tab.jinja2"
plugin_includes_tabs.append((data["name"], data))
del vars["_tab"]
if "_settings" in vars and isinstance(vars["_settings"], dict) and "name" in vars["_settings"]:
data = dict(vars["_settings"])
data["_div"] = "settings_plugin_" + name
data["_template"] = name + "_settings.jinja2"
plugin_includes_settings.append((data["name"], data))
del vars["_settings"]
for var_name, var_value in vars.items():
plugin_vars["plugin_" + name + "_" + var_name] = var_value
#~~ navbar
navbar_entries = plugin_includes_navbar + [
dict(_template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin"),
dict(_template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin"),
dict(_template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"])
]
#~~ sidebar
sidebar_entries = [
(gettext("Connection"), dict(_template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isAdmin")),
(gettext("State"), dict(_template="sidebar/state.jinja2", _div="state", icon="info-sign")),
(gettext("Files"), dict(_template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], header_addon="sidebar/files_header.jinja2"))
] + plugin_includes_sidebar
#~~ tabs
tab_entries = [
(gettext("Temperature"), dict(_template="tabs/temperature.jinja2", _div="temp")),
(gettext("Control"), dict(_template="tabs/control.jinja2", _div="control")),
(gettext("GCode Viewer"), dict(_template="tabs/gcodeviewer.jinja2", _div="gcode")),
(gettext("Terminal"), dict(_template="tabs/terminal.jinja2", _div="term")),
(gettext("Timelapse"), dict(_template="tabs/timelapse.jinja2", _div="timelapse"))
] + plugin_includes_tabs
#~~ settings dialog
settings_entries = [
(gettext("Printer"), None),
(gettext("Serial Connection"), dict(_template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", _active=True)),
(gettext("Serial Connection"), dict(_template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection")),
(gettext("Printer Profiles"), dict(_template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles")),
(gettext("Temperatures"), dict(_template="dialogs/settings/temperatures.jinja2", _div="settings_temperature")),
(gettext("Terminal Filters"), dict(_template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters")),
@ -159,14 +196,9 @@ def index():
(gettext("Appearance"), dict(_template="dialogs/settings/appearance.jinja2", _div="settings_appearance")),
(gettext("Logs"), dict(_template="dialogs/settings/logs.jinja2", _div="settings_logs", data_bind="allowBindings: false"))
]
if len(plugin_includes):
plugin_tuples = []
for name, data in plugin_includes.items():
if "_settings" in data:
plugin_tuples.append((data["_settings"]["name"], data["_settings"]))
if len(plugin_tuples):
settings_entries.append((gettext("Plugins"), None))
settings_entries.extend(sorted(plugin_tuples, key=lambda x: x[0]))
if len(plugin_includes_settings):
settings_entries.append((gettext("Plugins"), None))
settings_entries.extend(sorted(plugin_includes_settings, key=lambda x: x[0]))
#~~ prepare full set of template vars for rendering
@ -186,7 +218,11 @@ def index():
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
uiApiKey=UI_API_KEY,
navbarEntries=navbar_entries,
sidebarEntries=sidebar_entries,
tabEntries=tab_entries,
settingsEntries=settings_entries,
pluginNames=plugin_names,
assetPlugins=asset_plugin_urls,
)
render_kwargs.update(plugin_vars)
@ -309,6 +345,16 @@ class Server():
printer = Printer(fileManager, analysisQueue, printerProfileManager)
appSessionManager = util.flask.AppSessionManager()
pluginManager.initialize_implementations(dict(
printer_profile_manager=printerProfileManager,
event_manager=eventManager,
analysis_queue=analysisQueue,
slicing_manager=slicingManager,
storage_managers=storage_managers,
file_manager=fileManager,
app_session_manager=appSessionManager
))
# configure additional template folders for jinja2
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
additional_template_folders = []

View file

@ -418,9 +418,9 @@ $(function() {
settingsViewModel.requestData(function() {
ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog"));
ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion"));
ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion"));
ko.applyBindings(connectionViewModel, document.getElementById("connection_wrapper"));
ko.applyBindings(printerStateViewModel, document.getElementById("state_wrapper"));
ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_wrapper"));
ko.applyBindings(temperatureViewModel, document.getElementById("temp"));
ko.applyBindings(controlViewModel, document.getElementById("control"));
ko.applyBindings(terminalViewModel, document.getElementById("term"));
@ -444,24 +444,31 @@ $(function() {
// apply bindings and signal startup
_.each(additionalViewModels, function(additionalViewModel) {
if (additionalViewModel[1] === undefined) {
var viewModel = additionalViewModel[0];
var targets = additionalViewModel[1];
if (targets === undefined) {
return;
}
if (additionalViewModel[0].hasOwnProperty("onBeforeBinding")) {
additionalViewModel[0].onBeforeBinding();
if (!Array.isArray(targets)) {
targets = [targets];
}
// model instance, target container
try {
ko.applyBindings(additionalViewModel[0], additionalViewModel[1]);
} catch (exc) {
console.log("Could not apply bindings for additional view model " + additionalViewModel[0] + ": " + exc.message);
if (viewModel.hasOwnProperty("onBeforeBinding")) {
viewModel.onBeforeBinding();
}
_.each(targets, function(target) {
try {
ko.applyBindings(viewModel, target);
} catch (exc) {
console.log("Could not apply bindings for additional view model " + viewModel + ": " + exc.message);
}
});
if (additionalViewModel[0].hasOwnProperty("onAfterBinding")) {
additionalViewModel[0].onAfterBinding();
if (viewModel.hasOwnProperty("onAfterBinding")) {
viewModel.onAfterBinding();
}
});
});
@ -517,14 +524,14 @@ $(function() {
}
});
$(".accordion-toggle[href='#files']").click(function() {
$(".accordion-toggle[data-target='#files']").click(function() {
var files = $("#files");
if (files.hasClass("in")) {
files.removeClass("overflow_visible");
} else {
setTimeout(function() {
files.addClass("overflow_visible");
}, 1000);
}, 100);
}
});

View file

@ -665,6 +665,9 @@ ul.dropdown-menu li a {
overflow: visible !important;
}
.hidden {
display: none;
}
#drop_overlay {

View file

@ -10,7 +10,15 @@
{% if data is none %}
<li class="nav-header">{{ entry }}</li>
{% else %}
<li {% if "_active" in data and data["_active"] %}class="active"{% endif %}><a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a></li>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<li id="{{ data._div }}_link"
{% if "data_bind" in data %} data-bind="{{ data.data_bind }}" {% elif "custom_bindings" in data and data["custom_bindings"] %} data-bind="allowBindings: true" {% endif %}
class="{% if loop.first %}active{% endif %} {% if "classes_link" in data %}{{ data.classes_link|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
{% if "styles_link" in data %} style="{{ data.styles_link|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
>
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
</li>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endif %}
{% endfor %}
</ul>
@ -19,7 +27,11 @@
{% for entry, data in settingsEntries %}
{% if data is not none %}
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<div class="tab-pane {% if "_active" in data and data["_active"] %}active{% endif %}" data-bind="{% if "data_bind" in data %}{{ data.data_bind }}{% else %}allowBindings: true{% endif %}" id="{{ data._div }}">
<div id="{{ data._div }}"
{% if "data_bind" in data %} data-bind="{{ data.data_bind }}" {% elif "custom_bindings" in data and data["custom_bindings"] %} data-bind="allowBindings: true" {% endif %}
class="tab-pane {% if loop.first %}active{% endif %} {% if classes_content in data %}{{ data.classes_content|join(' ') }}{% elif classes in data %}{{ data.classes|join(' ') }}{% endif %}"
{% if "styles_content" in data %} style="{{ data.styles_content|join(', ') }}" {% elif styles in data %} style="{{ data.styles|join(', ') }}" {% endif %}
>
{% include data._template ignore missing %}
</div>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- /ko -->{% endif %}

View file

@ -7,68 +7,8 @@
<link rel="apple-touch-icon" sizes="114x114" href="{{ url_for('static', filename='img/apple-touch-icon-114x114.png') }}">
<link rel="apple-touch-icon" sizes="144x144" href="{{ url_for('static', filename='img/apple-touch-icon-144x144.png') }}">
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-modal.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-slider.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/pnotify.min.css') }}" rel="stylesheet" media="screen">
{% if stylesheet == "less" %}
<link href="{{ url_for('static', filename='less/octoprint.less') }}" rel="stylesheet/less" type="text/css" media="screen">
<!-- Plugin files -->
{% for name, assets in assetPlugins.items() %}
{% if "less" in assets %}
{% for asset in assets["less"] %}
<link href="{{ url_for('plugin_assets', name=name, filename=asset) }}" rel="stylesheet/less" type="text/css" media="screen">
{% endfor %}
{% endif %}
{% endfor %}
<!-- /Plugin files -->
<script src="{{ url_for('static', filename='js/lib/less.min.js') }}" type="text/javascript"></script>
{% else %}
<link href="{{ url_for('static', filename='css/octoprint.css') }}" rel="stylesheet" type="text/css" media="screen">
<!-- Plugin files -->
{% for name, assets in assetPlugins.items() %}
{% if "css" in assets %}
{% for asset in assets["css"] %}
<link href="{{ url_for('plugin_assets', name=name, filename=asset) }}" rel="stylesheet" type="text/css" media="screen">
{% endfor %}
{% endif %}
{% endfor %}
<!-- /Plugin files -->
{% endif %}
<script lang="javascript">
var BASEURL = "{{ url_for('index') }}";
var API_BASEURL = BASEURL + "api/";
var PLUGIN_BASEURL = BASEURL + "plugin/";
var GCODE_WORKER = "{{ url_for('static', filename='gcodeviewer/js/Worker.js') }}";
var CONFIG_GCODEFILESPERPAGE = 5000;
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_LOGFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
var CONFIG_TEMPERATURE_GRAPH = {% if enableTemperatureGraph -%} true; {% else %} false; {%- endif %}
var CONFIG_GCODE_SIZE_THRESHOLD = {{ gcodeThreshold }};
var CONFIG_GCODE_MOBILE_SIZE_THRESHOLD = {{ gcodeMobileThreshold }};
var SOCKJS_URI = "{{ url_for('index') }}" + "sockjs";
var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %}
var UI_API_KEY = "{{ uiApiKey }}";
var VERSION = "{{ version }}";
var DISPLAY_VERSION = "{{ display_version }}";
var LOCALE = "{{ g.locale }}";
var ADDITIONAL_VIEWMODELS = [];
</script>
{% include 'stylesheets.jinja2' %}
{% include 'initscript.jinja2' %}
</head>
<body>
<div id="navbar" class="navbar navbar-static-top">
@ -76,10 +16,19 @@
<div class="container">
<a class="brand" href="#"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
<div class="nav-collapse">
<!-- Navbar -->
<ul class="nav pull-right">
{% include 'navbar/settings.jinja2' %}
{% include 'navbar/systemmenu.jinja2' %}
{% include 'navbar/login.jinja2' %}
{% for data in navbarEntries %}
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<li id="{{ data._div }}"
data-bind="{% if "data_bind" in data %}{{ data.data_bind }}{% else %}allowBindings: true{% endif %}"
{% if "additional_classes" in data %}class="{{ data.additional_classes|join(' ') }}"{% endif %}
{% if "style" in data %}style="{{ data.style }}"{% endif %}
>
{% include data._template ignore missing %}
</li>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endfor %}
</ul>
</div>
</div>
@ -87,52 +36,64 @@
</div>
<div class="container octoprint-container">
<div class="row">
<!-- Sidebar -->
<div class="accordion span4">
{% include 'sidebar/connection.jinja2' %}
{% include 'sidebar/state.jinja2' %}
{% include 'sidebar/files.jinja2' %}
{% if templatePlugins %}
{% for plugin_name, vars in templatePlugins.items() %}
{% include plugin_name+"_sidebar.jinja2" ignore missing %}
{% endfor %}
{% endif %}
{% for entry, data in sidebarEntries %}
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<div id="{{ data._div }}_wrapper"
class="accordion-group {% if "classes_wrapper" in data %}{{ data.classes_wrapper|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
data-bind="{% if "data_bind" in data %}{{ data.data_bind }}{% elif "custom_bindings" in data and data["custom_bindings"] %}allowBindings: true{% endif %}"
{% if "styles_wrapper" in data %} style="{{ data.styles_wrapper|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
>
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-target="#{{ data._div }}">
{% if "icon" in data %}<i class="icon-{{ data.icon }}"></i> {% endif %}{{ entry }}
</a>
{% if "header_addon" in data %}
{% include data.header_addon ignore missing %}
{% endif %}
</div>
<div id="{{ data._div }}"
class="accordion-body collapse in {% if "classes_content" in data %}{{ data.classes_content|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
{% if "styles_content" in data %} style="{{ data.styles_content|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}"{% endif %}
>
<div class="accordion-inner">
{% include data._template ignore missing %}
</div>
</div>
</div>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endfor %}
</div>
<!-- Tabs -->
<div class="span8 tabbable">
<ul class="nav nav-tabs" id="tabs">
<li class="active"><a href="#temp" data-toggle="tab">{{ _('Temperature') }}</a></li>
<li><a href="#control" data-toggle="tab">{{ _('Control') }}</a></li>
{% if enableGCodeVisualizer %}<li><a href="#gcode" data-toggle="tab">{{ _('GCode Viewer') }}</a></li>{% endif %}
<li><a href="#term" data-toggle="tab">{{ _('Terminal') }}</a></li>
{% if enableTimelapse %}<li><a href="#timelapse" data-toggle="tab">{{ _('Timelapse') }}</a></li>{% endif %}
{% if templatePlugins %}
{% for plugin_name, vars in templatePlugins.items() %}
{% if "_tab_entry" in vars %}
<li><a href="#tab_plugin_{{ plugin_name }}" data-toggle="tab">{{ vars._tab_entry }}</a></li>
{% endif %}
{% endfor %}
{% endif %}
{% for entry, data in tabEntries %}
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<li id="{{ data._div }}_link"
class="{% if loop.first %}active{% endif %} {% if "classes_link" in data %}{{ data.classes_link|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
data-bind="{% if "data_bind" in data %}{{ data.data_bind }}{% elif "custom_bindings" in data and data["custom_bindings"] %}allowBindings: true{% endif %}"
{% if "link_style" in data %} style="{{ data.link_style }}" {% elif "style" in data %} style="{{ data.style }}" {% endif %}
>
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
</li>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endfor %}
</ul>
<div class="tab-content">
{% include 'tabs/temperature.jinja2' %}
{% include 'tabs/control.jinja2' %}
{% include 'tabs/gcodeviewer.jinja2' %}
{% include 'tabs/terminal.jinja2' %}
{% include 'tabs/timelapse.jinja2' %}
{% if templatePlugins %}
{% for plugin_name, vars in templatePlugins.items() %}
{% if "_tab_entry" in vars %}
<div class="tab-pane" id="tab_plugin_{{ plugin_name }}" data-bind="allowBindings: {% if "_tab_allow_bindings" in vars and vars["_tab_allow_bindings"] %}true{% else %}false{% endif %}">
{% include plugin_name + "_tab.jinja2" ignore missing %}
</div>
{% endif %}
{% endfor %}
{% endif %}
{% for entry, data in tabEntries %}
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<div id="{{ data._div }}"
class="tab-pane{% if loop.first %} active{% endif %}{% if "additional_classes" in data %} {{ data.additional_classes|join(' ') }}{% endif %}"
data-bind="{% if "data_bind" in data %}{{ data.data_bind }}{% elif "custom_bindings" in data and data["custom_bindings"] %}allowBindings: true{% endif %}"
{% if "content_style" in data %} style="{{ data.content_style }}" {% elif "style" in data %} style="{{ data.style }}" {% endif %}
>
{% include data._template ignore missing %}
</div>
{% if "custom_bindings" in data and data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endfor %}
</div>
</div>
</div>
@ -161,85 +122,14 @@
{% include 'overlays/offline.jinja2' %}
<!-- End of overlays -->
<!-- Plugin template files -->
<!-- Generic plugin template files -->
{% if templatePlugins %}
{% for plugin_name in templatePlugins %}
{% for plugin_name in pluginNames %}
{% include plugin_name + ".jinja2" ignore missing %}
{% endfor %}
{% endif %}
<!-- End plugin template files -->
<!-- End of generic plugin template files -->
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore-min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.string.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.mapping-latest.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/babel.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.slimscroll.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.qrcode.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/moment-with-locales.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pusher.color.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/detectmobilebrowser.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/md5.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pnotify.min.js') }}"></script>
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/appearance.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/connection.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/control.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/firstrun.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/gcode.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/files.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/loginstate.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/navigation.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/printerstate.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/printerprofiles.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/settings.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/slicing.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/temperature.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/log.js') }}"></script>
<!-- Plugin files -->
{% for name, assets in assetPlugins.items() %}
{% if "js" in assets %}
{% for asset in assets["js"] %}
<script type="text/javascript" src="{{ url_for('plugin_assets', name=name, filename=asset) }}"></script>
{% endfor %}
{% endif %}
{% endfor %}
<!-- /Plugin files -->
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/main.js') }}"></script>
<!-- /Include OctoPrint files -->
<!-- Include i18n language files -->
{% if g.locale != 'en' %}
<script type="text/javascript" src="{{ url_for('static', filename='js/i18n/%s.js' % g.locale) }}"></script>
{% endif %}
<!-- /Include i18n language files -->
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>
{% include 'javascripts.jinja2' %}
</body>
</html>

View file

@ -0,0 +1,28 @@
<script lang="javascript">
var BASEURL = "{{ url_for('index') }}";
var API_BASEURL = BASEURL + "api/";
var PLUGIN_BASEURL = BASEURL + "plugin/";
var GCODE_WORKER = "{{ url_for('static', filename='gcodeviewer/js/Worker.js') }}";
var CONFIG_GCODEFILESPERPAGE = 5000;
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_LOGFILESPERPAGE = 10;
var CONFIG_USERSPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
var CONFIG_TEMPERATURE_GRAPH = {% if enableTemperatureGraph -%} true; {% else %} false; {%- endif %}
var CONFIG_GCODE_SIZE_THRESHOLD = {{ gcodeThreshold }};
var CONFIG_GCODE_MOBILE_SIZE_THRESHOLD = {{ gcodeMobileThreshold }};
var SOCKJS_URI = "{{ url_for('index') }}" + "sockjs";
var SOCKJS_DEBUG = {% if debug -%} true; {% else %} false; {%- endif %}
var UI_API_KEY = "{{ uiApiKey }}";
var VERSION = "{{ version }}";
var DISPLAY_VERSION = "{{ display_version }}";
var LOCALE = "{{ g.locale }}";
var ADDITIONAL_VIEWMODELS = [];
</script>

View file

@ -0,0 +1,72 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore-min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.string.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.mapping-latest.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/babel.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.slimscroll.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.qrcode.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/moment-with-locales.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pusher.color.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/detectmobilebrowser.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/md5.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pnotify.min.js') }}"></script>
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/appearance.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/connection.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/control.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/firstrun.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/gcode.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/files.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/loginstate.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/navigation.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/printerstate.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/printerprofiles.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/settings.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/slicing.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/temperature.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/log.js') }}"></script>
<!-- Plugin files -->
{% for name, assets in assetPlugins.items() %}
{% if "js" in assets %}
{% for asset in assets["js"] %}
<script type="text/javascript" src="{{ url_for('plugin_assets', name=name, filename=asset) }}"></script>
{% endfor %}
{% endif %}
{% endfor %}
<!-- /Plugin files -->
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/main.js') }}"></script>
<!-- /Include OctoPrint files -->
<!-- Include i18n language files -->
{% if g.locale != 'en' %}
<script type="text/javascript" src="{{ url_for('static', filename='js/i18n/%s.js' % g.locale) }}"></script>
{% endif %}
<!-- /Include i18n language files -->
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>

View file

@ -1,22 +1,20 @@
{% if enableAccessControl %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText">{{ _('Login') }}</span>
<b class="caret"></b>
</a>
<div id="login_dropdown_loggedout" style="padding: 15px" class="dropdown-menu" data-bind="css: {hide: loginState.loggedIn(), 'dropdown-menu': !loginState.loggedIn()}">
<label for="login_user">{{ _('Username') }}</label>
<input type="text" id="login_user" placeholder="{{ _('Username') }}" autocapitalize="none">
<label for="login_pass">{{ _('Password') }}</label>
<input type="password" id="login_pass" placeholder="{{ _('Password') }}">
<label class="checkbox">
<input type="checkbox" id="login_remember"> {{ _('Remember me') }}
</label>
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">{{ _('Login') }}</button>
</div>
<ul id="login_dropdown_loggedin" class="hide" data-bind="css: {hide: !loginState.loggedIn(), 'dropdown-menu': loginState.loggedIn()}">
<li><a href="#" id="change_password_button" data-bind="click: function() { users.showChangePasswordDialog(loginState.currentUser()); }">{{ _('Change Password') }}</a></li>
<li><a href="#" id="logout_button" data-bind="click: loginState.logout">{{ _('Logout') }}</a></li>
</ul>
</li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText">{{ _('Login') }}</span>
<b class="caret"></b>
</a>
<div id="login_dropdown_loggedout" style="padding: 15px" class="dropdown-menu" data-bind="css: {hide: loginState.loggedIn(), 'dropdown-menu': !loginState.loggedIn()}">
<label for="login_user">{{ _('Username') }}</label>
<input type="text" id="login_user" placeholder="{{ _('Username') }}" autocapitalize="none">
<label for="login_pass">{{ _('Password') }}</label>
<input type="password" id="login_pass" placeholder="{{ _('Password') }}">
<label class="checkbox">
<input type="checkbox" id="login_remember"> {{ _('Remember me') }}
</label>
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">{{ _('Login') }}</button>
</div>
<ul id="login_dropdown_loggedin" class="hide" data-bind="css: {hide: !loginState.loggedIn(), 'dropdown-menu': loginState.loggedIn()}">
<li><a href="#" id="change_password_button" data-bind="click: function() { users.showChangePasswordDialog(loginState.currentUser()); }">{{ _('Change Password') }}</a></li>
<li><a href="#" id="logout_button" data-bind="click: loginState.logout">{{ _('Logout') }}</a></li>
</ul>
{% endif %}

View file

@ -1,5 +1,3 @@
<li style="display: none;" data-bind="visible: loginState.isAdmin">
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog">
<i class="icon-wrench"></i> {{ _('Settings') }}
</a>
</li>
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog">
<i class="icon-wrench"></i> {{ _('Settings') }}
</a>

View file

@ -1,11 +1,9 @@
{% if enableSystemMenu %}
<li class="dropdown" style="display: none" data-bind="visible: loginState.isAdmin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-off"></i> {{ _('System') }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-bind="foreach: systemActions">
<li><a href="#" data-bind="click: $root.triggerAction, text: name"></a></li>
</ul>
</li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-off"></i> {{ _('System') }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-bind="foreach: systemActions">
<li><a href="#" data-bind="click: $root.triggerAction, text: name"></a></li>
</ul>
{% endif %}

View file

@ -1,22 +1,13 @@
<div class="accordion-group" data-bind="visible: loginState.isUser" id="connection_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-target="#connection"><i class="icon-signal"></i> {{ _('Connection') }}</a>
</div>
<div class="accordion-body collapse in" id="connection">
<div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Serial Port') }}</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Baudrate') }}</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label for="connection_printers" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Printer Profile') }}</label>
<select id="connection_printers" data-bind="options: printerOptions, optionsText: 'name', optionsValue: 'id', value: selectedPrinter, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label class="checkbox">
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Save connection settings') }}
</label>
<label class="checkbox">
<input type="checkbox" id="connection_autoconnect" data-bind="checked: settings.serial_autoconnect, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Auto-connect on server startup') }}
</label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser()">{{ _('Connect') }}</button>
</div>
</div>
</div>
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Serial Port') }}</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Baudrate') }}</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label for="connection_printers" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Printer Profile') }}</label>
<select id="connection_printers" data-bind="options: printerOptions, optionsText: 'name', optionsValue: 'id', value: selectedPrinter, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label class="checkbox">
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Save connection settings') }}
</label>
<label class="checkbox">
<input type="checkbox" id="connection_autoconnect" data-bind="checked: settings.serial_autoconnect, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Auto-connect on server startup') }}
</label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser()">{{ _('Connect') }}</button>

View file

@ -1,108 +1,65 @@
<div class="accordion-group" id="files_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> {{ _('Files') }}</a>
<form class="form-search">
<input type="text" class="input-block search-query" data-bind="value: searchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}">
</form>
<div class="gcode_files" data-bind="slimScrolledForeach: listHelper.paginatedItems">
<div class="entry" data-bind="attr: { id: $root.getEntryId($data) }, template: { name: $root.templateFor($data), data: $data }"></div>
<div class="settings-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-wrench"></span>
</a>
<ul class="dropdown-menu">
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }} ({{ _('ascending') }})</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> {{ _('Sort by upload date') }} ({{ _('descending') }})</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }} ({{ _('descending') }})</a></li>
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('machinecode'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'machinecode') ? 'visible' : 'hidden'}"></i> {{ _('Only show GCode files') }}</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('model'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'model') ? 'visible' : 'hidden'}"></i> {{ _('Only show STL files') }}</a></li>
{% if enableSdSupport %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> {{ _('Only show files stored locally') }}</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> {{ _('Only show files stored on SD') }}</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> {{ _('Hide successfully printed files') }}</a></li>
</ul>
<script type="text/html" id="files_template_machinecode">
<div class="title" data-bind="css: $root.getSuccessClass($data), style: { 'font-weight': $root.listHelper.isSelected($data) ? 'bold' : 'normal' }, text: name"></div>
<div class="uploaded">{{ _('Uploaded') }}: <span data-bind="text: formatTimeAgo(date)"></span></div>
<div class="size">{{ _('Size') }}: <span data-bind="text: formatSize(size)"></span></div>
<div class="additionalInfo hide" data-bind="html: $root.getAdditionalData($data)"></div>
<div class="btn-group action-buttons">
<div class="btn btn-mini toggleAdditionalData" data-bind="click: function() { if ($root.enableAdditionalData($data)) { $root.toggleAdditionalData($data); } else { return; } }, css: { disabled: !$root.enableAdditionalData($data) }"><i class="icon-chevron-down"></i></div>
<a class="btn btn-mini" data-bind="attr: {href: $root.downloadLink($data), css: {disabled: !$root.downloadLink($data)}}"><i class="icon-download-alt" title="{{ _('Download') }}"></i></a>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="{{ _('Remove') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data, false); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-folder-open" title="{{ _('Load') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-print" title="{{ _('Load and Print') }}"></i></div>
</div>
</script>
<script type="text/html" id="files_template_model">
<div class="title muted" data-bind="text: name"></div>
<div class="uploaded">{{ _('Uploaded') }}: <span data-bind="text: formatTimeAgo(date)"></span></div>
<div class="size">{{ _('Size') }}: <span data-bind="text: formatSize(size)"></span></div>
<div class="btn-group action-buttons">
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="{{ _('Remove') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSlicing($data)) { $root.sliceFile($data); } else { return; } }, css: {disabled: !$root.enableSlicing($data)}"><i class="icon-magic" title="{{ _('Slice') }}"></i></div>
</div>
</script>
<script type="text/html" id="files_template_folder">
<div class="title" data-bind="text: name"></div>
</script>
</div>
<div class="muted text-right">
<small>{{ _('Free') }}: <span data-bind="text: freeSpaceString"></span></small>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<div class="row-fluid upload-buttons">
{% if enableSdSupport %}
<div class="sd-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> {{ _('Initialize SD card') }}</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.refreshSdFiles(); }"><i class="icon-refresh"></i> {{ _('Refresh SD files') }}</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.releaseSdCard(); }"><i class="icon-eject"></i> {{ _('Release SD card') }}</a></li>
</ul>
</div>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>{{ _('Upload to SD') }}</span>
<input id="gcode_upload_sd" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser() && isSdReady()">
</span>
{% else %}
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
{% endif %}
</div>
<div class="accordion-body collapse in overflow_visible" id="files">
<div class="accordion-inner">
<form class="form-search">
<input type="text" class="input-block search-query" data-bind="value: searchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}">
</form>
<div class="gcode_files" data-bind="slimScrolledForeach: listHelper.paginatedItems">
<div class="entry" data-bind="attr: { id: $root.getEntryId($data) }, template: { name: $root.templateFor($data), data: $data }"></div>
<script type="text/html" id="files_template_machinecode">
<div class="title" data-bind="css: $root.getSuccessClass($data), style: { 'font-weight': $root.listHelper.isSelected($data) ? 'bold' : 'normal' }, text: name"></div>
<div class="uploaded">{{ _('Uploaded') }}: <span data-bind="text: formatTimeAgo(date)"></span></div>
<div class="size">{{ _('Size') }}: <span data-bind="text: formatSize(size)"></span></div>
<div class="additionalInfo hide" data-bind="html: $root.getAdditionalData($data)"></div>
<div class="btn-group action-buttons">
<div class="btn btn-mini toggleAdditionalData" data-bind="click: function() { if ($root.enableAdditionalData($data)) { $root.toggleAdditionalData($data); } else { return; } }, css: { disabled: !$root.enableAdditionalData($data) }"><i class="icon-chevron-down"></i></div>
<a class="btn btn-mini" data-bind="attr: {href: $root.downloadLink($data), css: {disabled: !$root.downloadLink($data)}}"><i class="icon-download-alt" title="{{ _('Download') }}"></i></a>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="{{ _('Remove') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data, false); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-folder-open" title="{{ _('Load') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-print" title="{{ _('Load and Print') }}"></i></div>
</div>
</script>
<script type="text/html" id="files_template_model">
<div class="title muted" data-bind="text: name"></div>
<div class="uploaded">{{ _('Uploaded') }}: <span data-bind="text: formatTimeAgo(date)"></span></div>
<div class="size">{{ _('Size') }}: <span data-bind="text: formatSize(size)"></span></div>
<div class="btn-group action-buttons">
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="{{ _('Remove') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSlicing($data)) { $root.sliceFile($data); } else { return; } }, css: {disabled: !$root.enableSlicing($data)}"><i class="icon-magic" title="{{ _('Slice') }}"></i></div>
</div>
</script>
<script type="text/html" id="files_template_folder">
<div class="title" data-bind="text: name"></div>
</script>
</div>
<div class="muted text-right">
<small>{{ _('Free') }}: <span data-bind="text: freeSpaceString"></span></small>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<div class="row-fluid upload-buttons">
{% if enableSdSupport %}
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>{{ _('Upload to SD') }}</span>
<input id="gcode_upload_sd" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser() && isSdReady()">
</span>
{% else %}
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
{% endif %}
</div>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
</div>
</div>
</div>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
</div>
</div>

View file

@ -0,0 +1,33 @@
<div class="settings-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-wrench"></span>
</a>
<ul class="dropdown-menu">
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }} ({{ _('ascending') }})</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> {{ _('Sort by upload date') }} ({{ _('descending') }})</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }} ({{ _('descending') }})</a></li>
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('machinecode'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'machinecode') ? 'visible' : 'hidden'}"></i> {{ _('Only show GCode files') }}</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('model'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'model') ? 'visible' : 'hidden'}"></i> {{ _('Only show STL files') }}</a></li>
{% if enableSdSupport %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> {{ _('Only show files stored locally') }}</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> {{ _('Only show files stored on SD') }}</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> {{ _('Hide successfully printed files') }}</a></li>
</ul>
</div>
{% if enableSdSupport %}
<div class="sd-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> {{ _('Initialize SD card') }}</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.refreshSdFiles(); }"><i class="icon-refresh"></i> {{ _('Refresh SD files') }}</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.releaseSdCard(); }"><i class="icon-eject"></i> {{ _('Release SD card') }}</a></li>
</ul>
</div>
{% endif %}

View file

@ -1,29 +1,20 @@
<div class="accordion-group" id="state_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> {{ _('State') }}</a>
</div>
<div class="accordion-body collapse in" id="state">
<div class="accordion-inner">
{{ _('Machine State') }}: <strong data-bind="text: stateString"></strong><br>
{{ _('File') }}: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
{{ _('Timelapse') }}: <strong data-bind="text: timelapseString"></strong><br>
<!-- ko foreach: filament -->
<span data-bind="text: 'Filament (' + name() + '): '"></span><strong data-bind="text: formatFilament(data())"></strong><br>
<!-- /ko -->
{{ _('Approx. Total Print Time') }}: <strong data-bind="text: estimatedPrintTimeString"></strong><br>
{{ _('Print Time') }}: <strong data-bind="text: printTimeString"></strong><br>
{{ _('Print Time Left') }}: <strong data-bind="text: printTimeLeftString"></strong><br>
{{ _('Printed') }}: <strong data-bind="text: byteString"></strong><br>
{{ _('Machine State') }}: <strong data-bind="text: stateString"></strong><br>
{{ _('File') }}: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
{{ _('Timelapse') }}: <strong data-bind="text: timelapseString"></strong><br>
<!-- ko foreach: filament -->
<span data-bind="text: 'Filament (' + name() + '): '"></span><strong data-bind="text: formatFilament(data())"></strong><br>
<!-- /ko -->
{{ _('Approx. Total Print Time') }}: <strong data-bind="text: estimatedPrintTimeString"></strong><br>
{{ _('Print Time') }}: <strong data-bind="text: printTimeString"></strong><br>
{{ _('Print Time Left') }}: <strong data-bind="text: printTimeLeftString"></strong><br>
{{ _('Printed') }}: <strong data-bind="text: byteString"></strong><br>
<div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
</div>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
<button class="btn btn-primary span4" data-bind="click: print, enable: isOperational() && isReady() && !isPrinting() && loginState.isUser(), css: {'btn-danger': isPaused()}, attr: {title: titlePrintButton}" id="job_print"><i class="icon-white" data-bind="css: {'icon-print': !isPaused(), 'icon-undo': isPaused()}"></i> <span data-bind="text: (isPaused() ? '{{ _('Restart') }}' : '{{ _('Print') }}')">{{ _('Print') }}</span></button>
<button class="btn span4" id="job_pause" data-bind="click: pause, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser(), css: {active: isPaused()}, attr: {title: titlePauseButton}"><i data-bind="css: {'icon-pause': !isPaused(), 'icon-play': isPaused()}"></i> <span data-bind="visible: !isPaused()">{{ _('Pause') }}</span><span data-bind="visible: isPaused()">{{ _('Resume') }}</span></button>
<button class="btn span4" id="job_cancel" data-bind="click: cancel, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser()" title="{{ _('Cancels the print job') }}"><i class="icon-stop"></i> {{ _('Cancel') }}</button>
</div>
</div>
</div>
<div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
</div>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
<button class="btn btn-primary span4" data-bind="click: print, enable: isOperational() && isReady() && !isPrinting() && loginState.isUser(), css: {'btn-danger': isPaused()}, attr: {title: titlePrintButton}" id="job_print"><i class="icon-white" data-bind="css: {'icon-print': !isPaused(), 'icon-undo': isPaused()}"></i> <span data-bind="text: (isPaused() ? '{{ _('Restart') }}' : '{{ _('Print') }}')">{{ _('Print') }}</span></button>
<button class="btn span4" id="job_pause" data-bind="click: pause, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser(), css: {active: isPaused()}, attr: {title: titlePauseButton}"><i data-bind="css: {'icon-pause': !isPaused(), 'icon-play': isPaused()}"></i> <span data-bind="visible: !isPaused()">{{ _('Pause') }}</span><span data-bind="visible: isPaused()">{{ _('Resume') }}</span></button>
<button class="btn span4" id="job_cancel" data-bind="click: cancel, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser()" title="{{ _('Cancels the print job') }}"><i class="icon-stop"></i> {{ _('Cancel') }}</button>
</div>

View file

@ -0,0 +1,34 @@
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-modal.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-slider.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/pnotify.min.css') }}" rel="stylesheet" media="screen">
{% if stylesheet == "less" %}
<link href="{{ url_for('static', filename='less/octoprint.less') }}" rel="stylesheet/less" type="text/css" media="screen">
<!-- Plugin files -->
{% for name, assets in assetPlugins.items() %}
{% if "less" in assets %}
{% for asset in assets["less"] %}
<link href="{{ url_for('plugin_assets', name=name, filename=asset) }}" rel="stylesheet/less" type="text/css" media="screen">
{% endfor %}
{% endif %}
{% endfor %}
<!-- /Plugin files -->
<script src="{{ url_for('static', filename='js/lib/less.min.js') }}" type="text/javascript"></script>
{% else %}
<link href="{{ url_for('static', filename='css/octoprint.css') }}" rel="stylesheet" type="text/css" media="screen">
<!-- Plugin files -->
{% for name, assets in assetPlugins.items() %}
{% if "css" in assets %}
{% for asset in assets["css"] %}
<link href="{{ url_for('plugin_assets', name=name, filename=asset) }}" rel="stylesheet" type="text/css" media="screen">
{% endfor %}
{% endif %}
{% endfor %}
<!-- /Plugin files -->
{% endif %}

View file

@ -1,133 +1,131 @@
<div class="tab-pane" id="control">
{% if webcamStream %}
<div id="webcam_container" tabindex="0" data-bind="event: { keydown: onKeyDown, mouseover: onMouseOver, mouseout: onMouseOut, focus: onFocus }">
<img id="webcam_image" data-bind="css: { flipH: settings.webcam_flipH(), flipV: settings.webcam_flipV() }">
<div class="keycontrol_overlay" data-bind="visible: showKeycontrols">
<div class="keycontrol_overlay_heading">{{ _("Keyboard controls active") }} <a href="#" data-bind="click: toggleKeycontrolHelp"><i data-bind="css: { 'icon-chevron-down': !keycontrolHelpActive(), 'icon-chevron-up': keycontrolHelpActive() }"></i></a></div>
<div data-bind="visible: keycontrolHelpActive">
<div class="keycontrol_overlay_column">
<kbd>&rarr;</kbd> / <kbd>&larr;</kbd>: {{ _("X Axis") }} +/-<br>
<kbd>&uarr;</kbd> / <kbd>&darr;</kbd>: {{ _("Y Axis") }} +/-<br>
<kbd>W</kbd>, <kbd>{{ _("Page&uarr;") }}</kbd> / <kbd>S</kbd>, <kbd>{{ _("Page&darr;") }}</kbd>: {{ _("Z Axis") }} +/-
</div>
<div class="keycontrol_overlay_column">
<kbd>Home</kbd>: {{ _("Home X/Y") }}<br>
<kbd>End</kbd>: {{ _("Home Z") }}<br>
<kbd>1</kbd>, <kbd>2</kbd>, <kbd>3</kbd>, <kbd>4</kbd>: {{ _("Stepsize") }} 0.1, 1, 10, 100mm
</div>
{% if webcamStream %}
<div id="webcam_container" tabindex="0" data-bind="event: { keydown: onKeyDown, mouseover: onMouseOver, mouseout: onMouseOut, focus: onFocus }">
<img id="webcam_image" data-bind="css: { flipH: settings.webcam_flipH(), flipV: settings.webcam_flipV() }">
<div class="keycontrol_overlay" data-bind="visible: showKeycontrols">
<div class="keycontrol_overlay_heading">{{ _("Keyboard controls active") }} <a href="#" data-bind="click: toggleKeycontrolHelp"><i data-bind="css: { 'icon-chevron-down': !keycontrolHelpActive(), 'icon-chevron-up': keycontrolHelpActive() }"></i></a></div>
<div data-bind="visible: keycontrolHelpActive">
<div class="keycontrol_overlay_column">
<kbd>&rarr;</kbd> / <kbd>&larr;</kbd>: {{ _("X Axis") }} +/-<br>
<kbd>&uarr;</kbd> / <kbd>&darr;</kbd>: {{ _("Y Axis") }} +/-<br>
<kbd>W</kbd>, <kbd>{{ _("Page&uarr;") }}</kbd> / <kbd>S</kbd>, <kbd>{{ _("Page&darr;") }}</kbd>: {{ _("Z Axis") }} +/-
</div>
<div class="keycontrol_overlay_column">
<kbd>Home</kbd>: {{ _("Home X/Y") }}<br>
<kbd>End</kbd>: {{ _("Home Z") }}<br>
<kbd>1</kbd>, <kbd>2</kbd>, <kbd>3</kbd>, <kbd>4</kbd>: {{ _("Stepsize") }} 0.1, 1, 10, 100mm
</div>
</div>
</div>
<div data-bind="visible: keycontrolPossible">
<small>{{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}</small>
</div>
{% endif %}
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<!-- XY jogging control panel -->
<div class="jog-panel">
<h1>X/Y</h1>
<div>
<button class="btn box" id="control-yinc" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('y',1) }"><i class="icon-arrow-up"></i></button>
</div>
<div>
<button class="btn box pull-left" id="control-xdec" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',-1) }"><i class="icon-arrow-left"></i></button>
<button class="btn box pull-left" id="control-xyhome" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand(['x', 'y']) }"><i class="icon-home"></i></button>
<button class="btn box pull-left" id="control-xinc" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',1) }"><i class="icon-arrow-right"></i></button>
</div>
<div>
<button class="btn box" id="control-ydec" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('y',-1) }"><i class="icon-arrow-down"></i></button>
</div>
</div>
<!-- Z jogging control panel -->
<div class="jog-panel">
<h1>Z</h1>
<div>
<button class="btn box" id="control-zinc" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',1) }"><i class="icon-arrow-up"></i></button>
</div>
<div>
<button class="btn box" id="control-zhome" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand(['z']) }"><i class="icon-home"></i></button>
</div>
<div>
<button class="btn box" id="control-zdec" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',-1) }"><i class="icon-arrow-down"></i></button>
</div>
</div>
<!-- Jog distance -->
<div class="distance">
<div class="btn-group" data-toggle="buttons-radio" id="jog_distance">
<button type="button" id="control-distance01" class="btn distance" data-distance="0.1" data-bind="enable: loginState.isUser()">0.1</button>
<button type="button" id="control-distance1" class="btn distance" data-distance="1" data-bind="enable: loginState.isUser()">1</button>
<button type="button" id="control-distance10" class="btn distance active" data-distance="10" data-bind="enable: loginState.isUser()">10</button>
<button type="button" id="control-distance100" class="btn distance" data-distance="100" data-bind="enable: loginState.isUser()">100</button>
</div>
</div>
</div>
<!-- Extrusion control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>Tool (E)</h1>
<div data-bind="visible: keycontrolPossible">
<small>{{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}</small>
</div>
{% endif %}
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<!-- XY jogging control panel -->
<div class="jog-panel">
<h1>X/Y</h1>
<div>
<div class="btn-group control-box">
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && !isPrinting() && !isPaused() && loginState.isUser()">
{{ _('Select Tool...') }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" data-bind="foreach: tools">
<li><a href="#" data-bind="click: $root.sendSelectToolCommand, text: name(), enable: $root.isOperational() && !$root.isPrinting() && !$root.isPaused() && $root.loginState.isUser()"></a></li>
</ul>
</div>
<div class="input-append control-box">
<input type="text" class="input-mini text-right" data-bind="value: extrusionAmount, enable: isOperational() && !isPrinting() && loginState.isUser(), attr: {placeholder: settings.printer_defaultExtrusionLength}">
<span class="add-on">mm</span>
</div>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">{{ _('Extrude') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">{{ _('Retract') }}</button>
<button class="btn box" id="control-yinc" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('y',1) }"><i class="icon-arrow-up"></i></button>
</div>
<div>
<button class="btn box pull-left" id="control-xdec" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',-1) }"><i class="icon-arrow-left"></i></button>
<button class="btn box pull-left" id="control-xyhome" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand(['x', 'y']) }"><i class="icon-home"></i></button>
<button class="btn box pull-left" id="control-xinc" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',1) }"><i class="icon-arrow-right"></i></button>
</div>
<div>
<button class="btn box" id="control-ydec" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('y',-1) }"><i class="icon-arrow-down"></i></button>
</div>
</div>
<!-- General control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>{{ _('General') }}</h1>
<!-- Z jogging control panel -->
<div class="jog-panel">
<h1>Z</h1>
<div>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }">{{ _('Motors off') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">{{ _('Fans on') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">{{ _('Fans off') }}</button>
<button class="btn box" id="control-zinc" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',1) }"><i class="icon-arrow-up"></i></button>
</div>
<div>
<button class="btn box" id="control-zhome" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand(['z']) }"><i class="icon-home"></i></button>
</div>
<div>
<button class="btn box" id="control-zdec" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',-1) }"><i class="icon-arrow-down"></i></button>
</div>
</div>
<!-- Container for custom controls -->
<div style="clear: both; display: none;" data-bind="visible: loginState.isUser, template: { name: $root.displayMode, foreach: controls }"></div>
<!-- Templates for custom controls -->
<script type="text/html" id="customControls_sectionTemplate">
<h1 data-bind="text: name"></h1>
<div data-bind="template: { name: $root.displayMode, foreach: children }"></div>
</script>
<script type="text/html" id="customControls_commandTemplate">
<form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_feedbackCommandTemplate">
<form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button> <span data-bind="text: output"></span>
</form>
</script>
<script type="text/html" id="customControls_feedbackTemplate">
<div>
<strong data-bind="text: name"></strong>: <span data-bind="text: output"></span>
<!-- Jog distance -->
<div class="distance">
<div class="btn-group" data-toggle="buttons-radio" id="jog_distance">
<button type="button" id="control-distance01" class="btn distance" data-distance="0.1" data-bind="enable: loginState.isUser()">0.1</button>
<button type="button" id="control-distance1" class="btn distance" data-distance="1" data-bind="enable: loginState.isUser()">1</button>
<button type="button" id="control-distance10" class="btn distance active" data-distance="10" data-bind="enable: loginState.isUser()">10</button>
<button type="button" id="control-distance100" class="btn distance" data-distance="100" data-bind="enable: loginState.isUser()">100</button>
</div>
</script>
<script type="text/html" id="customControls_parametricCommandTemplate">
<form class="form-inline">
<!-- ko foreach: input -->
<label data-bind="text: name"></label>
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
<!-- /ko -->
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls -->
</div>
</div>
<!-- Extrusion control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>Tool (E)</h1>
<div>
<div class="btn-group control-box">
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && !isPrinting() && !isPaused() && loginState.isUser()">
{{ _('Select Tool...') }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" data-bind="foreach: tools">
<li><a href="#" data-bind="click: $root.sendSelectToolCommand, text: name(), enable: $root.isOperational() && !$root.isPrinting() && !$root.isPaused() && $root.loginState.isUser()"></a></li>
</ul>
</div>
<div class="input-append control-box">
<input type="text" class="input-mini text-right" data-bind="value: extrusionAmount, enable: isOperational() && !isPrinting() && loginState.isUser(), attr: {placeholder: settings.printer_defaultExtrusionLength}">
<span class="add-on">mm</span>
</div>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">{{ _('Extrude') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">{{ _('Retract') }}</button>
</div>
</div>
<!-- General control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>{{ _('General') }}</h1>
<div>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }">{{ _('Motors off') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">{{ _('Fans on') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">{{ _('Fans off') }}</button>
</div>
</div>
<!-- Container for custom controls -->
<div style="clear: both; display: none;" data-bind="visible: loginState.isUser, template: { name: $root.displayMode, foreach: controls }"></div>
<!-- Templates for custom controls -->
<script type="text/html" id="customControls_sectionTemplate">
<h1 data-bind="text: name"></h1>
<div data-bind="template: { name: $root.displayMode, foreach: children }"></div>
</script>
<script type="text/html" id="customControls_commandTemplate">
<form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_feedbackCommandTemplate">
<form class="form-inline">
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button> <span data-bind="text: output"></span>
</form>
</script>
<script type="text/html" id="customControls_feedbackTemplate">
<div>
<strong data-bind="text: name"></strong>: <span data-bind="text: output"></span>
</div>
</script>
<script type="text/html" id="customControls_parametricCommandTemplate">
<form class="form-inline">
<!-- ko foreach: input -->
<label data-bind="text: name"></label>
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
<!-- /ko -->
<button class="btn" data-bind="text: name, enable: $root.isOperational() && $root.loginState.isUser(), click: function() { $root.sendCustomCommand($data) }"></button>
</form>
</script>
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
<!-- End of templates for custom controls -->

View file

@ -1,79 +1,77 @@
{% if enableGCodeVisualizer %}
<div class="tab-pane" id="gcode">
<div data-bind="visible: !waitForApproval()">
<input id="gcode_slider_layers" type="text">
<canvas id="gcode_canvas" width="568" height="568"></canvas>
<input id="gcode_slider_commands" type="text" style="width: 554px">
<div data-bind="visible: !waitForApproval()">
<input id="gcode_slider_layers" type="text">
<canvas id="gcode_canvas" width="568" height="568"></canvas>
<input id="gcode_slider_commands" type="text" style="width: 554px">
<div class="progress" >
<div class="bar" style="width: 0%;" data-bind="text: ui_progress_text, style: { width: ui_progress_percentage() + '%' }"></div>
</div>
<div class="row-fluid">
<div class="span7">
<h1>Model info</h1>
<p data-bind="html: ui_modelInfo"></p>
<h1>Layer info</h1>
<p data-bind="html: ui_layerInfo"></p>
</div>
<div class="span5">
<h1>Renderer options</h1>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_syncProgress">{{ _('Sync with job progress') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_centerViewport">{{ _('Center viewport on model') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_zoomOnModel">{{ _('Zoom in on model') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showMoves">{{ _('Show moves') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showRetracts">{{ _('Show retracts') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showPrevious">{{ _('Also show previous layer') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showNext">{{ _('Also show next layer') }}
</label>
</p>
<p>
<button class="btn btn-block" data-bind="click: reload, enable: enableReload">{{ _('Reload') }}</button>
</p>
</div>
</div>
<div class="progress" >
<div class="bar" style="width: 0%;" data-bind="text: ui_progress_text, style: { width: ui_progress_percentage() + '%' }"></div>
</div>
<div data-bind="visible: waitForApproval">
<h1>Warning</h1>
<p>
You've selected <strong data-bind="text: selectedFile.name"></strong> for printing which has a size of
<strong data-bind="text: formatSize(selectedFile.size())"></strong>. Depending on your machine this
might be too large for rendering and cause your browser to become unresponsive or crash.
</p>
<div class="row-fluid">
<div class="span7">
<h1>Model info</h1>
<p data-bind="html: ui_modelInfo"></p>
<p>
Are you sure you want to visualize this file nevertheless?
</p>
<h1>Layer info</h1>
<p data-bind="html: ui_layerInfo"></p>
</div>
<div class="span5">
<h1>Renderer options</h1>
<button class="btn btn-warning btn-block" data-bind="click: approveLargeFile">
Yes, please visualize <span data-bind="text: selectedFile.name"></span> regardless of its size
</button>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_syncProgress">{{ _('Sync with job progress') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_centerViewport">{{ _('Center viewport on model') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_zoomOnModel">{{ _('Zoom in on model') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showMoves">{{ _('Show moves') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showRetracts">{{ _('Show retracts') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showPrevious">{{ _('Also show previous layer') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showNext">{{ _('Also show next layer') }}
</label>
</p>
<p>
<button class="btn btn-block" data-bind="click: reload, enable: enableReload">{{ _('Reload') }}</button>
</p>
</div>
</div>
</div>
<div data-bind="visible: waitForApproval">
<h1>Warning</h1>
<p>
You've selected <strong data-bind="text: selectedFile.name"></strong> for printing which has a size of
<strong data-bind="text: formatSize(selectedFile.size())"></strong>. Depending on your machine this
might be too large for rendering and cause your browser to become unresponsive or crash.
</p>
<p>
Are you sure you want to visualize this file nevertheless?
</p>
<button class="btn btn-warning btn-block" data-bind="click: approveLargeFile">
Yes, please visualize <span data-bind="text: selectedFile.name"></span> regardless of its size
</button>
</div>
{% endif %}

View file

@ -1,57 +1,55 @@
<div class="tab-pane active" id="temp">
{% if enableTemperatureGraph %}
<div class="row" style="padding-left: 20px">
<div id="temperature-graph"></div>
</div>
{% endif %}
<div class="row-fluid">
<table class="table table-bordered table-hover" style="table-layout: fixed; width: 100%; margin-top: 20px">
<tr>
<th style="width: 18%"></th>
<th style="width: 12%; text-align: right">{{ _('Actual') }}</th>
<th style="width: 35%">{{ _('Target') }}</th>
<th style="width: 35%">{{ _('Offset') }}</th>
</tr>
<!-- ko foreach: tools -->
<tr data-bind="template: { name: 'temprow-template' }"></tr>
<!-- /ko -->
<tr data-bind="template: { name: 'temprow-template', data: bedTemp }, visible: hasBed"></tr>
</table>
<script type="text/html" id="temprow-template">
<th style="vertical-align: middle" data-bind="text: name"></th>
<td style="text-align: right; vertical-align: middle" data-bind="html: formatTemperature(actual())"></td>
<td style="vertical-align: middle; overflow: visible">
<div class="input-append">
<input type="text" class="input-mini text-right tempInput" data-bind="attr: {placeholder: cleanTemperature(target()) }, value: newTarget, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'target', $data);} }">
<span class="add-on">&deg;C</span>
<div class="btn-group">
<button type="submit" data-bind="click: $parent.setTarget, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">{{ _('Set') }}</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: $root.isOperational() && $root.loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: $root.temperature_profiles -->
<li>
<a href="#" data-bind="click: function() {$root.setTargetFromProfile($parent, $data);}, text: 'Set ' + name + ' (' + ($parent.key() == 'bed' ? bed : extruder) + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: $root.setTargetToZero">{{ _('Off') }}</a>
</li>
</ul>
</div>
</div>
</td>
<td style="vertical-align: middle">
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right tempInput" data-bind="attr: {placeholder: offset}, value: newOffset, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'offset', $data);} }">
<span class="add-on">&deg;C</span>
<button type="submit" data-bind="click: $root.setOffset, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">{{ _('Set') }}</button>
</div>
</td>
</script>
{% if enableTemperatureGraph %}
<div class="row" style="padding-left: 20px">
<div id="temperature-graph"></div>
</div>
{% endif %}
<div class="row-fluid">
<table class="table table-bordered table-hover" style="table-layout: fixed; width: 100%; margin-top: 20px">
<tr>
<th style="width: 18%"></th>
<th style="width: 12%; text-align: right">{{ _('Actual') }}</th>
<th style="width: 35%">{{ _('Target') }}</th>
<th style="width: 35%">{{ _('Offset') }}</th>
</tr>
<!-- ko foreach: tools -->
<tr data-bind="template: { name: 'temprow-template' }"></tr>
<!-- /ko -->
<tr data-bind="template: { name: 'temprow-template', data: bedTemp }, visible: hasBed"></tr>
</table>
<script type="text/html" id="temprow-template">
<th style="vertical-align: middle" data-bind="text: name"></th>
<td style="text-align: right; vertical-align: middle" data-bind="html: formatTemperature(actual())"></td>
<td style="vertical-align: middle; overflow: visible">
<div class="input-append">
<input type="text" class="input-mini text-right tempInput" data-bind="attr: {placeholder: cleanTemperature(target()) }, value: newTarget, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'target', $data);} }">
<span class="add-on">&deg;C</span>
<div class="btn-group">
<button type="submit" data-bind="click: $parent.setTarget, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">{{ _('Set') }}</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: $root.isOperational() && $root.loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: $root.temperature_profiles -->
<li>
<a href="#" data-bind="click: function() {$root.setTargetFromProfile($parent, $data);}, text: 'Set ' + name + ' (' + ($parent.key() == 'bed' ? bed : extruder) + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: $root.setTargetToZero">{{ _('Off') }}</a>
</li>
</ul>
</div>
</div>
</td>
<td style="vertical-align: middle">
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right tempInput" data-bind="attr: {placeholder: offset}, value: newOffset, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'offset', $data);} }">
<span class="add-on">&deg;C</span>
<button type="submit" data-bind="click: $root.setOffset, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">{{ _('Set') }}</button>
</div>
</td>
</script>
</div>

View file

@ -1,16 +1,14 @@
<div class="tab-pane" id="term">
<pre id="terminal-output" class="pre-scrollable"></pre>
<pre id="terminal-output" class="pre-scrollable"></pre>
<label class="checkbox">
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> {{ _('Autoscroll') }}
</label>
<div data-bind="foreach: filters">
<label class="checkbox">
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> {{ _('Autoscroll') }}
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
</label>
<div data-bind="foreach: filters">
<label class="checkbox">
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
</label>
</div>
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
</div>
</div>
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
</div>

View file

@ -1,71 +1,69 @@
{% if enableTimelapse %}
<div class="tab-pane" id="timelapse">
<div style="display: none;" data-bind="visible: loginState.isUser">
<h1>{{ _('Timelapse Configuration') }}</h1>
<div style="display: none;" data-bind="visible: loginState.isUser">
<h1>{{ _('Timelapse Configuration') }}</h1>
<label for="webcam_timelapse_mode">{{ _('Timelapse Mode') }}</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">{{ _('Off') }}</option>
<option value="zchange">{{ _('On Z Change') }}</option>
<option value="timed">{{ _('Timed') }}</option>
</select>
<label for="webcam_timelapse_mode">{{ _('Timelapse Mode') }}</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">{{ _('Off') }}</option>
<option value="zchange">{{ _('On Z Change') }}</option>
<option value="timed">{{ _('Timed') }}</option>
</select>
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">{{ _('sec') }}</span>
</div>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
<label for="webcam_timelapse_interval">{{ _('Interval') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('sec') }}</span>
</div>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
<label for="webcam_timelapse_interval">{{ _('Interval') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('sec') }}</span>
</div>
</div>
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist"> {{ _('Save as default') }}
</label>
</div>
<div>
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">{{ _('Save config') }}</button>
</div>
</div>
<h1>{{ _('Finished Timelapses') }}</h1>
<div class="pull-right">
<small>{{ _('Sort by') }}: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">{{ _('Name') }} ({{ _('ascending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('creation'); }">{{ _('Creation date') }} ({{ _('descending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">{{ _('Size') }} ({{ _('descending') }})</a></small>
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist"> {{ _('Save as default') }}
</label>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
<thead>
<tr>
<th class="timelapse_files_name">{{ _('Name') }}</th>
<th class="timelapse_files_size">{{ _('Size') }}</th>
<th class="timelapse_files_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: listHelper.pages">
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
<div>
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">{{ _('Save config') }}</button>
</div>
</div>
<h1>{{ _('Finished Timelapses') }}</h1>
<div class="pull-right">
<small>{{ _('Sort by') }}: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">{{ _('Name') }} ({{ _('ascending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('creation'); }">{{ _('Creation date') }} ({{ _('descending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">{{ _('Size') }} ({{ _('descending') }})</a></small>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
<thead>
<tr>
<th class="timelapse_files_name">{{ _('Name') }}</th>
<th class="timelapse_files_size">{{ _('Size') }}</th>
<th class="timelapse_files_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
<td class="timelapse_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a>&nbsp;|&nbsp;<a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: listHelper.pages">
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
{% endif %}

View file

@ -21,16 +21,16 @@ class PluginTestCase(unittest.TestCase):
def test_plugin_loading(self):
self.assertEquals(4, len(self.plugin_manager.plugins))
self.assertEquals(1, len(self.plugin_manager.plugin_hooks))
self.assertEquals(2, len(self.plugin_manager.plugin_implementations))
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type))
self.assertTrue("octoprint.core.startup" in self.plugin_manager.plugin_hooks)
self.assertEquals(1, len(self.plugin_manager.plugin_hooks["octoprint.core.startup"]))
self.assertTrue(octoprint.plugin.StartupPlugin in self.plugin_manager.plugin_implementations)
self.assertEquals(2, len(self.plugin_manager.plugin_implementations[octoprint.plugin.StartupPlugin]))
self.assertTrue(octoprint.plugin.StartupPlugin in self.plugin_manager.plugin_implementations_by_type)
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.StartupPlugin]))
self.assertTrue(octoprint.plugin.SettingsPlugin in self.plugin_manager.plugin_implementations)
self.assertEquals(2, len(self.plugin_manager.plugin_implementations[octoprint.plugin.SettingsPlugin]))
self.assertTrue(octoprint.plugin.SettingsPlugin in self.plugin_manager.plugin_implementations_by_type)
self.assertEquals(2, len(self.plugin_manager.plugin_implementations_by_type[octoprint.plugin.SettingsPlugin]))
def test_get_plugin(self):
plugin = self.plugin_manager.get_plugin("hook_plugin")