Merge branch 'maintenance' into devel
Conflicts: THIRDPARTYLICENSES.md setup.py src/octoprint/server/__init__.py src/octoprint/server/views.py src/octoprint/settings.py src/octoprint/static/css/octoprint.css
This commit is contained in:
commit
76b4a45495
31 changed files with 793 additions and 37 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
src/octoprint/templates/_data/AUTHORS.md
|
||||
src/octoprint/templates/_data/CHANGELOG.md
|
||||
src/octoprint/templates/_data/SPONSORS.md
|
||||
src/octoprint/templates/_data/SUPPORTERS.md
|
||||
src/octoprint/templates/_data/THIRDPARTYLICENSES.md
|
||||
|
||||
devtools
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ The documentation is located at [docs.octoprint.org](http://docs.octoprint.org).
|
|||
|
||||
The official plugin repository can be reached at [plugins.octoprint.org](http://plugins.octoprint.org).
|
||||
|
||||
OctoPrint's development wouldn't be possible without the [financial support by its community](http://octoprint.org/support-octoprint/).
|
||||
If you enjoy OctoPrint, please consider becoming a regular supporter!
|
||||
|
||||

|
||||
|
||||
You are currently looking at the source code repository of OctoPrint. If you already installed it
|
||||
|
|
@ -26,7 +29,7 @@ Contributions of all kinds are welcome, not only in the form of code but also wi
|
|||
[official documentation](http://docs.octoprint.org/) or [the public wiki](https://github.com/foosel/OctoPrint/wiki), support
|
||||
of other users in the [bug tracker](https://github.com/foosel/OctoPrint/issues),
|
||||
[the Mailinglist](https://groups.google.com/group/octoprint) or
|
||||
[the G+ Community](https://plus.google.com/communities/102771308349328485741).
|
||||
[the G+ Community](https://plus.google.com/communities/102771308349328485741) and also [financially](http://octoprint.org/support-octoprint/).
|
||||
|
||||
If you think something is bad as it is about OctoPrint or its documentation the way it is, please help
|
||||
in any way to make it better instead of just complaining about it -- this is an Open Source Project
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Sponsors
|
||||
# Supporters
|
||||
|
||||
Development of this version of OctoPrint wouldn't have been possible without
|
||||
[financial support by the community](http://octoprint.org/support-octoprint/) -
|
||||
|
|
@ -14,8 +14,8 @@ thanks to everyone who contributed!
|
|||
* E3D BigBox
|
||||
* Erik de Bruijn
|
||||
* Ernesto Martinez
|
||||
* Exovite
|
||||
* georgeroblesjr
|
||||
* Exovite
|
||||
* georgeroblesjr
|
||||
* Gregor Luetolf
|
||||
* Kale Stedman
|
||||
* Makespace Madrid
|
||||
|
|
@ -26,6 +26,6 @@ thanks to everyone who contributed!
|
|||
* Samer Najia
|
||||
* Stefan Krister
|
||||
* Sven Mueller
|
||||
* Tom
|
||||
* Tom
|
||||
|
||||
and 321 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)!
|
||||
and 321 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)!
|
||||
|
|
@ -34,10 +34,11 @@
|
|||
|
||||
* [Awesome-Slugify](https://pypi.python.org/pypi/awesome-slugify): GPLv3
|
||||
* [Click](http://click.pocoo.org/): BSD
|
||||
* [feedparser](https://github.com/kurtmckee/feedparser): BSD
|
||||
* [Flask](http://flask.pocoo.org/): BSD
|
||||
* [Flask-Assets](http://github.com/miracle2k/flask-assets): BSD
|
||||
* [Flask-Babel](http://github.com/mitsuhiko/flask-babel): BSD
|
||||
* [Flask-Login](https://flask-login.readthedocs.org/en/latest/https://github.com/maxcountryman/flask-login): MIT
|
||||
* [Flask-Login](https://github.com/maxcountryman/flask-login): MIT
|
||||
* [Flask-Markdown](http://github.com/dcolish/flask-markdown): BSD
|
||||
* [Flask-Principal](http://packages.python.org/Flask-Principal/): MIT
|
||||
* [netaddr](https://github.com/drkjam/netaddr/): BSD
|
||||
|
|
|
|||
5
setup.py
5
setup.py
|
|
@ -36,7 +36,8 @@ INSTALL_REQUIRES = [
|
|||
"semantic_version>=2.4.2,<2.5",
|
||||
"psutil>=3.2.1,<3.3",
|
||||
"Click>=6.2,<6.3",
|
||||
"awesome-slugify>=1.6.5,<1.7"
|
||||
"awesome-slugify>=1.6.5,<1.7",
|
||||
"feedparser>=5.2.1,<5.3"
|
||||
]
|
||||
|
||||
# Additional requirements for optional install options
|
||||
|
|
@ -114,7 +115,7 @@ def get_cmdclass():
|
|||
"octoprint/templates/_data": [
|
||||
"AUTHORS.md",
|
||||
"CHANGELOG.md",
|
||||
"SPONSORS.md",
|
||||
"SUPPORTERS.md",
|
||||
"THIRDPARTYLICENSES.md",
|
||||
]
|
||||
}, cmdclass["build_py"] if "build_py" in cmdclass else _build_py)
|
||||
|
|
|
|||
286
src/octoprint/plugins/announcements/__init__.py
Normal file
286
src/octoprint/plugins/announcements/__init__.py
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
import calendar
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import threading
|
||||
|
||||
import feedparser
|
||||
import flask
|
||||
|
||||
from octoprint.server import admin_permission
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from flask.ext.babel import gettext
|
||||
|
||||
class AnnouncementPlugin(octoprint.plugin.AssetPlugin,
|
||||
octoprint.plugin.SettingsPlugin,
|
||||
octoprint.plugin.BlueprintPlugin,
|
||||
octoprint.plugin.StartupPlugin,
|
||||
octoprint.plugin.TemplatePlugin):
|
||||
|
||||
def __init__(self):
|
||||
self._cached_channels = dict()
|
||||
self._cached_channels_mutex = threading.RLock()
|
||||
|
||||
# StartupPlugin
|
||||
|
||||
def on_after_startup(self):
|
||||
self._fetch_all_channels()
|
||||
|
||||
# SettingsPlugin
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return dict(channels=dict(_important=dict(name="Important OctoPrint Announcements",
|
||||
priority=1,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/important.xml",
|
||||
read_until=1449446400),
|
||||
_releases=dict(name="OctoPrint Release Announcements",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/releases.xml",
|
||||
read_until=1458121176),
|
||||
_spotlight=dict(name="OctoPrint Community Spotlights",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/spotlight.xml",
|
||||
read_until=1447953971),
|
||||
_octopi=dict(name="OctoPi Announcements",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://octoprint.org/feeds/octopi.xml",
|
||||
read_until=1462200600),
|
||||
_plugins=dict(name="New Plugins in the Repository",
|
||||
priority=2,
|
||||
type="rss",
|
||||
url="http://plugins.octoprint.org/feed.xml",
|
||||
read_until=1461628800)),
|
||||
enabled_channels=[],
|
||||
forced_channels=["_important"],
|
||||
ttl=6*60,
|
||||
display_limit=3,
|
||||
summary_limit=300)
|
||||
|
||||
# AssetPlugin
|
||||
|
||||
def get_assets(self):
|
||||
return dict(js=["js/announcements.js"],
|
||||
less=["less/announcements.less"],
|
||||
css=["css/announcements.css"])
|
||||
|
||||
# Template Plugin
|
||||
|
||||
def get_template_configs(self):
|
||||
return [
|
||||
dict(type="settings", name=gettext("Announcements"), template="announcements_settings.jinja2", custom_bindings=True),
|
||||
dict(type="navbar", template="announcements_navbar.jinja2", styles=["display: none"], data_bind="visible: loginState.isAdmin")
|
||||
]
|
||||
|
||||
# Blueprint Plugin
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/channels", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def get_channel_data(self):
|
||||
result = dict()
|
||||
|
||||
channel_data = self._fetch_all_channels()
|
||||
|
||||
channel_configs = self._get_channel_configs()
|
||||
enabled = self._settings.get(["enabled_channels"])
|
||||
forced = self._settings.get(["forced_channels"])
|
||||
for key, data in channel_configs.items():
|
||||
entries = self._to_internal_feed(channel_data.get(key, []), read_until=channel_configs[key].get("read_until", None))
|
||||
unread = len(filter(lambda e: not e["read"], entries))
|
||||
|
||||
result[key] = dict(channel=data["name"],
|
||||
url=data["url"],
|
||||
priority=data["priority"],
|
||||
enabled=key in enabled or key in forced,
|
||||
forced=key in forced,
|
||||
data=entries,
|
||||
unread=unread)
|
||||
|
||||
return flask.jsonify(result)
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/channels/<channel>", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def channel_command(self, channel):
|
||||
from octoprint.server.util.flask import get_json_command_from_request
|
||||
from octoprint.server import NO_CONTENT
|
||||
|
||||
valid_commands = dict(read=["until"],
|
||||
toggle=[])
|
||||
|
||||
command, data, response = get_json_command_from_request(flask.request, valid_commands=valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
if command == "read":
|
||||
current_read_until = None
|
||||
channel_data = self._settings.get(["channels", channel], merged=True)
|
||||
if channel_data:
|
||||
current_read_until = channel_data.get("read_until", None)
|
||||
|
||||
defaults = dict(plugins=dict(announcements=dict(channels=dict())))
|
||||
defaults["plugins"]["announcements"]["channels"][channel] = dict(read_until=current_read_until)
|
||||
|
||||
until = data["until"]
|
||||
self._settings.set(["channels", channel, "read_until"], until, defaults=defaults)
|
||||
self._settings.save()
|
||||
|
||||
elif command == "toggle":
|
||||
enabled_channels = list(self._settings.get(["enabled_channels"]))
|
||||
|
||||
if channel in enabled_channels:
|
||||
enabled_channels.remove(channel)
|
||||
else:
|
||||
enabled_channels.append(channel)
|
||||
|
||||
self._settings.set(["enabled_channels"], enabled_channels)
|
||||
self._settings.save()
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
# Internal Tools
|
||||
|
||||
def _get_channel_configs(self):
|
||||
return self._settings.get(["channels"], merged=True)
|
||||
|
||||
def _fetch_all_channels(self):
|
||||
with self._cached_channels_mutex:
|
||||
channels = self._get_channel_configs()
|
||||
enabled = self._settings.get(["enabled_channels"])
|
||||
forced = self._settings.get(["forced_channels"])
|
||||
|
||||
all_channels = dict()
|
||||
for key, config in channels.items():
|
||||
if not key in enabled and not key in forced:
|
||||
continue
|
||||
|
||||
data = self._get_channel_data(key, config)
|
||||
if data is not None:
|
||||
all_channels[key] = data
|
||||
|
||||
self._cached_channels = all_channels
|
||||
|
||||
return self._cached_channels
|
||||
|
||||
def _get_channel_data(self, key, config):
|
||||
data = self._get_channel_data_from_cache(key, config)
|
||||
if data is None:
|
||||
data = self._get_channel_data_from_network(key, config)
|
||||
return data
|
||||
|
||||
def _get_channel_data_from_cache(self, key, config):
|
||||
channel_path = os.path.join(self.get_plugin_data_folder(), "{}.cache".format(key))
|
||||
|
||||
if os.path.exists(channel_path):
|
||||
if "ttl" in config and isinstance(config["ttl"], int):
|
||||
ttl = config["ttl"]
|
||||
else:
|
||||
ttl = self._settings.get_int(["ttl"])
|
||||
|
||||
ttl *= 60
|
||||
now = time.time()
|
||||
if os.stat(channel_path).st_mtime + ttl > now:
|
||||
d = feedparser.parse(channel_path)
|
||||
self._logger.info("Loaded channel {} from cache".format(key))
|
||||
return d
|
||||
|
||||
return None
|
||||
|
||||
def _get_channel_data_from_network(self, key, config):
|
||||
import requests
|
||||
|
||||
url = config["url"]
|
||||
try:
|
||||
r = requests.get(url)
|
||||
self._logger.info("Loaded channel {} from {}".format(key, config["url"]))
|
||||
except Exception as e:
|
||||
self._logger.exception(
|
||||
"Could not fetch channel {} from {}: {}".format(key, config["url"], str(e)))
|
||||
return None
|
||||
|
||||
response = r.text
|
||||
channel_path = os.path.join(self.get_plugin_data_folder(), "{}.cache".format(key))
|
||||
with codecs.open(channel_path, mode="w", encoding="utf-8") as f:
|
||||
f.write(response)
|
||||
return feedparser.parse(response)
|
||||
|
||||
def _to_internal_feed(self, feed, read_until=None):
|
||||
result = []
|
||||
if "entries" in feed:
|
||||
for entry in feed["entries"]:
|
||||
internal_entry = self._to_internal_entry(entry, read_until=read_until)
|
||||
if internal_entry:
|
||||
result.append(internal_entry)
|
||||
return result
|
||||
|
||||
def _to_internal_entry(self, entry, read_until=None):
|
||||
published = calendar.timegm(entry["published_parsed"])
|
||||
|
||||
read = False
|
||||
if read_until is not None:
|
||||
read = published <= read_until
|
||||
|
||||
return dict(title=entry["title"],
|
||||
title_without_tags=_strip_tags(entry["title"]),
|
||||
summary=entry["summary"],
|
||||
summary_without_images=_strip_images(entry["summary"]),
|
||||
published=published,
|
||||
link=entry["link"],
|
||||
read=read)
|
||||
|
||||
|
||||
_image_tag_re = re.compile(r'<img.*?/?>')
|
||||
def _strip_images(text):
|
||||
return _image_tag_re.sub('', text)
|
||||
|
||||
|
||||
def _strip_tags(text):
|
||||
"""
|
||||
>>> _strip_tags(u"<a href='test.html'>Hello world</a><img src='foo.jpg'>")
|
||||
u"Hello world<img src='foo.jpg'>"
|
||||
>>> _strip_tags(u"> > Foo")
|
||||
u'> > Foo'
|
||||
"""
|
||||
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
class TagStripper(HTMLParser):
|
||||
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
self._fed = []
|
||||
|
||||
def handle_data(self, data):
|
||||
self._fed.append(data)
|
||||
|
||||
def handle_entityref(self, ref):
|
||||
self._fed.append("&{};".format(ref))
|
||||
|
||||
def handle_charref(self, ref):
|
||||
self._fed.append("&#{};".format(ref))
|
||||
|
||||
def get_data(self):
|
||||
return "".join(self._fed)
|
||||
|
||||
tag_stripper = TagStripper()
|
||||
tag_stripper.feed(text)
|
||||
return tag_stripper.get_data()
|
||||
|
||||
|
||||
__plugin_name__ = "Announcement Plugin"
|
||||
__plugin_implementation__ = AnnouncementPlugin()
|
||||
|
|
@ -0,0 +1 @@
|
|||
table td.settings_plugin_announcements_channels_name,table th.settings_plugin_announcements_channels_name{text-overflow:ellipsis;text-align:left}table td.settings_plugin_announcements_channels_actions,table th.settings_plugin_announcements_channels_actions{text-align:center;width:80px}table td.settings_plugin_announcements_channels_actions a,table th.settings_plugin_announcements_channels_actions a{text-decoration:none;color:#000}table td.settings_plugin_announcements_channels_actions a.disabled,table th.settings_plugin_announcements_channels_actions a.disabled{color:#ccc;cursor:default}#plugin_announcements_dialog .unread{font-weight:700}#plugin_announcements_dialog article{padding-right:20px}#plugin_announcements_dialog article.read{opacity:.5}#plugin_announcements_dialog article.read:hover{opacity:1}#plugin_announcements_dialog article .actions{background-color:#f5f5f5;border-radius:2px;padding:2px 5px;margin-top:5px}#plugin_announcements_dialog article .actions .markread{float:right}#plugin_announcements_dialog article .actions a{color:#000}
|
||||
300
src/octoprint/plugins/announcements/static/js/announcements.js
Normal file
300
src/octoprint/plugins/announcements/static/js/announcements.js
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
$(function() {
|
||||
function AnnouncementsViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = parameters[0];
|
||||
self.settings = parameters[1];
|
||||
|
||||
self.channels = new ItemListHelper(
|
||||
"plugin.announcements.channels",
|
||||
{
|
||||
"channel": function (a, b) {
|
||||
// sorts ascending
|
||||
if (a["channel"].toLocaleLowerCase() < b["channel"].toLocaleLowerCase()) return -1;
|
||||
if (a["channel"].toLocaleLowerCase() > b["channel"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
"name",
|
||||
[],
|
||||
[],
|
||||
5
|
||||
);
|
||||
|
||||
self.unread = ko.observable();
|
||||
self.hiddenChannels = [];
|
||||
self.channelNotifications = {};
|
||||
|
||||
self.announcementDialog = undefined;
|
||||
self.announcementDialogContent = undefined;
|
||||
self.announcementDialogTabs = undefined;
|
||||
|
||||
self.setupTabLink = function(item) {
|
||||
$("a[data-toggle='tab']", item).on("show", self.resetContentScroll);
|
||||
};
|
||||
|
||||
self.resetContentScroll = function() {
|
||||
self.announcementDialogContent.scrollTop(0);
|
||||
};
|
||||
|
||||
self.toggleButtonCss = function(data) {
|
||||
var icon = data.enabled ? "icon-circle" : "icon-circle-blank";
|
||||
var disabled = (self.enableToggle(data)) ? "" : " disabled";
|
||||
|
||||
return icon + disabled;
|
||||
};
|
||||
|
||||
self.toggleButtonTitle = function(data) {
|
||||
return data.forced ? gettext("Cannot be toggled") : (data.enabled ? gettext("Disable Channel") : gettext("Enable Channel"));
|
||||
};
|
||||
|
||||
self.enableToggle = function(data) {
|
||||
return !data.forced;
|
||||
};
|
||||
|
||||
self.markRead = function(channel, until) {
|
||||
if (!self.loginState.isAdmin()) return;
|
||||
|
||||
var url = PLUGIN_BASEURL + "announcements/channels/" + channel;
|
||||
|
||||
var payload = {
|
||||
command: "read",
|
||||
until: until
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload),
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
success: function() {
|
||||
self.retrieveData()
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.toggleChannel = function(channel) {
|
||||
if (!self.loginState.isAdmin()) return;
|
||||
|
||||
var url = PLUGIN_BASEURL + "announcements/channels/" + channel;
|
||||
|
||||
var payload = {
|
||||
command: "toggle"
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload),
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
success: function() {
|
||||
self.retrieveData()
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.retrieveData = function(force) {
|
||||
if (!self.loginState.isAdmin()) return;
|
||||
|
||||
var url = PLUGIN_BASEURL + "announcements/channels";
|
||||
if (force) {
|
||||
url += "?force=true";
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
self.fromResponse(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
self.fromResponse = function(data) {
|
||||
var currentTab = $("li.active a", self.announcementDialogTabs).attr("href");
|
||||
|
||||
var unread = 0;
|
||||
var channels = [];
|
||||
_.each(data, function(value, key) {
|
||||
value.key = key;
|
||||
value.last = value.data.length ? value.data[0].published : undefined;
|
||||
value.count = value.data.length;
|
||||
unread += value.unread;
|
||||
channels.push(value);
|
||||
});
|
||||
self.channels.updateItems(channels);
|
||||
self.unread(unread);
|
||||
|
||||
self.displayAnnouncements(channels);
|
||||
|
||||
self.selectTab(currentTab);
|
||||
};
|
||||
|
||||
self.showAnnouncementDialog = function(channel) {
|
||||
self.announcementDialogContent.scrollTop(0);
|
||||
|
||||
if (!self.announcementDialog.hasClass("in")) {
|
||||
self.announcementDialog.modal({
|
||||
minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); }
|
||||
}).css({
|
||||
width: 'auto',
|
||||
'margin-left': function() { return -($(this).width() /2); }
|
||||
});
|
||||
}
|
||||
|
||||
var tab = undefined;
|
||||
if (channel) {
|
||||
tab = "#plugin_announcements_dialog_channel_" + channel;
|
||||
}
|
||||
self.selectTab(tab);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
self.selectTab = function(tab) {
|
||||
if (tab != undefined) {
|
||||
if (!_.startsWith(tab, "#")) {
|
||||
tab = "#" + tab;
|
||||
}
|
||||
$('a[href="' + tab + '"]', self.announcementDialogTabs).tab("show");
|
||||
} else {
|
||||
$('a:first', self.announcementDialogTabs).tab("show");
|
||||
}
|
||||
};
|
||||
|
||||
self.displayAnnouncements = function(channels) {
|
||||
var displayLimit = self.settings.settings.plugins.announcements.display_limit();
|
||||
var maxLength = self.settings.settings.plugins.announcements.summary_limit();
|
||||
|
||||
var cutAfterNewline = function(text) {
|
||||
text = text.trim();
|
||||
|
||||
var firstNewlinePos = text.indexOf("\n");
|
||||
if (firstNewlinePos > 0) {
|
||||
text = text.substr(0, firstNewlinePos).trim();
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
var stripParagraphs = function(text) {
|
||||
if (_.startsWith(text, "<p>")) {
|
||||
text = text.substr("<p>".length);
|
||||
}
|
||||
if (_.endsWith(text, "</p>")) {
|
||||
text = text.substr(0, text.length - "</p>".length);
|
||||
}
|
||||
|
||||
return text.replace(/<\/p>\s*<p>/ig, "<br>");
|
||||
};
|
||||
|
||||
_.each(channels, function(value) {
|
||||
var key = value.key;
|
||||
var channel = value.channel;
|
||||
var priority = value.priority;
|
||||
var items = value.data;
|
||||
|
||||
if ($.inArray(key, self.hiddenChannels) > -1) {
|
||||
// channel currently ignored
|
||||
return;
|
||||
}
|
||||
|
||||
var newItems = _.filter(items, function(entry) { return !entry.read; });
|
||||
if (newItems.length == 0) {
|
||||
// no new items at all, we don't display anything for this channel
|
||||
return;
|
||||
}
|
||||
|
||||
var displayedItems;
|
||||
if (newItems.length > displayLimit) {
|
||||
displayedItems = newItems.slice(0, displayLimit);
|
||||
} else {
|
||||
displayedItems = newItems;
|
||||
}
|
||||
var rest = newItems.length - displayedItems.length;
|
||||
|
||||
var text = "<ul>";
|
||||
_.each(displayedItems, function(item) {
|
||||
var limitedSummary = stripParagraphs(item.summary_without_images.trim());
|
||||
if (limitedSummary.length > maxLength) {
|
||||
limitedSummary = limitedSummary.substr(0, maxLength);
|
||||
limitedSummary = limitedSummary.substr(0, Math.min(limitedSummary.length, limitedSummary.lastIndexOf(" ")));
|
||||
limitedSummary += "...";
|
||||
}
|
||||
|
||||
text += "<li><a href='" + item.link + "' target='_blank' rel='noreferrer noopener'>" + cutAfterNewline(item.title) + "</a><br><small>" + formatTimeAgo(item.published) + "</small><p>" + limitedSummary + "</p></li>";
|
||||
});
|
||||
text += "</ul>";
|
||||
|
||||
if (rest) {
|
||||
text += gettext(_.sprintf("... and %(rest)d more.", {rest: rest}));
|
||||
}
|
||||
|
||||
var options = {
|
||||
title: channel,
|
||||
text: text,
|
||||
hide: false,
|
||||
confirm: {
|
||||
confirm: true,
|
||||
buttons: [{
|
||||
text: gettext("Later"),
|
||||
click: function(notice) {
|
||||
self.hiddenChannels.push(key);
|
||||
notice.remove();
|
||||
}
|
||||
}, {
|
||||
text: gettext("Mark read"),
|
||||
click: function(notice) {
|
||||
self.markRead(key, value.last);
|
||||
notice.remove();
|
||||
}
|
||||
}, {
|
||||
text: gettext("Read..."),
|
||||
addClass: "btn-primary",
|
||||
click: function(notice) {
|
||||
self.showAnnouncementDialog(key);
|
||||
self.markRead(key, value.last);
|
||||
notice.remove();
|
||||
}
|
||||
}]
|
||||
},
|
||||
buttons: {
|
||||
sticker: false,
|
||||
closer: false
|
||||
}
|
||||
};
|
||||
|
||||
if (priority == 1) {
|
||||
options.type = "error";
|
||||
}
|
||||
|
||||
if (self.channelNotifications[key]) {
|
||||
self.channelNotifications[key].remove();
|
||||
}
|
||||
self.channelNotifications[key] = new PNotify(options);
|
||||
});
|
||||
};
|
||||
|
||||
self.onUserLoggedIn = function() {
|
||||
self.retrieveData();
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.announcementDialog = $("#plugin_announcements_dialog");
|
||||
self.announcementDialogContent = $("#plugin_announcements_dialog_content");
|
||||
self.announcementDialogTabs = $("#plugin_announcements_dialog_tabs");
|
||||
}
|
||||
}
|
||||
|
||||
// view model class, parameters for constructor, container to bind to
|
||||
ADDITIONAL_VIEWMODELS.push([
|
||||
AnnouncementsViewModel,
|
||||
["loginStateViewModel", "settingsViewModel"],
|
||||
["#plugin_announcements_dialog", "#settings_plugin_announcements", "#navbar_plugin_announcements"]
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
table {
|
||||
th, td {
|
||||
&.settings_plugin_announcements_channels_name {
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.settings_plugin_announcements_channels_actions {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#plugin_announcements_dialog {
|
||||
.unread {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
article {
|
||||
padding-right: 20px;
|
||||
|
||||
&.read {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.read:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
padding: 2px 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
.markread {
|
||||
float: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<div id="plugin_announcements_dialog" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="announcements_dialog_label" aria-hidden="true">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3 id="announcements_dialog_label">{{ _('Announcements') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="full-sized-box">
|
||||
<div class="tabbable row-fluid">
|
||||
<div class="span3 scrollable" id="plugin_announcements_dialog_menu">
|
||||
<ul class="nav nav-list" id="plugin_announcements_dialog_tabs">
|
||||
<!-- ko foreach: {data: channels.items, afterAdd: setupTabLink} -->
|
||||
<!-- ko if: $data.enabled || $data.forced -->
|
||||
<li>
|
||||
<a data-toggle="tab" target="_blank" data-bind="text: $data.channel + ' (' + $data.unread + '/' + $data.count + ')', attr: {href: '#plugin_announcements_dialog_channel_' + $data.key}, css: {unread: $data.unread}"></a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content span9 scrollable" id="plugin_announcements_dialog_content">
|
||||
<!-- ko foreach: channels.items -->
|
||||
<!-- ko if: $data.enabled || $data.forced -->
|
||||
<div class="tab-pane" data-bind="attr: {id: 'plugin_announcements_dialog_channel_' + $data.key}">
|
||||
<!-- ko foreach: $data.data -->
|
||||
<article data-bind="css: {read: $data.read}">
|
||||
<hr data-bind="visible: $index() > 0">
|
||||
<h3><a data-bind="text: $data.title, attr: {href: $data.link}" target="_blank" rel="noreferrer noopener"></a> <small data-bind="text: formatTimeAgo($data.published), attr: {title: formatDate($data.published)}"></small></h3>
|
||||
<div class="content" data-bind="html: $data.summary"></div>
|
||||
<div class="actions">
|
||||
<span class="articlelink"><small><a data-bind="text: $data.link, attr: {href: $data.link}" target="_blank" rel="noreferrer noopener"></a></small></span>
|
||||
<span class="markread"><small><a href="javascript:void(0)" data-bind="click: function() { $root.markRead($parent.key, $data.published) }"><i class="icon-eye-open"></i> {{ _('Mark as last read') }}</a></small></span>
|
||||
</div>
|
||||
</article>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-block" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<a id="navbar_show_announcements" class="pull-right" href="javascript:void(0)" data-bind="click: function() { showAnnouncementDialog(); }" title="{{ _('Announcements') }}">
|
||||
<i data-bind="css: {'icon-bell': !unread(), 'icon-bell-alt': unread()}"></i>
|
||||
</a>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<h3>{{ _('Configured Channels') }}</h3>
|
||||
|
||||
<table class="table table-striped table-hover table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="settings_plugin_announcements_channels_name">{{ _('Name') }}</th>
|
||||
<th class="settings_plugin_announcements_channels_actions">{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: channels.paginatedItems">
|
||||
<tr>
|
||||
<td class="settings_plugin_announcements_channels_name">
|
||||
<div data-bind="text: channel, css: {muted: !enabled}"></div>
|
||||
<div><small class="muted" data-bind="text: url"> </small></div>
|
||||
</td>
|
||||
<td class="settings_plugin_announcements_channels_actions">
|
||||
<a href="javascript:void(0)" data-bind="css: $root.toggleButtonCss($data), attr: {title: $root.toggleButtonTitle($data)}, enable: $root.enableToggle($data), click: function() { $root.toggleChannel($data.key) }"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: channels.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: channels.prevPage">«</a></li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: channels.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.channels.currentPage(), disabled: $data.number === -1 }"><a href="javascript:void(0)" data-bind="text: $data.text, click: function() { $root.channels.changePage($data.number); }"></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: channels.currentPage() === channels.lastPage()}"><a href="javascript:void(0)" data-bind="click: channels.nextPage">»</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-block" data-bind="click: $root.showAnnouncementDialog">{{ _('Show Announcements...') }}</button>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<ul>
|
||||
{% for plugin in plugin_pluginmanager_thirdparty %}
|
||||
<li><a href="{{ plugin.url }}">{{ plugin.name }}</a>: {{ plugin.license }}</li>
|
||||
<li>{% if plugin.url %}<a href="{{ plugin.url }}" target="_blank" rel="noreferrer noopener">{% endif %}{{ plugin.name }}{% if plugin.url %}</a>{% endif %}: {% if plugin.license %}{{ plugin.license }}{% else %}-{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i class="icon-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="icon-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="icon-minus" data-bind="visible: pending_uninstall"></i></div>
|
||||
<div><small class="muted" data-bind="text: description"> </small></div>
|
||||
<div data-bind="css: {muted: !enabled}">
|
||||
<small data-bind="visible: url"><i class="icon-home"></i> <a data-bind="attr: {href: url}">{{ _('Homepage') }}</a></small>
|
||||
<small data-bind="visible: url"><i class="icon-home"></i> <a data-bind="attr: {href: url}" target="_blank" rel="noreferrer noopener">{{ _('Homepage') }}</a></small>
|
||||
<small data-bind="visible: license"><i class="icon-legal"></i> <span data-bind="text: license"></span></small>
|
||||
<small data-bind="visible: author"><i class="icon-user"></i> <span data-bind="text: author"></span></small>
|
||||
<small> </small>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<!-- ko foreach: availableAndPossible -->
|
||||
<li>
|
||||
<span class="name" data-bind="text: fullNameRemote, attr: {title: fullNameRemote}"></span>
|
||||
<div class="releaseNotes" data-bind="visible: releaseNotes"><a data-bind="attr: {href: releaseNotes}">{{ _('Release Notes') }}</a></div>
|
||||
<div class="releaseNotes" data-bind="visible: releaseNotes"><a data-bind="attr: {href: releaseNotes}" target="_blank" rel="noreferrer noopener">{{ _('Release Notes') }}</a></div>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<small class="muted">
|
||||
{{ _('Installed:') }} <span data-bind="text: information.local.name"></span><br>
|
||||
{{ _('Available:') }} <span data-bind="text: information.remote.name"></span><br>
|
||||
<span data-bind="visible: releaseNotes">{{ _('Release Notes:') }} <a data-bind="attr: {href: releaseNotes}, text: releaseNotes" target="_blank"></a></span>
|
||||
<span data-bind="visible: releaseNotes">{{ _('Release Notes:') }} <a data-bind="attr: {href: releaseNotes}, text: releaseNotes" target="_blank" rel="noreferrer noopener"></a></span>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -668,9 +668,24 @@ class Server(object):
|
|||
return "{hashs} {content}".format(hashs="#" * number, content=match.group("content"))
|
||||
return markdown_header_regex.sub(repl, s)
|
||||
|
||||
html_link_regex = re.compile("<(?P<tag>a.*?)>(?P<content>.*?)</a>")
|
||||
def externalize_links(text):
|
||||
def repl(match):
|
||||
tag = match.group("tag")
|
||||
if not u"href" in tag:
|
||||
return match.group(0)
|
||||
|
||||
if not u"target=" in tag and not u"rel=" in tag:
|
||||
tag += u" target=\"_blank\" rel=\"noreferrer noopener\""
|
||||
|
||||
content = match.group("content")
|
||||
return u"<{tag}>{content}</a>".format(tag=tag, content=content)
|
||||
return html_link_regex.sub(repl, text)
|
||||
|
||||
app.jinja_env.filters["regex_replace"] = regex_replace
|
||||
app.jinja_env.filters["offset_html_headers"] = offset_html_headers
|
||||
app.jinja_env.filters["offset_markdown_headers"] = offset_markdown_headers
|
||||
app.jinja_env.filters["externalize_links"] = externalize_links
|
||||
|
||||
# configure additional template folders for jinja2
|
||||
import jinja2
|
||||
|
|
@ -682,7 +697,7 @@ class Server(object):
|
|||
loaders = [app.jinja_loader, filesystem_loader]
|
||||
if octoprint.util.is_running_from_source():
|
||||
root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
allowed = ["AUTHORS.md", "CHANGELOG.md", "SPONSORS.md", "THIRDPARTYLICENSES.md"]
|
||||
allowed = ["AUTHORS.md", "CHANGELOG.md", "SUPPORTERS.md", "THIRDPARTYLICENSES.md"]
|
||||
files = {"_data/" + name: os.path.join(root, name) for name in allowed}
|
||||
loaders.append(octoprint.util.jinja.SelectedFilesLoader(files))
|
||||
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ def _process_templates():
|
|||
thirdparty=(gettext("Third Party Licenses"), dict(template="dialogs/about/thirdparty.jinja2", _div="about_thirdparty", custom_bindings=False)),
|
||||
authors=(gettext("Authors"), dict(template="dialogs/about/authors.jinja2", _div="about_authors", custom_bindings=False)),
|
||||
changelog=(gettext("Changelog"), dict(template="dialogs/about/changelog.jinja2", _div="about_changelog", custom_bindings=False)),
|
||||
sponsors = (gettext("Sponsors"), dict(template="dialogs/about/sponsors.jinja2", _div="about_sponsors", custom_bindings=False))
|
||||
supporters=(gettext("Supporters"), dict(template="dialogs/about/supporters.jinja2", _div="about_sponsors", custom_bindings=False))
|
||||
)
|
||||
|
||||
# extract data from template plugins
|
||||
|
|
|
|||
|
|
@ -214,17 +214,17 @@ default_settings = {
|
|||
"defaultLanguage": "_default",
|
||||
"components": {
|
||||
"order": {
|
||||
"navbar": ["settings", "systemmenu", "login"],
|
||||
"navbar": ["settings", "systemmenu", "login", "plugin_announcements"],
|
||||
"sidebar": ["connection", "state", "files"],
|
||||
"tab": ["temperature", "control", "gcodeviewer", "terminal", "timelapse"],
|
||||
"settings": [
|
||||
"section_printer", "serial", "printerprofiles", "temperatures", "terminalfilters", "gcodescripts",
|
||||
"section_features", "features", "webcam", "accesscontrol", "api",
|
||||
"section_octoprint", "server", "folders", "appearance", "logs", "plugin_pluginmanager", "plugin_softwareupdate"
|
||||
"section_octoprint", "server", "folders", "appearance", "logs", "plugin_pluginmanager", "plugin_softwareupdate", "plugin_announcements"
|
||||
],
|
||||
"usersettings": ["access", "interface"],
|
||||
"wizard": ["access"],
|
||||
"about": ["about", "sponsors", "authors", "changelog", "license", "thirdparty", "plugin_pluginmanager"],
|
||||
"about": ["about", "supporters", "authors", "changelog", "license", "thirdparty", "plugin_pluginmanager"],
|
||||
"generic": []
|
||||
},
|
||||
"disabled": {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ $(function() {
|
|||
self.aboutContent.scrollTop(0);
|
||||
});
|
||||
};
|
||||
|
||||
self.showTab = function(tab) {
|
||||
$("a[href=#" + tab + "]", self.aboutTabs).tab("show");
|
||||
};
|
||||
}
|
||||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ $(function() {
|
|||
self.loginState = parameters[0];
|
||||
self.users = parameters[1];
|
||||
self.printerProfiles = parameters[2];
|
||||
self.about = parameters[3];
|
||||
|
||||
self.allViewModels = [];
|
||||
|
||||
|
|
@ -799,7 +800,7 @@ $(function() {
|
|||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
SettingsViewModel,
|
||||
["loginStateViewModel", "usersViewModel", "printerProfilesViewModel"],
|
||||
["loginStateViewModel", "usersViewModel", "printerProfilesViewModel", "aboutViewModel"],
|
||||
["#settings_dialog", "#navbar_settings"]
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -694,6 +694,10 @@ ul.dropdown-menu li a {
|
|||
.acl_decision {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.aboutlink {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,18 @@
|
|||
<p>Version <span class="version">{{ display_version }}</span></p>
|
||||
|
||||
<ul>
|
||||
<li>Website: <a href="http://octoprint.org" target="_blank">octoprint.org</a></li>
|
||||
<li>Source Code: <a href="https://github.com/foosel/OctoPrint" target="_blank">github.com/foosel/OctoPrint</a></li>
|
||||
<li>Documentation: <a href="http://docs.octoprint.org" target="_blank">docs.octoprint.org</a></li>
|
||||
<li>Website: <a href="http://octoprint.org" target="_blank" rel="noreferrer noopener">octoprint.org</a></li>
|
||||
<li>Source Code: <a href="https://github.com/foosel/OctoPrint" target="_blank" rel="noreferrer noopener">github.com/foosel/OctoPrint</a></li>
|
||||
<li>Documentation: <a href="http://docs.octoprint.org" target="_blank" rel="noreferrer noopener">docs.octoprint.org</a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>OctoPrint is sponsored by a lot of awesome people. Please see "Sponsors" to the left.</strong>
|
||||
<strong>Development of OctoPrint wouldn't be possible without <a href="javascript:void(0)" data-bind="click: function() { showTab('{{ templates.about.entries.supporters[1]._div }}'); }">its supporters</a>.</strong><br>
|
||||
If you enjoy OctoPrint, please consider <a href="http://octoprint.org/support-octoprint/" target="_blank" rel="noreferrer noopener">supporting its ongoing development</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
© 2012-{{ now.strftime("%Y") }} The OctoPrint Authors
|
||||
© 2012-{{ now.strftime("%Y") }} <a href="javascript:void(0)" data-bind="click: function() { showTab('{{ templates.about.entries.authors[1]._div }}'); }">The OctoPrint Authors</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
|
@ -33,16 +34,16 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
For a copy of the GNU Affero General Public License, see "OctoPrint License"
|
||||
For a copy of the GNU Affero General Public License, see <a href="javascript:void(0)" data-bind="click: function() { showTab('{{ templates.about.entries.license[1]._div }}'); }">{{ templates.about.entries.license[0] }}</a>
|
||||
to the left.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
OctoPrint also utilizes various dependencies under terms of their
|
||||
respective licenses, which can be found under "Third Party Licenses" to
|
||||
respective licenses, which can be found under <a href="javascript:void(0)" data-bind="click: function() { showTab('{{ templates.about.entries.thirdparty[1]._div }}'); }">{{ templates.about.entries.thirdparty[0] }}</a> to
|
||||
the left. Copyright of those dependencies lies with their respective authors
|
||||
</p>
|
||||
|
||||
<p><small>
|
||||
"OctoPrint" is a registered trademark
|
||||
"OctoPrint" is a registered trademark.
|
||||
</small></p>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/AUTHORS.md" ignore missing %}{% endfilter %}{% endfilter %}
|
||||
{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/AUTHORS.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/CHANGELOG.md" ignore missing %}{% endfilter %}{% endfilter %}
|
||||
{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/CHANGELOG.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{% include "_data/agpl.html" %}
|
||||
{% filter externalize_links %}{% include "_data/agpl.html" %}{% endfilter %}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/SPONSORS.md" ignore missing %}{% endfilter %}{% endfilter %}
|
||||
1
src/octoprint/templates/dialogs/about/supporters.jinja2
Normal file
1
src/octoprint/templates/dialogs/about/supporters.jinja2
Normal file
|
|
@ -0,0 +1 @@
|
|||
{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/SUPPORTERS.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %}
|
||||
|
|
@ -1 +1 @@
|
|||
{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/THIRDPARTYLICENSES.md" ignore missing %}{% endfilter %}{% endfilter %}
|
||||
{% filter externalize_links %}{% filter markdown %}{% filter offset_markdown_headers(2) %}{% include "_data/THIRDPARTYLICENSES.md" ignore missing %}{% endfilter %}{% endfilter %}{% endfilter %}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn aboutlink" data-bind="click: about.show"><i class="icon-info-sign"></i> {{ _('About OctoPrint') }}</button>
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</button>
|
||||
<button class="btn btn-primary" data-bind="click: function() { saveData(undefined, $root.hide) }, enable: !exchanging(), css: {disabled: exchanging()}"><i class="icon-spinner icon-spin" data-bind="visible: sending"></i> {{ _('Save') }}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@
|
|||
<li><small>{{ _('Version') }}: <span class="version">{{ display_version|e }}</span></small></li>
|
||||
</ul>
|
||||
<ul class="pull-right">
|
||||
<li><a href="http://octoprint.org"><i class="icon-home"></i> {{ _('Homepage') }}</a></li>
|
||||
<li><a href="https://github.com/foosel/OctoPrint/"><i class="icon-github"></i> {{ _('Sourcecode') }}</a></li>
|
||||
<li><a href="http://docs.octoprint.org"><i class="icon-book"></i> {{ _('Documentation') }}</a></li>
|
||||
<li><a href="https://github.com/foosel/OctoPrint/issues"><i class="icon-flag"></i> {{ _('Bugs and Requests') }}</a></li>
|
||||
<li><a href="http://octoprint.org" target="_blank" rel="noreferrer noopener"><i class="icon-home"></i> {{ _('Homepage') }}</a></li>
|
||||
<li><a href="https://github.com/foosel/OctoPrint/" target="_blank" rel="noreferrer noopener"><i class="icon-github"></i> {{ _('Sourcecode') }}</a></li>
|
||||
<li><a href="http://docs.octoprint.org" target="_blank" rel="noreferrer noopener"><i class="icon-book"></i> {{ _('Documentation') }}</a></li>
|
||||
<li><a href="https://github.com/foosel/OctoPrint/issues" target="_blank" rel="noreferrer noopener"><i class="icon-flag"></i> {{ _('Bugs and Requests') }}</a></li>
|
||||
<li id="footer_about"><a href="javascript:void(0)" data-bind="click: show"><i class="icon-info-sign"></i> {{ _('About') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue