Finalized plugin tutorial (for now)
This commit is contained in:
parent
00760ef1b3
commit
4d5443ed6b
1 changed files with 447 additions and 2 deletions
|
|
@ -4,11 +4,13 @@ Getting Started
|
|||
===============
|
||||
|
||||
Over the course of this little tutorial we'll build a full fledged, installable OctoPrint plugin that displays "Hello World!"
|
||||
at various places throughout OctoPrint.
|
||||
at some locations throughout OctoPrint and also offers some other basic functionality to give you an idea of what
|
||||
you can achieve with OctoPrint's plugin system.
|
||||
|
||||
We'll start at the most basic form a plugin can take - just a couple of simple lines of Python code:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -37,6 +39,7 @@ Apart from being discovered by OctoPrint, our plugin does nothing yet. We want t
|
|||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 4-8,13
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -100,6 +103,7 @@ Then edit the configuration in the ``setup.py`` file to mirror our own "Hello Wo
|
|||
look something like this:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
plugin_identifier = "helloworld"
|
||||
plugin_name = "OctoPrint-HelloWorld"
|
||||
|
|
@ -137,6 +141,7 @@ Something is still a bit ugly though. Take a look into ``__init__.py`` and ``set
|
|||
of information now defined twice:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
# __init__.py:
|
||||
__plugin_name__ = "Hello World"
|
||||
|
|
@ -153,6 +158,7 @@ within ``setup.py``! So, we don't really need to define all this data twice. Rem
|
|||
and ``__plugin_description__``:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -174,6 +180,7 @@ Our "Hello World" Plugin still gets detected fine, but it's now listed under the
|
|||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 10
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -218,6 +225,7 @@ add the :class:`TemplatePlugin` to our ``HelloWorldPlugin`` class:
|
|||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 7
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -236,6 +244,7 @@ Next, we'll create a sub folder ``templates`` underneath our ``octoprint_hellowo
|
|||
``helloworld_navbar.jinja2`` like so:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
|
||||
<a href="https://en.wikipedia.org/wiki/Hello_world">Hello World!</a>
|
||||
|
||||
|
|
@ -279,6 +288,7 @@ Let's take a look at how all that would look in our plugin's ``__init__.py``:
|
|||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 8, 10, 12-13
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -312,6 +322,7 @@ Adjust your plugin's ``__init__.py`` like this:
|
|||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 15-16
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -336,6 +347,7 @@ Adjust your plugin's ``__init__.py`` like this:
|
|||
Also adjust your plugin's ``templates/helloworld_navbar.jinja2`` like this:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
|
||||
<a href="{{ plugin_helloworld_url|escape }}">Hello World!</a>
|
||||
|
||||
|
|
@ -372,6 +384,7 @@ So in your plugin's ``templates`` folder create a new file ``helloworld_settings
|
|||
into it:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
|
|
@ -393,6 +406,7 @@ Now adjust your ``templates/helloworld_navbar.jinja2`` file to use a ``data-bind
|
|||
settings view model into the ``href`` attribute of the link tag:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
|
||||
<a href="#" data-bind="attr: {href: settings.settings.plugins.helloworld.url}">Hello World!</a>
|
||||
|
||||
|
|
@ -415,6 +429,7 @@ again since we don't use that anymore:
|
|||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 15-19
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
|
@ -458,4 +473,434 @@ Nice! Edit the value, then click "Save". Your link in the navigation bar should
|
|||
not storing a copy of the settings data while it is being edited, which might be changed in the future to
|
||||
prevent this unexpected behaviour from occurring.
|
||||
|
||||
Congratulations, you've just made your Plugin configurable :)
|
||||
Congratulations, you've just made your Plugin configurable :)
|
||||
|
||||
More frontend fun: Adding custom javascript to your frontend components
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
In the previous section we set that ``custom_bindings`` parameter to ``False`` since we wanted OctoPrint to bind the
|
||||
``SettingsViewModel`` to our settings dialog and the ``NavigationViewModel`` to our entry in the nav bar.
|
||||
|
||||
But what if we want to define our own, with more functionality that is already available? Let's take a look. We'll now
|
||||
add an additional UI component to our OctoPrint interface, a custom tab. It will act as a little internal web browser,
|
||||
showing the website behind the URL from the settings in an IFrame but also allowing the user to load a different URL
|
||||
without having to change the settings.
|
||||
|
||||
First let us create the Jinja2 template for our tab. In your plugin's ``templates`` folder create a new file
|
||||
``helloworld_tab.jinja2`` like so:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-xxlarge" data-bind="value: newUrl">
|
||||
<button class="btn btn-primary" data-bind="click: goToUrl">{{ _('Go') }}</button>
|
||||
</div>
|
||||
|
||||
|
||||
<iframe data-bind="attr: {src: currentUrl}" style="width: 100%; height: 600px; border: 1px solid #808080"></iframe>
|
||||
|
||||
Then we create a new folder in your plugin's root called ``static`` and within that folder another folder by the name of
|
||||
``js``. Finally, within that folder create a file ``helloworld.js``. Our plugin's folder structure should now
|
||||
look like this::
|
||||
|
||||
octoprint_helloworld/
|
||||
static/
|
||||
js/
|
||||
helloworld.js
|
||||
templates/
|
||||
helloworld_navbar.jinja2
|
||||
helloworld_settings.jinja2
|
||||
helloworld_tab.jinja2
|
||||
__init__.py
|
||||
README.md
|
||||
requirements.txt
|
||||
setup.py
|
||||
|
||||
We need to tell OctoPrint about this new static asset so that it will properly inject it into the page. For this we
|
||||
just need to subclass :class:`~octoprint.plugin.AssetPlugin` and override its method :func:`~octoprint.plugin.AssetPlugin.get_assets`
|
||||
like so:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 9,22-25
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.AssetPlugin):
|
||||
def on_after_startup(self):
|
||||
self._logger.info("Hello World! (more: %s)" % self._settings.get(["url"]))
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
|
||||
|
||||
def get_template_configs(self):
|
||||
return [
|
||||
dict(type="navbar", custom_bindings=False),
|
||||
dict(type="settings", custom_bindings=False)
|
||||
]
|
||||
|
||||
def get_assets(self):
|
||||
return dict(
|
||||
js=["js/helloworld.js"]
|
||||
)
|
||||
|
||||
__plugin_name__ = "Hello World"
|
||||
__plugin_implementations__ = [HelloWorldPlugin()]
|
||||
|
||||
Note how we did not add another entry to the return value of :func:`~octoprint.plugin.TemplatePlugin.get_template_configs`.
|
||||
Remember how we only added those since we wanted OctoPrint to use existing bindings on our navigation bar and settings
|
||||
menu entries? We don't want this this time, and we named our tab template such that OctoPrint will pick it up automatically
|
||||
so we don't have to do anything here.
|
||||
|
||||
Then we'll create our custom `Knockout <http://knockoutjs.com/documentation/introduction.html>`_ view model in ``helloworld.js``
|
||||
like so:
|
||||
|
||||
.. code-block:: javascript
|
||||
:linenos:
|
||||
|
||||
$(function() {
|
||||
function HelloWorldViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.settings = parameters[0];
|
||||
|
||||
// this will hold the URL currently displayed by the iframe
|
||||
self.currentUrl = ko.observable();
|
||||
|
||||
// this will hold the URL entered in the text field
|
||||
self.newUrl = ko.observable();
|
||||
|
||||
// this will be called when the user clicks the "Go" button and set the iframe's URL to
|
||||
// the entered URL
|
||||
self.goToUrl = function() {
|
||||
self.currentUrl(self.newUrl());
|
||||
};
|
||||
|
||||
// This will get called before the HelloWorldViewModel gets bound to the DOM, but after its
|
||||
// dependencies have already been initialized. It is especially guaranteed that this method
|
||||
// gets called _after_ the settings have been retrieved from the OctoPrint backend and thus
|
||||
// the SettingsViewModel been properly populated.
|
||||
self.onBeforeBinding = function() {
|
||||
self.newUrl(self.settings.settings.plugins.helloworld.url());
|
||||
self.goToUrl();
|
||||
}
|
||||
}
|
||||
|
||||
// This is how our plugin registers itself with the application, by adding some configuration
|
||||
// information to the global variable ADDITIONAL_VIEWMODELS
|
||||
ADDITIONAL_VIEWMODELS.push([
|
||||
// This is the constructor to call for instantiating the plugin
|
||||
HelloWorldViewModel,
|
||||
|
||||
// This is a list of dependencies to inject into the plugin, the order which you request
|
||||
// here is the order in which the dependencies will be injected into your view model upon
|
||||
// instantiation via the parameters argument
|
||||
["settingsViewModel"],
|
||||
|
||||
// Finally, this is the list of all elements we want this view model to be bound to.
|
||||
[document.getElementById("tab_plugin_helloworld")]
|
||||
]);
|
||||
});
|
||||
|
||||
Take a close look at lines 29 to 40. This is how our plugin tells OctoPrint about our new view model, how to
|
||||
instantiate it, which dependencies to inject and to which elements in the final page to bind. Since we want to access
|
||||
the URL from the settings of our plugin, we'll have OctoPrint inject the ``SettingsViewModel`` into our own view model,
|
||||
which is registered within OctoPrint under the name ``settingsViewModel``. We'll only bind to our custom tab
|
||||
for now, which OctoPrint will make available in a container with the id ``tab_plugin_helloworld`` (unless otherwise
|
||||
configured).
|
||||
|
||||
Our view model defines two observables: ``newUrl``, which we bound to the input field in our template, and ``currentUrl``
|
||||
which we bound to the ``src`` attribute of the "browser iframe" in our template. There's also a function ``goToUrl``
|
||||
which we bound to the click event of the "Go" button in our template.
|
||||
|
||||
Restart OctoPrint and shift-reload the browser. You should see a shiny new "Hello World" tab right at the end of the
|
||||
tab bar. Click on it!
|
||||
|
||||
.. _fig-plugins-gettingstarted-helloworld_tab:
|
||||
.. figure:: ../images/plugins_gettingstarted_helloworld_tab.png
|
||||
:align: center
|
||||
:alt: Our "Hello World" tab in action
|
||||
|
||||
The desktop version of that article looks a bit squished in there, so let's enter ``https://de.m.wikipedia.org/wiki/Hallo-Welt-Programm``
|
||||
into the input field and click the "Go" button. The page inside the iframe should be replaced with the mobile version
|
||||
of the same article.
|
||||
|
||||
Style matters: Injecting custom CSS into the page
|
||||
-------------------------------------------------
|
||||
|
||||
So it appears that this stuff is working great already. Only one thing is a bit ugly, let's take another look at
|
||||
our ``helloworld_tab.jinja2``:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
:emphasize-lines: 6
|
||||
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-xxlarge" data-bind="value: newUrl">
|
||||
<button class="btn btn-primary" data-bind="click: goToUrl">{{ _('Go') }}</button>
|
||||
</div>
|
||||
|
||||
<iframe data-bind="attr: {src: currentUrl}" style="width: 100%; height: 600px; border: 1px solid #808080"></iframe>
|
||||
|
||||
We hardcoded some ``style`` on our ``iframe`` in line 6, to make it look a bit better. It would be nicer if that was actually
|
||||
located inside a stylesheet instead of directly inside our HTML template. Of course that's no problem, we'll just
|
||||
add a CSS file to our plugin's provided static assets.
|
||||
|
||||
First we'll create a new folder within our plugin's ``static`` folder called ``css`` and within that folders a file
|
||||
``helloworld.css``. Our plugin's file structure should now look like this::
|
||||
|
||||
octoprint_helloworld/
|
||||
static/
|
||||
css/
|
||||
helloworld.css
|
||||
js/
|
||||
helloworld.js
|
||||
templates/
|
||||
helloworld_navbar.jinja2
|
||||
helloworld_settings.jinja2
|
||||
helloworld_tab.jinja2
|
||||
__init__.py
|
||||
README.md
|
||||
requirements.txt
|
||||
setup.py
|
||||
|
||||
|
||||
Put something like the following into ``helloworld.css``:
|
||||
|
||||
.. code-block:: css
|
||||
:linenos:
|
||||
|
||||
#tab_plugin_helloworld {
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #808080;
|
||||
}
|
||||
}
|
||||
|
||||
Don't forget to remove the ``style`` attribute from the ``iframe`` tag in ``helloworld_tab.jinja2``:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
:emphasize-lines: 6
|
||||
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-xxlarge" data-bind="value: newUrl">
|
||||
<button class="btn btn-primary" data-bind="click: goToUrl">{{ _('Go') }}</button>
|
||||
</div>
|
||||
|
||||
<iframe data-bind="attr: {src: currentUrl}"></iframe>
|
||||
|
||||
Then adjust our plugin's ``__init__.py`` so that the :func:`~octoprint.plugin.AssetPlugin.get_assets` method returns
|
||||
a reference to our CSS file:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 26
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.AssetPlugin):
|
||||
|
||||
def on_after_startup(self):
|
||||
self._logger.info("Hello World! (more: %s)" % self._settings.get(["url"]))
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
|
||||
|
||||
def get_template_configs(self):
|
||||
return [
|
||||
dict(type="navbar", custom_bindings=False),
|
||||
dict(type="settings", custom_bindings=False)
|
||||
]
|
||||
|
||||
def get_assets(self):
|
||||
return dict(
|
||||
js=["js/helloworld.js"],
|
||||
css=["css/helloworld.css"]
|
||||
)
|
||||
|
||||
__plugin_name__ = "Hello World"
|
||||
__plugin_implementations__ = [HelloWorldPlugin()]
|
||||
|
||||
Restart OctoPrint, shift-reload your browser and take a look. Everything should still look like before, but now
|
||||
OctoPrint linked to our stylesheet and the style information for the ``iframe`` is taken from that instead of
|
||||
hardcoded in our template. Way better!
|
||||
|
||||
Now, if you had something more complicated than just the couple of line of CSS we used here, you might want to use
|
||||
something like LESS for generating your CSS from. If you use `LESS <http://lesscss.org/>`_, which is what OctoPrint
|
||||
uses for that purpose, you can even put OctoPrint into a mode where it directly uses your LESS files instead of the
|
||||
generated CSS files (and compiles them on the fly in your browser using `lessjs <http://lesscss.org/#client-side-usage>`_),
|
||||
which makes development so much easier. Let's try that, so you know how it works for future bigger projects.
|
||||
|
||||
Add another folder to our ``static`` folder called ``less`` and within that create a file ``helloworld.less``. Put
|
||||
into that the same content as into our CSS file. Compile that LESS file to CSS [#f1]_, overwriting our old ``helloworld.css``
|
||||
in the process. The folder structure of our plugin should now look like this::
|
||||
|
||||
octoprint_helloworld/
|
||||
static/
|
||||
css/
|
||||
helloworld.css
|
||||
js/
|
||||
helloworld.js
|
||||
less/
|
||||
helloworld.less
|
||||
templates/
|
||||
helloworld_navbar.jinja2
|
||||
helloworld_settings.jinja2
|
||||
helloworld_tab.jinja2
|
||||
__init__.py
|
||||
README.md
|
||||
requirements.txt
|
||||
setup.py
|
||||
|
||||
|
||||
Then adjust our returned assets to include our LESS file as well:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 27
|
||||
:linenos:
|
||||
|
||||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
|
||||
octoprint.plugin.TemplatePlugin,
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.AssetPlugin):
|
||||
|
||||
def on_after_startup(self):
|
||||
self._logger.info("Hello World! (more: %s)" % self._settings.get(["url"]))
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
|
||||
|
||||
def get_template_configs(self):
|
||||
return [
|
||||
dict(type="navbar", custom_bindings=False),
|
||||
dict(type="settings", custom_bindings=False)
|
||||
]
|
||||
|
||||
def get_assets(self):
|
||||
return dict(
|
||||
js=["js/helloworld.js"],
|
||||
css=["css/helloworld.css"],
|
||||
less=["less/helloworld.less"]
|
||||
)
|
||||
|
||||
__plugin_name__ = "Hello World"
|
||||
__plugin_implementations__ = [HelloWorldPlugin()]
|
||||
|
||||
|
||||
and enable LESS mode by adjusting one of OctoPrint's ``devel`` flags via the ``config.yaml`` file:
|
||||
|
||||
.. code-block:: yaml
|
||||
:emphasize-lines: 2-3
|
||||
|
||||
# [...]
|
||||
devel:
|
||||
stylesheet: less
|
||||
# [...]
|
||||
|
||||
Restart OctoPrint and shift-reload. Your "Hello World" tab should still look like before. Take a look at the site's
|
||||
source code. In the ``head`` section of the page you'll see that instead of your ``helloworld.css`` OctoPrint now
|
||||
embedded the ``helloworld.less`` file instead:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
:emphasize-lines: 7
|
||||
|
||||
<head>
|
||||
<!-- [...] -->
|
||||
<link href="/static/less/octoprint.less" rel="stylesheet/less" type="text/css" media="screen">
|
||||
|
||||
<!-- Plugin files -->
|
||||
<!-- [...] -->
|
||||
<link href="/plugin_assets/helloworld/less/helloworld.less" rel="stylesheet/less" type="text/css" media="screen">
|
||||
<!-- [...] -->
|
||||
<!-- /Plugin files -->
|
||||
|
||||
<script src="/static/js/lib/less.min.js" type="text/javascript"></script>
|
||||
<!-- [...] -->
|
||||
</head>
|
||||
|
||||
Switch your config back to CSS mode by either removing the ``stylesheet`` setting we just added to ``config.yaml`` or
|
||||
setting it to ``css``, e.g.
|
||||
|
||||
.. code-block:: yaml
|
||||
:emphasize-lines: 3
|
||||
|
||||
# [...]
|
||||
devel:
|
||||
stylesheet: css
|
||||
# [...]
|
||||
|
||||
Restart and shift-reload and take another look at the ``head``:
|
||||
|
||||
.. code-block:: html
|
||||
:linenos:
|
||||
:emphasize-lines: 7
|
||||
|
||||
<head>
|
||||
<!-- [...] -->
|
||||
<link href="/static/css/octoprint.css" rel="stylesheet" type="text/css" media="screen">
|
||||
|
||||
<!-- Plugin files -->
|
||||
<!-- [...] -->
|
||||
<link href="/plugin_assets/helloworld/css/helloworld.css" rel="stylesheet" type="text/css" media="screen">
|
||||
<!-- [...] -->
|
||||
<!-- /Plugin files -->
|
||||
<!-- [...] -->
|
||||
</head>
|
||||
|
||||
Now the CSS file is linked and no trace of the LESS links is left in the source. This should help to speed up your development
|
||||
tremendously when you have to work with complex stylesheets, just don't forgot to check the generated CSS file in with
|
||||
the rest of your plugin or people will miss it when trying to run your plugin!
|
||||
|
||||
.. note::
|
||||
|
||||
If your plugin only provides CSS files, OctoPrint will detect this when switched to LESS mode and include your
|
||||
CSS files instead of any non-existing LESS files. So you don't really *have* to use LESS if you don't want, but
|
||||
as soon as you need it just switch over.
|
||||
|
||||
The same thing works the other way around too btw. If your plugin only provides LESS files, OctoPrint will link to
|
||||
those and add lessjs to the page as well. Please keep in mind though that also providing CSS files is the cleaner
|
||||
way.
|
||||
|
||||
Where do we go from here?
|
||||
-------------------------
|
||||
|
||||
You've now seen how easy it is to add functionality to OctoPrint with this little tutorial. You can find the full
|
||||
source code of the little Hello World plugin we built together here :ref:`on Github <https://github.com/OctoPrint/Plugin-Examples/tree/master/helloworld>`.
|
||||
|
||||
But I want to invite you to dive deeper into OctoPrint's plugin system. To get an idea of all the other various plugin types
|
||||
you haven't seen yet, :ref:`take a look at the available plugin mixins <sec-plugins-mixins>`.
|
||||
|
||||
For some insight on how to create plugins that react to various events within OctoPrint,
|
||||
`the Growl Plugin <https://github.com/OctoPrint/OctoPrint-Growl>`_ might be a good example to learn from. For how to
|
||||
add support for a slicer, OctoPrint's own bundled `CuraEngine plugin <https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura>`_
|
||||
might give some hints. For extending OctoPrint's interface, the `NavbarTemp plugin <https://github.com/imrahil/OctoPrint-NavbarTemp>`_
|
||||
might show what's possible with a few lines of code already. Finally, just take a look at the
|
||||
`list of available plugins <https://github.com/foosel/OctoPrint/wiki#plugins>`_ on the OctoPrint wiki if you are
|
||||
looking for examples.
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] Refer to the `LESS documentation <http://lesscss.org/#using-less>`_ on how to do that. If you are developing
|
||||
your plugin under Windows you might also want to give `WinLESS <http://winless.org/>`_ a look which will run
|
||||
in the background and keep your CSS files up to date with your various project's LESS files automatically.
|
||||
|
|
|
|||
Loading…
Reference in a new issue