Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/OctoPrint/OctoPrint.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGina HÀußge <gina@octoprint.org>2022-10-26 11:41:22 +0300
committerGina HÀußge <gina@octoprint.org>2022-10-26 11:41:22 +0300
commitf993ab05af2d9f1a3f8df59577bfd845ed06cc32 (patch)
tree06ede91b647e0ce5266238e88430f602697ea926
parentcdde1bbc07f35798dce1d375fa539d4915d7cb2c (diff)
parent0224a02d908a6a88d7b9a25e5e9845fdf833d3d0 (diff)
Merge branch 'maintenance' into devel
-rw-r--r--.github/workflows/build.yml34
-rw-r--r--.github/workflows/issue_automation.yml4
-rw-r--r--.github/workflows/linkify_bundles.yml6
-rw-r--r--.github/workflows/nightly_merge.yml2
-rw-r--r--.github/workflows/pr_automation.yml2
-rw-r--r--.github/workflows/test_install.yml6
-rw-r--r--AUTHORS.md1
-rw-r--r--SECURITY.md10
-rw-r--r--src/octoprint/plugins/gcodeviewer/__init__.py29
-rw-r--r--src/octoprint/plugins/gcodeviewer/static/js/gcodeviewer.js73
-rw-r--r--src/octoprint/plugins/gcodeviewer/static/js/viewer/reader.js25
-rw-r--r--src/octoprint/plugins/gcodeviewer/static/js/viewer/ui.js6
-rw-r--r--src/octoprint/plugins/gcodeviewer/static/js/viewer/worker.js118
-rw-r--r--src/octoprint/plugins/gcodeviewer/templates/gcodeviewer_settings.jinja23
-rw-r--r--src/octoprint/plugins/pluginmanager/__init__.py127
-rw-r--r--src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js466
-rw-r--r--src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja218
-rw-r--r--src/octoprint/static/js/app/client/files.js8
18 files changed, 772 insertions, 166 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c6f89e6d6..9eb7968ca 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,11 +11,11 @@ jobs:
name: πŸ”¨ Build distribution
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
- name: πŸ— Set up Python 3.7
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v4
with:
python-version: 3.7
- name: πŸ— Install build dependencies
@@ -25,7 +25,7 @@ jobs:
run: |
python setup.py sdist bdist_wheel
- name: ⬆ Upload build result
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3
with:
name: dist
path: dist
@@ -34,9 +34,9 @@ jobs:
name: 🧹 Pre-commit
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: πŸ— Set up Python 3.7
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v4
with:
python-version: 3.7
- name: πŸ— Set up dev dependencies
@@ -53,9 +53,9 @@ jobs:
python: ["3.7", "3.8", "3.9", "3.10"]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: πŸ— Set up Python ${{ matrix.python }}
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: πŸ— Set up test dependencies
@@ -70,7 +70,7 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: πŸ— Set up test dependencies
run: |
npm -g install node-qunit-puppeteer@2.1.0
@@ -89,12 +89,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ⬇ Download build result
- uses: actions/download-artifact@v1
+ uses: actions/download-artifact@v3
with:
name: dist
path: dist
- name: πŸ— Set up Python ${{ matrix.python }}
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: πŸš€ Install wheel
@@ -113,15 +113,15 @@ jobs:
continue-on-error: true
steps:
- name: ⬇ Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: ⬇ Download build result
- uses: actions/download-artifact@v1
+ uses: actions/download-artifact@v3
with:
name: dist
path: dist
- name: πŸ— Set up Python 3.7
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v4
with:
python-version: 3.7
- name: πŸš€ Install wheel
@@ -134,7 +134,7 @@ jobs:
cp -r .github/fixtures/with_acl/* e2econfig
- name: πŸš€ Run Cypress
- uses: cypress-io/github-action@v2
+ uses: cypress-io/github-action@v4.2.0
with:
working-directory: tests/cypress
browser: chrome
@@ -146,7 +146,7 @@ jobs:
CYPRESS_DEPLOYSENTINEL_KEY: ${{ secrets.DEPLOYSENTINEL_APIKEY }}
- name: ⬆ Upload screenshots
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
@@ -170,7 +170,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ⬇ Download build result
- uses: actions/download-artifact@v1
+ uses: actions/download-artifact@v3
with:
name: dist
path: dist
@@ -188,7 +188,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ⬇ Download build result
- uses: actions/download-artifact@v1
+ uses: actions/download-artifact@v3
with:
name: dist
path: dist
diff --git a/.github/workflows/issue_automation.yml b/.github/workflows/issue_automation.yml
index 13830c66a..ab645ab90 100644
--- a/.github/workflows/issue_automation.yml
+++ b/.github/workflows/issue_automation.yml
@@ -7,7 +7,7 @@ jobs:
issue-automation:
runs-on: ubuntu-latest
steps:
- - uses: actions/github-script@v5
+ - uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -72,7 +72,7 @@ jobs:
# with:
# repo-token: ${{ secrets.GITHUB_TOKEN }}
- - uses: actions/github-script@v5
+ - uses: actions/github-script@v6
env:
REMINDER: >
Hi @${{ github.event.issue.user.login }}!
diff --git a/.github/workflows/linkify_bundles.yml b/.github/workflows/linkify_bundles.yml
index 9e0553891..18bc21a07 100644
--- a/.github/workflows/linkify_bundles.yml
+++ b/.github/workflows/linkify_bundles.yml
@@ -7,7 +7,7 @@ jobs:
linkifyBundles:
runs-on: ubuntu-latest
steps:
- - uses: actions/github-script@v2
+ - uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -38,14 +38,14 @@ jobs:
});
text += "\n*edited by @github-actions to add bundle viewer links*\n";
- github.issues.updateComment({
+ github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
body: text
});
} else if (botWasHere !== -1) {
- github.issues.updateComment({
+ github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml
index 56821fbc0..e101eb722 100644
--- a/.github/workflows/nightly_merge.yml
+++ b/.github/workflows/nightly_merge.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ⬇ Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
fetch-depth: 0
diff --git a/.github/workflows/pr_automation.yml b/.github/workflows/pr_automation.yml
index cc411d121..a7f7ef7cf 100644
--- a/.github/workflows/pr_automation.yml
+++ b/.github/workflows/pr_automation.yml
@@ -12,7 +12,7 @@ jobs:
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- - uses: actions/github-script@v5
+ - uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/test_install.yml b/.github/workflows/test_install.yml
index 76d7b2e23..18f6d7b39 100644
--- a/.github/workflows/test_install.yml
+++ b/.github/workflows/test_install.yml
@@ -19,11 +19,11 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: 🐍 Set up Python ${{ matrix.python }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: ⬇ Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
ref: ${{ matrix.branch }}
fetch-depth: 0
@@ -60,7 +60,7 @@ jobs:
with:
repository: "OctoPrint/OctoPrint"
- name: 🐍 Set up Python ${{ matrix.python }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: πŸ‘· Build and install latest release
diff --git a/AUTHORS.md b/AUTHORS.md
index 3fe4458b3..c491b2bc9 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -172,6 +172,7 @@ date of first contribution):
* [Dawid Pieper](https://github.com/dawidpieper)
* ["arrdem"](https://github.com/arrdem)
* [Arkadiusz MiΕ›kiewicz](https://github.com/arekm)
+ * ["tempodat"](https://github.com/tempodat)
* [Frederik Kemner](https://github.com/040medien)
OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by
diff --git a/SECURITY.md b/SECURITY.md
index 79b94cdd9..0722f9a3c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,11 +1,3 @@
# Security Policy
-## Supported Versions
-
-Always update to the latest version of OctoPrint to keep up with security patches.
-
-## Reporting a Vulnerability
-
-Email to security@octoprint.org.
-
-For the sake of the userbase of OctoPrint please always disclose responsibly and with a 90+ day window.
+OctoPrint's security policy can be found [here](https://octoprint.org/security/).
diff --git a/src/octoprint/plugins/gcodeviewer/__init__.py b/src/octoprint/plugins/gcodeviewer/__init__.py
index a72b1f860..a145c3887 100644
--- a/src/octoprint/plugins/gcodeviewer/__init__.py
+++ b/src/octoprint/plugins/gcodeviewer/__init__.py
@@ -1,15 +1,20 @@
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
__copyright__ = "Copyright (C) 2020 The OctoPrint Project - Released under terms of the AGPLv3 License"
+import os
+
+import flask
from flask_babel import gettext
import octoprint.plugin
+from octoprint.util.files import search_through_file
class GcodeviewerPlugin(
octoprint.plugin.AssetPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin,
+ octoprint.plugin.BlueprintPlugin,
):
def get_assets(self):
js = [
@@ -70,6 +75,30 @@ class GcodeviewerPlugin(
self._settings.set_int(["sizeThreshold"], config["sizeThreshold"])
self._settings.global_remove(["gcodeViewer"])
+ @octoprint.plugin.BlueprintPlugin.route(
+ "/skipuntilcheck/<string:origin>/<path:filename>", methods=["GET"]
+ )
+ def check_skip_until_presence(self, origin, filename):
+ try:
+ path = self._file_manager.path_on_disk(origin, filename)
+ except NotImplementedError:
+ # storage doesn't support path on disk
+ flask.abort(404)
+
+ if not os.path.exists(path):
+ # path doesn't exist
+ flask.abort(404)
+
+ skipUntilThis = self._settings.get(["skipUntilThis"])
+ if not skipUntilThis:
+ # no skipUntilThis, no need to search, shortcut
+ return flask.jsonify(present=False)
+
+ return flask.jsonify(present=search_through_file(path, skipUntilThis))
+
+ def is_blueprint_csrf_protected(self):
+ return True
+
__plugin_name__ = gettext("GCode Viewer")
__plugin_author__ = "Gina HÀußge"
diff --git a/src/octoprint/plugins/gcodeviewer/static/js/gcodeviewer.js b/src/octoprint/plugins/gcodeviewer/static/js/gcodeviewer.js
index aaf38a58d..23d1c8552 100644
--- a/src/octoprint/plugins/gcodeviewer/static/js/gcodeviewer.js
+++ b/src/octoprint/plugins/gcodeviewer/static/js/gcodeviewer.js
@@ -16,14 +16,6 @@ $(function () {
self.ui_progress_text = ko.pureComputed(function () {
var text = "";
switch (self.ui_progress_type()) {
- case "downloading": {
- text = gettext("Downloading...");
- break;
- }
- case "splitting": {
- text = gettext("Splitting lines...");
- break;
- }
case "parsing": {
text =
gettext("Parsing...") +
@@ -396,6 +388,7 @@ $(function () {
onProgress: self._onProgress,
onModelLoaded: self._onModelLoaded,
onLayerSelected: self._onLayerSelected,
+ onFileLoaded: self._onFileLoaded,
bed: self._retrieveBedDimensions(),
toolOffsets: self._retrieveToolOffsets(),
invertAxes: self._retrieveAxesConfiguration(),
@@ -517,53 +510,35 @@ $(function () {
self.needsLoad = false;
if (self.status === "idle" && self.errorCount < 3) {
self.status = "request";
- self._onProgress("downloading");
- OctoPrint.files
- .download("local", path)
- .done(function (response, rstatus) {
- if (rstatus === "success") {
- self.showGCodeViewer(response, rstatus);
- self.loadedFilepath = path;
- self.loadedFileDate = date;
- self.status = "idle";
- self.enableReload(true);
- }
- })
- .fail(function () {
- self.status = "idle";
- self.errorCount++;
- });
- }
- };
- self.showGCodeViewer = function (response, rstatus) {
- // Slice of the gcode
- var findThis = self.settings.settings.plugins.gcodeviewer.skipUntilThis();
- if (findThis && findThis !== "") {
- var indexPos = response.indexOf("\n" + findThis);
- if (indexPos !== -1) {
- // Slice and make sure we comment out any string left back after slicing - so if a user configures something like "G1" we dont end up with a snippet of gcode commands
- // Yes it would be prettier to parse it line by line and remove the entire line, that is very slow and uses mem - this way we find the string, and remove it
- response = ";" + response.slice(indexPos + findThis.length + 1);
+ self.cachedPath = path;
+ self.cachedDate = date;
+ var par = {
+ url: OctoPrint.files.downloadPath("local", path),
+ path: path,
+ skipUntil: self.settings.settings.plugins.gcodeviewer.skipUntilThis()
+ };
+
+ GCODE.renderer.clear();
+ self._onProgress("parsing");
+ GCODE.gCodeReader.loadFile(par);
+
+ if (self.layerSlider !== undefined) {
+ self.layerSlider.slider("disable");
}
- }
- var par = {
- target: {
- result: response
+ if (self.layerCommandSlider !== undefined) {
+ self.layerCommandSlider.slider("disable");
}
- };
- GCODE.renderer.clear();
- self._onProgress("splitting");
- GCODE.gCodeReader.loadFile(par);
-
- if (self.layerSlider !== undefined) {
- self.layerSlider.slider("disable");
- }
- if (self.layerCommandSlider !== undefined) {
- self.layerCommandSlider.slider("disable");
}
};
+ self._onFileLoaded = function () {
+ self.loadedFilepath = self.cachedPath;
+ self.loadedFileDate = self.cachedDate;
+ self.status = "idle";
+ self.enableReload(true);
+ };
+
self.reload = function () {
if (!self.enableReload()) return;
self.loadFile(self.loadedFilepath, self.loadedFileDate);
diff --git a/src/octoprint/plugins/gcodeviewer/static/js/viewer/reader.js b/src/octoprint/plugins/gcodeviewer/static/js/viewer/reader.js
index 7afd71cc8..31df5d3ab 100644
--- a/src/octoprint/plugins/gcodeviewer/static/js/viewer/reader.js
+++ b/src/octoprint/plugins/gcodeviewer/static/js/viewer/reader.js
@@ -210,25 +210,6 @@ GCODE.gCodeReader = (function () {
loadFile: function (reader) {
this.clear();
- var totalSize = reader.target.result.length;
-
- /*
- * Split by line ending
- *
- * Be aware that for windows line endings \r\n this leaves the \r attached to
- * the lines. That will not influence our parser, but makes file position
- * calculation way easier (line length + 1), so we just leave it in.
- *
- * This cannot cope with old MacOS \r line endings, but those should
- * really not be used anymore and thus we'll happily ignore them here.
- *
- * Note: A simple string split uses up *much* less memory than regex.
- */
- lines = reader.target.result.split("\n");
-
- reader.target.result = null;
- prepareGCode(totalSize);
-
var mustCompress =
gCodeOptions["forceCompression"] ||
gCodeOptions["alwaysCompress"] ||
@@ -236,9 +217,11 @@ GCODE.gCodeReader = (function () {
gCodeOptions["compressionSizeThreshold"] <= totalSize);
GCODE.ui.worker.postMessage({
- cmd: "parseGCode",
+ cmd: "downloadAndParseGCode",
msg: {
- gcode: gcode,
+ url: reader.url,
+ path: reader.path,
+ skipUntil: reader.skipUntil,
options: {
firstReport: 5,
toolOffsets: gCodeOptions["toolOffsets"],
diff --git a/src/octoprint/plugins/gcodeviewer/static/js/viewer/ui.js b/src/octoprint/plugins/gcodeviewer/static/js/viewer/ui.js
index 3e9672f22..9fa1494ae 100644
--- a/src/octoprint/plugins/gcodeviewer/static/js/viewer/ui.js
+++ b/src/octoprint/plugins/gcodeviewer/static/js/viewer/ui.js
@@ -13,7 +13,8 @@ GCODE.ui = (function () {
bedDimensions: undefined,
onProgress: undefined,
onModelLoaded: undefined,
- onLayerSelected: undefined
+ onLayerSelected: undefined,
+ onFileLoaded: undefined
};
var setProgress = function (type, progress) {
@@ -49,6 +50,9 @@ GCODE.ui = (function () {
var data = e.data;
switch (data.cmd) {
case "returnModel":
+ if (uiOptions["onFileLoaded"]) {
+ uiOptions.onFileLoaded();
+ }
GCODE.ui.worker.postMessage({
cmd: "analyzeModel",
msg: {}
diff --git a/src/octoprint/plugins/gcodeviewer/static/js/viewer/worker.js b/src/octoprint/plugins/gcodeviewer/static/js/viewer/worker.js
index dcdd0c69b..c1be5700c 100644
--- a/src/octoprint/plugins/gcodeviewer/static/js/viewer/worker.js
+++ b/src/octoprint/plugins/gcodeviewer/static/js/viewer/worker.js
@@ -4,7 +4,10 @@
* Time: 12:18 PM
*/
-var gcode;
+// raw path suitable for fetch()
+var url;
+// path relative to Local
+var path;
var firstReport;
var toolOffsets = [{x: 0, y: 0}];
var g90InfluencesExtruder = false;
@@ -40,6 +43,8 @@ var emptyLayers = [];
var percentageByLayer = [];
var mustCompress = false;
+var skipUntil = null;
+var skipUntilPresent = false;
importScripts("../lib/pako.js");
@@ -337,7 +342,73 @@ var analyzeModel = function () {
sendAnalyzeDone();
};
-var doParse = function () {
+var gCodeLineGenerator = async function* (fileURL) {
+ const utf8Decoder = new TextDecoder("utf-8");
+ const response = await fetch(fileURL);
+
+ // the download failed.
+ if (!response.ok) return;
+
+ // create a reader object that will read the incoming data
+ const reader = response.body.getReader();
+
+ // we use these two variables to calculate the percentage.
+ var totalDownloadLength = response.headers.get("content-length");
+ var currentDownloadLength = 0;
+
+ // lets read a first data chunk
+ let {value: chunk, done: readerDone} = await reader.read();
+ chunk = chunk ? utf8Decoder.decode(chunk) : "";
+
+ // some init
+ const re = /\n|\r|\r\n/gm;
+ let startIndex = 0;
+ let result;
+
+ // now continue until all the downloaded data is processed.
+ for (;;) {
+ // cut at a new line
+ let result = re.exec(chunk);
+ if (!result) {
+ // there was not a complete line, what is up?
+
+ if (readerDone) {
+ // we reached the end of the file.
+ break;
+ }
+ // lets read a new chunk
+ let remainder = chunk.substr(startIndex);
+ ({value: chunk, done: readerDone} = await reader.read());
+ // concatenate with our leftovers from the previous chunk
+ chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : "");
+ // reset the indexes
+ startIndex = re.lastIndex = 0;
+ continue;
+ }
+ // we use re.lastIndex and not result.index so we include
+ // the actual terminator.
+ currentDownloadLength += re.lastIndex - startIndex;
+ // return the data to the caller
+ yield [
+ chunk.substring(startIndex, result.index),
+ (100 * currentDownloadLength) / totalDownloadLength
+ ];
+
+ // move to after the line we just returned
+ startIndex = re.lastIndex;
+ }
+ if (startIndex < chunk.length) {
+ // last line didn't end in a newline char
+ currentDownloadLength += chunk.length - startIndex;
+ // return the data to the caller
+ yield [
+ chunk.substr(startIndex),
+ (100 * currentDownloadLength) / totalDownloadLength
+ ];
+ }
+};
+
+var doParse = async function () {
var argChar, numSlice;
var activeLayer = undefined;
var sendLayer = undefined;
@@ -376,10 +447,23 @@ var doParse = function () {
// visualizer doesn't actually have a physical offset ;)
var activeToolOffset = toolOffsets[0];
+ // skipUntil preparations
+ var skipUntilFound = false;
+
var i, j, args;
+ // if skipUntil is set, get skipUntilPresent
+ skipUntilPresent = false;
+ if (skipUntil !== undefined && skipUntil !== "") {
+ result = await fetch("/plugin/gcodeviewer/skipuntilcheck/local/" + path);
+ if (result.ok) {
+ response = await result.json();
+ skipUntilPresent = response.present;
+ }
+ }
+
model = [];
- for (i = 0; i < gcode.length; i++) {
+ for await (let [line, percentage] of gCodeLineGenerator(url)) {
x = undefined;
y = undefined;
z = undefined;
@@ -388,8 +472,11 @@ var doParse = function () {
center_j = undefined;
direction = undefined;
- var line = gcode[i].line;
- var percentage = gcode[i].percentage;
+ // find the skipUntil if it is present
+ // we do not actually remove the line, it is parsed normally.
+ if (skipUntilPresent && !skipUntilFound && line.startsWith(skipUntil)) {
+ skipUntilFound = true;
+ }
extrude = false;
line = line.split(/[\(;]/)[0];
@@ -646,7 +733,9 @@ var doParse = function () {
zLift = false;
}
- if (addToModel) {
+ // if skipUntilPresent is true, we will not add anything to
+ // the model until the skipUntil string is found.
+ if (addToModel && (!skipUntilPresent || skipUntilFound)) {
if (!model[layer]) model[layer] = [];
if (model[layer] instanceof Uint8Array)
model[layer] = decompress(model[layer]);
@@ -748,9 +837,9 @@ var doParse = function () {
}
if (typeof sendLayer !== "undefined") {
- if (i - lastSend > gcode.length * 0.02 && sendMultiLayer.length !== 0) {
- lastSend = i;
- sendLayersToParent(sendMultiLayer, (i / gcode.length) * 100);
+ if (percentage - lastSend > 2 && sendMultiLayer.length !== 0) {
+ lastSend = percentage;
+ sendLayersToParent(sendMultiLayer, percentage);
sendMultiLayer = [];
sendMultiLayerZ = [];
}
@@ -770,8 +859,9 @@ var doParse = function () {
sendLayersToParent(sendMultiLayer, 100);
};
-var parseGCode = function (message) {
- gcode = message.gcode;
+var parseGCode = async function (message) {
+ url = message.url;
+ path = message.path;
firstReport = message.options.firstReport;
toolOffsets = message.options.toolOffsets;
if (!toolOffsets || toolOffsets.length === 0) toolOffsets = [{x: 0, y: 0}];
@@ -780,9 +870,9 @@ var parseGCode = function (message) {
g90InfluencesExtruder = message.options.g90InfluencesExtruder;
boundingBox.minZ = min.z = message.options.bedZ;
mustCompress = message.options.compress;
+ skipUntil = message.skipUntil;
- doParse();
- gcode = [];
+ await doParse();
self.postMessage({
cmd: "returnModel",
msg: {}
@@ -827,7 +917,7 @@ onmessage = function (e) {
var data = e.data;
// for some reason firefox doesn't garbage collect when something inside closures is deleted, so we delete and recreate whole object eaech time
switch (data.cmd) {
- case "parseGCode":
+ case "downloadAndParseGCode":
parseGCode(data.msg);
break;
case "setOption":
diff --git a/src/octoprint/plugins/gcodeviewer/templates/gcodeviewer_settings.jinja2 b/src/octoprint/plugins/gcodeviewer/templates/gcodeviewer_settings.jinja2
index a83b4a90a..0fc5342f8 100644
--- a/src/octoprint/plugins/gcodeviewer/templates/gcodeviewer_settings.jinja2
+++ b/src/octoprint/plugins/gcodeviewer/templates/gcodeviewer_settings.jinja2
@@ -7,9 +7,6 @@
<span class="help-block">
{% trans %}If provided, the GCode Viewer will search for a line beginning with this string. If found, all GCode up to that point will be skipped. This can be used to skip "purge/nozzle wipe" GCode in the viewer. Note the search is case sensitive.{% endtrans %}
</span>
- <span class="help-block">
- {% trans %}<strong>Important!</strong> Make sure that any setup codes (especially things like <code>G20</code>/<code>G21</code>, <code>G90</code>/<code>G91</code>, <code>M82</code>/<code>M83</code>) are <strong>not</strong> skipped, or your files might not get rendered correctly! If things look broken with a skip configuration, you are skipping too much.{% endtrans %}
- </span>
</div>
</div>
<div class="control-group" title="{{ _('Maximum size for which the GCode Viewer autoloads the file for preview')|edq }}">
diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py
index 1063aa5d1..a32123038 100644
--- a/src/octoprint/plugins/pluginmanager/__init__.py
+++ b/src/octoprint/plugins/pluginmanager/__init__.py
@@ -33,7 +33,7 @@ from octoprint.server.util.flask import (
with_revalidation_checking,
)
from octoprint.settings import valid_boolean_trues
-from octoprint.util import deprecated, to_bytes
+from octoprint.util import RepeatedTimer, deprecated, to_bytes
from octoprint.util.net import download_file
from octoprint.util.pip import (
OUTPUT_SUCCESS,
@@ -193,6 +193,10 @@ class PluginManagerPlugin(
self._install_task = None
self._install_lock = threading.RLock()
+ self._queued_installs = []
+ self._queued_installs_abort_timer = None
+ self._print_cancelled = False
+
def initialize(self):
self._console_logger = logging.getLogger(
"octoprint.plugins.pluginmanager.console"
@@ -619,7 +623,42 @@ class PluginManagerPlugin(
def on_event(self, event, payload):
from octoprint.events import Events
- if (
+ if event == Events.PRINT_STARTED:
+ self._queued_installs_timer_stop()
+ self._print_cancelled = False
+ elif (
+ event == Events.PRINT_DONE
+ and self._settings.global_get(["webcam", "timelapse", "type"]) == "off"
+ and len(self._queued_installs) > 0
+ ):
+ self._queued_installs_timer_start()
+ elif event == Events.PRINT_FAILED and len(self._queued_installs) > 0:
+ self._send_result_notification(
+ "queued_installs",
+ {
+ "type": "queued_installs",
+ "print_failed": True,
+ "queued": self._queued_installs,
+ },
+ )
+ self._print_cancelled = True
+ elif (
+ event == Events.MOVIE_DONE
+ and self._settings.global_get(["webcam", "timelapse", "type"]) != "off"
+ and len(self._queued_installs) > 0
+ and not (self._printer.is_printing() or self._printer.is_paused())
+ and not self._print_cancelled
+ ):
+ self._queued_installs_timer_start()
+ elif event == Events.USER_LOGGED_IN and len(self._queued_installs) > 0:
+ self._send_result_notification(
+ "queued_installs",
+ {
+ "type": "queued_installs",
+ "queued": self._queued_installs,
+ },
+ )
+ elif (
event != Events.CONNECTIVITY_CHANGED
or not payload
or not payload.get("new", False)
@@ -627,6 +666,53 @@ class PluginManagerPlugin(
return
self._fetch_all_data(do_async=True)
+ def _queued_installs_timer_start(self):
+ if self._queued_installs_abort_timer is not None:
+ return
+
+ self._logger.debug("Starting queued updates timer.")
+
+ self._timeout_value = 60
+ self._queued_installs_abort_timer = RepeatedTimer(
+ 1, self._queued_installs_timer_task
+ )
+ self._queued_installs_abort_timer.start()
+
+ def _queued_installs_timer_stop(self):
+ if self._queued_installs_abort_timer is not None:
+ self._queued_installs_abort_timer.cancel()
+ self._queued_installs_abort_timer = None
+ self._send_result_notification(
+ "queued_installs",
+ {
+ "type": "queued_installs",
+ "queued": self._queued_installs,
+ "timeout_value": -1,
+ },
+ )
+
+ def _queued_installs_timer_task(self):
+ if self._timeout_value is None:
+ return
+
+ self._timeout_value -= 1
+ self._send_result_notification(
+ "queued_installs",
+ {
+ "type": "queued_installs",
+ "queued": self._queued_installs,
+ "timeout_value": self._timeout_value,
+ },
+ )
+ if self._timeout_value <= 0:
+ if self._queued_installs_abort_timer is not None:
+ self._queued_installs_abort_timer.cancel()
+ self._queued_installs_abort_timer = None
+ for plugin in self._queued_installs:
+ plugin.pop("command")
+ self.command_install(**plugin)
+ self._queued_installs = []
+
##~~ SimpleApiPlugin
def get_api_commands(self):
@@ -638,17 +724,48 @@ class PluginManagerPlugin(
"cleanup": ["plugin"],
"cleanup_all": [],
"refresh_repository": [],
+ "clear_queued_plugin": ["plugin"],
+ "clear_queued_installs": [],
}
def on_api_command(self, command, data):
if not Permissions.PLUGIN_PLUGINMANAGER_MANAGE.can():
abort(403)
- if self._printer.is_printing() or self._printer.is_paused():
+ if command == "clear_queued_plugin":
+ if data["plugin"] and data["plugin"] in self._queued_installs:
+ self._queued_installs.remove(data["plugin"])
+ return (
+ jsonify({"queued_installs": self._queued_installs}),
+ 202,
+ )
+
+ elif command == "clear_queued_installs":
+ self._queued_installs.clear()
+ return (
+ jsonify({"queued_installs": self._queued_installs}),
+ 202,
+ )
+
+ elif self._printer.is_printing() or self._printer.is_paused():
# do not update while a print job is running
- abort(409, description="Printer is currently printing or paused")
+ # store targets to be run later on print done event
+ if command == "install" and data not in self._queued_installs:
+ self._logger.debug(f"Queuing install of {data}")
+ self._queued_installs.append(data)
+ if len(self._queued_installs) > 0:
+ self._logger.debug(f"Queued installs: {self._queued_installs}")
+ return (
+ jsonify({"queued_installs": self._queued_installs}),
+ 202,
+ )
+ else:
+ abort(
+ 409,
+ description="Printer is currently printing or paused and install could not be queued",
+ )
- if command == "install":
+ elif command == "install":
if not Permissions.PLUGIN_PLUGINMANAGER_INSTALL.can():
abort(403)
url = data["url"]
diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
index 65efd2101..6afae8e33 100644
--- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
+++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
@@ -358,7 +358,6 @@ $(function () {
self.enableRepoInstall = function (data) {
return (
- self.enableManagement() &&
self.pipAvailable() &&
!self.safeMode() &&
!self.throttled() &&
@@ -533,6 +532,107 @@ $(function () {
}
};
+ self.multiInstallQueue = ko.observableArray([]);
+ self.queuedInstalls = ko.observableArray([]);
+ self.multiInstallRunning = ko.observable(false);
+ self.multiInstallInitialSize = ko.observable(0);
+
+ self.multiInstallValid = function () {
+ return (
+ self.loginState.hasPermission(
+ self.access.permissions.PLUGIN_PLUGINMANAGER_INSTALL
+ ) &&
+ self.pipAvailable() &&
+ !self.safeMode() &&
+ !self.throttled() &&
+ self.online() &&
+ self.multiInstallQueue().length > 0 &&
+ self.multiInstallQueue().every(self.isCompatible)
+ );
+ };
+
+ self.repoInstallSelectedButtonText = function () {
+ return self.multiInstallQueue().some(self.installed)
+ ? "(Re)install selected"
+ : "Install selected";
+ };
+
+ self.repoInstallSelectedConfirm = function () {
+ if (!self.multiInstallValid()) return;
+
+ if (self.multiInstallQueue().length === 1) {
+ self.installFromRepository(self.multiInstallQueue()[0]);
+ return;
+ }
+
+ var question = "<ul>";
+ self.multiInstallQueue().forEach(function (plugin) {
+ var action = self.installed(plugin)
+ ? gettext("Reinstall")
+ : gettext("Install");
+
+ question += _.sprintf(
+ "<li>%(action)s <em><b>%(name)s@%(version)s</b></em></li>",
+ {
+ action: _.escape(action),
+ name: _.escape(plugin.title),
+ version: _.escape(plugin.github.latest_release.tag)
+ }
+ );
+ });
+ question += "</ul>";
+
+ showConfirmationDialog({
+ title: gettext("Confirm installation of multiple plugins"),
+ message: gettext("Please confirm you want to perform these actions:"),
+ question: question,
+ cancel: gettext("Cancel"),
+ proceed: gettext("Install"),
+ proceedClass: "primary",
+ onproceed: self.startMultiInstall
+ });
+ };
+
+ self.startMultiInstall = function () {
+ if (self.multiInstallRunning() || !self.multiInstallValid()) return;
+
+ self.multiInstallRunning(true);
+ self.multiInstallInitialSize(self.multiInstallQueue().length);
+
+ self._markWorking(
+ gettext("Installing multiple plugins"),
+ gettext("Starting installation of multiple plugins...")
+ );
+ self.performMultiInstallJob();
+ };
+
+ self.performMultiInstallJob = function () {
+ if (!self.multiInstallRunning() || self.multiInstallQueue().length === 0)
+ return;
+
+ var plugin = self.multiInstallQueue.pop();
+
+ self.installFromRepository(plugin);
+ };
+
+ self.alertMultiInstallJobDone = function (response) {
+ if (
+ !self.multiInstallRunning() ||
+ response.action != "install" ||
+ !response.result
+ )
+ return;
+
+ if (self.multiInstallQueue().length === 0) {
+ self.installUrl("");
+ self.multiInstallQueue([]);
+ self.multiInstallRunning(false);
+ self._markDone();
+ } else {
+ self.performMultiInstallJob();
+ }
+ };
+
self.performRepositorySearch = function () {
var query = self.repositorySearchQuery();
if (query !== undefined && query.trim() !== "") {
@@ -1050,10 +1150,6 @@ $(function () {
return;
}
- if (!self.enableManagement()) {
- return;
- }
-
self.installPlugin(
data.archive,
data.title,
@@ -1062,6 +1158,22 @@ $(function () {
);
};
+ self.removeFromQueue = function (plugin) {
+ var data = {
+ plugin: {
+ command: self.installed(plugin) ? "reinstall" : "install",
+ url: plugin.archive,
+ dependency_links:
+ plugin.follow_dependency_links || self.followDependencyLinks()
+ }
+ };
+ OctoPrint.simpleApiCommand("pluginmanager", "clear_queued_plugin", data).done(
+ function (response) {
+ self.queuedInstalls(response.queued_installs);
+ }
+ );
+ };
+
self.installPlugin = function (url, name, reinstall, followDependencyLinks) {
if (
!self.loginState.hasPermission(
@@ -1071,10 +1183,6 @@ $(function () {
return;
}
- if (!self.enableManagement()) {
- return;
- }
-
if (self.throttled()) {
return;
}
@@ -1108,10 +1216,59 @@ $(function () {
{url: _.escape(url), name: _.escape(name)}
);
}
+
+ if (self.multiInstallRunning()) {
+ workTitle =
+ _.sprintf("[%(index)d/%(total)d] ", {
+ index:
+ this.multiInstallInitialSize() -
+ self.multiInstallQueue().length,
+ total: this.multiInstallInitialSize()
+ }) + workTitle;
+ }
+
self._markWorking(workTitle, workText);
var onSuccess = function (response) {
self.installUrl("");
+ if (response.hasOwnProperty("queued_installs")) {
+ self.queuedInstalls(response.queued_installs);
+ var text =
+ '<div class="row-fluid"><p>' +
+ gettext("The following plugins are queued to be installed.") +
+ "</p><ul><li>" +
+ _.map(response.queued_installs, function (info) {
+ var plugin = ko.utils.arrayFirst(
+ self.repositoryplugins.paginatedItems(),
+ function (item) {
+ return item.archive === info.url;
+ }
+ );
+ return plugin.title;
+ }).join("</li><li>") +
+ "</li></ul></div>";
+ if (typeof self.installQueuePopup !== "undefined") {
+ self.installQueuePopup.update({
+ text: text
+ });
+ if (self.installQueuePopup.state === "closed") {
+ self.installQueuePopup.open();
+ }
+ } else {
+ self.installQueuePopup = new PNotify({
+ title: gettext("Plugin installs queued"),
+ text: text,
+ type: "notice"
+ });
+ }
+ if (self.multiInstallQueue().length > 0) {
+ self.performMultiInstallJob();
+ } else {
+ self.multiInstallRunning(false);
+ self.workingDialog.modal("hide");
+ self._markDone();
+ }
+ }
},
onError = function (jqXHR) {
if (jqXHR.status === 409) {
@@ -1429,19 +1586,58 @@ $(function () {
);
};
+ self.installButtonAction = function (data) {
+ if (self.enableRepoInstall(data)) {
+ if (!self.installQueued(data)) {
+ self.installFromRepository(data);
+ } else {
+ self.removeFromQueue(data);
+ }
+ } else {
+ return false;
+ }
+ };
+
self.installButtonText = function (data) {
- return self.isCompatible(data)
- ? self.installed(data)
- ? gettext("Reinstall")
- : gettext("Install")
- : data.disabled
- ? gettext("Disabled")
- : gettext("Incompatible");
+ if (!self.isCompatible(data)) {
+ if (data.disabled) {
+ return gettext("Disabled");
+ } else {
+ return gettext("Incompatible");
+ }
+ }
+
+ if (self.installQueued(data)) {
+ return gettext("Dequeue");
+ } else if (self.installed(data)) {
+ return gettext("Reinstall");
+ } else {
+ return gettext("Install");
+ }
};
self._processPluginManagementResult = function (response, action, plugin) {
if (response.result) {
- self._markDone();
+ if (self.queuedInstalls().length > 0 && action === "install") {
+ var plugin_dequeue = ko.utils.arrayFirst(
+ self.queuedInstalls(),
+ function (item) {
+ return item.url === response.source;
+ }
+ );
+ if (plugin_dequeue) {
+ self.queuedInstalls.remove(plugin_dequeue);
+ }
+ if (self.queuedInstalls().length === 0) {
+ self.multiInstallRunning(false);
+ self._markDone();
+ }
+ } else if (self.multiInstallRunning() && action === "install") {
+ // A MultiInstall job has finished
+ self.alertMultiInstallJobDone(response);
+ } else {
+ self._markDone();
+ }
} else {
self._markDone(response.reason, response.faq);
}
@@ -1541,7 +1737,7 @@ $(function () {
"</p>";
type = "warning";
- if (self.restartCommandSpec) {
+ if (self.restartCommandSpec && !self.multiInstallRunning()) {
var restartClicked = false;
confirm = {
confirm: true,
@@ -1596,20 +1792,22 @@ $(function () {
"</p>";
type = "warning";
- var refreshClicked = false;
- confirm = {
- confirm: true,
- buttons: [
- {
- text: gettext("Reload now"),
- click: function () {
- if (refreshClicked) return;
- refreshClicked = true;
- location.reload(true);
+ if (!self.multiInstallRunning()) {
+ var refreshClicked = false;
+ confirm = {
+ confirm: true,
+ buttons: [
+ {
+ text: gettext("Reload now"),
+ click: function () {
+ if (refreshClicked) return;
+ refreshClicked = true;
+ location.reload(true);
+ }
}
- }
- ]
- };
+ ]
+ };
+ }
} else if (self.logContents.action_reconnect) {
text +=
"<p>" +
@@ -2034,7 +2232,10 @@ $(function () {
var messageType = data.type;
- if (messageType === "loglines" && self.working()) {
+ if (
+ messageType === "loglines" &&
+ (self.working() || self.queuedInstalls().length > 0)
+ ) {
_.each(data.loglines, function (line) {
self.loglines.push(self._preprocessLine(line));
});
@@ -2054,9 +2255,210 @@ $(function () {
self._processPluginManagementResult(data, action, name);
self.requestPluginData();
+ } else if (messageType === "queued_installs") {
+ if (data.hasOwnProperty("queued")) {
+ self.queuedInstalls(data.queued);
+ var queuedInstallsPopupOptions = {
+ title: gettext("Queued Installs"),
+ text: "",
+ type: "notice",
+ icon: false,
+ hide: false,
+ buttons: {
+ closer: false,
+ sticker: false
+ },
+ history: {
+ history: false
+ }
+ };
+
+ if (data.print_failed && data.queued.length > 0) {
+ queuedInstallsPopupOptions.title = gettext(
+ "Queued Installs Paused"
+ );
+ queuedInstallsPopupOptions.text =
+ '<div class="row-fluid"><p>' +
+ gettext("The following plugins are queued to be installed.") +
+ "</p><ul><li>" +
+ _.map(self.queuedInstalls(), function (info) {
+ var plugin = ko.utils.arrayFirst(
+ self.repositoryplugins.paginatedItems(),
+ function (item) {
+ return item.archive === info.url;
+ }
+ );
+ return plugin.title;
+ }).join("</li><li>") +
+ "</li></ul></div>";
+ queuedInstallsPopupOptions.confirm = {
+ confirm: true,
+ buttons: [
+ {
+ text: gettext("Continue Installs"),
+ addClass: "btn-block btn-primary",
+ promptTrigger: true,
+ click: function (notice, value) {
+ notice.remove();
+ notice
+ .get()
+ .trigger("pnotify.continue", [notice, value]);
+ }
+ },
+ {
+ text: gettext("Cancel Installs"),
+ addClass: "btn-block btn-danger",
+ promptTrigger: true,
+ click: function (notice, value) {
+ notice.remove();
+ notice
+ .get()
+ .trigger("pnotify.cancel", [notice, value]);
+ }
+ }
+ ]
+ };
+ } else if (
+ data.hasOwnProperty("timeout_value") &&
+ data.timeout_value > 0 &&
+ data.queued.length > 0
+ ) {
+ var progress_percent = Math.floor(
+ (data.timeout_value / 60) * 100
+ );
+ var progress_class =
+ progress_percent < 25
+ ? "progress-danger"
+ : progress_percent > 75
+ ? "progress-success"
+ : "progress-warning";
+ var countdownText = _.sprintf(
+ gettext("Installing in %(sec)i secs..."),
+ {
+ sec: data.timeout_value
+ }
+ );
+
+ queuedInstallsPopupOptions.title = gettext(
+ "Starting Queued Installs"
+ );
+ queuedInstallsPopupOptions.text =
+ '<div class="row-fluid"><p>' +
+ gettext("The following plugins are going to be installed.") +
+ "</p><ul><li>" +
+ _.map(self.queuedInstalls(), function (info) {
+ var plugin = ko.utils.arrayFirst(
+ self.repositoryplugins.paginatedItems(),
+ function (item) {
+ return item.archive === info.url;
+ }
+ );
+ return plugin.title;
+ }).join("</li><li>") +
+ '</li></ul></p></div><div class="progress progress-softwareupdate ' +
+ progress_class +
+ '"><div class="bar">' +
+ countdownText +
+ '</div><div class="progress-text" style="clip-path: inset(0 0 0 ' +
+ progress_percent +
+ "%);-webkit-clip-path: inset(0 0 0 " +
+ progress_percent +
+ '%);">' +
+ countdownText +
+ "</div></div>";
+ queuedInstallsPopupOptions.confirm = {
+ confirm: true,
+ buttons: [
+ {
+ text: gettext("Cancel Installs"),
+ addClass: "btn-block btn-danger",
+ promptTrigger: true,
+ click: function (notice, value) {
+ notice.remove();
+ notice
+ .get()
+ .trigger("pnotify.cancel", [notice, value]);
+ }
+ },
+ {
+ text: "",
+ addClass: "hidden"
+ }
+ ]
+ };
+ } else if (
+ data.hasOwnProperty("timeout_value") &&
+ data.timeout_value === 0 &&
+ data.queued.length > 0
+ ) {
+ self.multiInstallRunning(true);
+ self._markWorking(
+ gettext("Installing queued plugins"),
+ gettext("Starting installation of multiple plugins...")
+ );
+ self.queuedInstallsPopup.remove();
+ self.queuedInstallsPopup = undefined;
+ return;
+ } else {
+ if (typeof self.queuedInstallsPopup !== "undefined") {
+ self.queuedInstallsPopup.remove();
+ self.queuedInstallsPopup = undefined;
+ }
+ return;
+ }
+
+ if (typeof self.queuedInstallsPopup !== "undefined") {
+ self.queuedInstallsPopup.update(queuedInstallsPopupOptions);
+ } else {
+ self.queuedInstallsPopup = new PNotify(
+ queuedInstallsPopupOptions
+ );
+ self.queuedInstallsPopup.get().on("pnotify.cancel", function () {
+ self.queuedInstallsPopup = undefined;
+ self.cancelQueuedInstalls();
+ });
+ self.queuedInstallsPopup
+ .get()
+ .on("pnotify.continue", function () {
+ self.queuedInstallsPopup = undefined;
+ self.performQueuedInstalls();
+ });
+ }
+ }
}
};
+ self.cancelQueuedInstalls = function () {
+ OctoPrint.simpleApiCommand("pluginmanager", "clear_queued_installs", {}).done(
+ function (response) {
+ self.queuedInstalls(response.queued_installs);
+ }
+ );
+ };
+
+ self.installQueued = function (plugin) {
+ var plugin_queued = ko.utils.arrayFirst(
+ self.queuedInstalls(),
+ function (item) {
+ return item.url === plugin.archive;
+ }
+ );
+ return typeof plugin_queued !== "undefined";
+ };
+
+ self.performQueuedInstalls = function () {
+ self.queuedInstalls().forEach(function (plugin) {
+ var queued_plugin = ko.utils.arrayFirst(
+ self.repositoryplugins.paginatedItems(),
+ function (item) {
+ return plugin.url === item.archive;
+ }
+ );
+ self.multiInstallQueue.push(queued_plugin);
+ });
+ self.startMultiInstall();
+ };
+
self._forcedStdoutLine =
/You are using pip version .*?, however version .*? is available\.|You should consider upgrading via the '.*?' command\./;
self._preprocessLine = function (line) {
diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
index 49bcf6705..5486d4de2 100644
--- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
+++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
@@ -1,6 +1,6 @@
{% macro pluginmanager_printing() %}
<div class="alert" data-bind="visible: !enableManagement()">
- {{ _('Take note that all plugin management functionality is disabled while your printer is printing or paused.') }}
+ {{ _('Take note that most plugin management functionality is disabled while your printer is printing or paused.') }}
</div>
{% endmacro %}
@@ -225,7 +225,6 @@
</div>
<div class="modal-body">
<div class="full-sized-box">
- {{ pluginmanager_printing() }}
{{ pluginmanager_nopip() }}
{{ pluginmanager_safemode() }}
{{ pluginmanager_throttled() }}
@@ -260,14 +259,23 @@
</div>
</div>
</div>
-
+ <div style="text-align: center">
+ <div style="margin: 0 auto;">
+ <span class="muted" style="margin-right: 8px;" data-bind="text: multiInstallQueue().length + ' selected'"></span>
+ <button class="btn btn-primary" data-bind="enable: multiInstallValid(), click: repoInstallSelectedConfirm, text: repoInstallSelectedButtonText()"></button>
+ </div>
+ </div>
+ <hr />
<div data-bind="visible: repositoryAvailable() && repositoryplugins.paginatedItems().length > 0">
<div id="settings_plugin_pluginmanager_repositorydialog_list" data-bind="foreach: repositoryplugins.paginatedItems">
<div class="entry">
<div class="row-fluid">
- <div class="span12">
+ <label class="checkbox span1">
+ <input type="checkbox" data-bind="value: $data, checked: $parent.multiInstallQueue">
+ </label>
+ <div class="span11">
<div class="span3 pull-right">
- <button class="btn btn-primary btn-block" data-bind="enable: $root.enableRepoInstall($data), css: {disabled: !$root.enableRepoInstall($data)}, click: function() { if ($root.enableRepoInstall($data)) { $root.installFromRepository($data); } else { return false; } }, attr: {title: !$root.enableRepoInstall($data) ? gettext('Plugin install is disabled') : ''}"><span data-bind="text: $root.installButtonText($data)"></span></button>
+ <button class="btn btn-primary btn-block" data-bind="enable: $root.enableRepoInstall($data), css: {disabled: !$root.enableRepoInstall($data)}, click: $root.installButtonAction"><span data-bind="text: $root.installButtonText($data)"></span></button>
<div data-bind="visible: $data.disabled !== undefined" style="text-align: center"><small><a data-bind="attr: {href: page}" target="_blank">{{ _('"Why?"') }}</a></small></div>
</div>
<div>
diff --git a/src/octoprint/static/js/app/client/files.js b/src/octoprint/static/js/app/client/files.js
index 7d5458f5c..9bd523e80 100644
--- a/src/octoprint/static/js/app/client/files.js
+++ b/src/octoprint/static/js/app/client/files.js
@@ -156,6 +156,14 @@
return this.base.download(downloadForEntry(location, path), opts);
};
+ OctoPrintFilesClient.prototype.downloadPath = function (location, path) {
+ var url = downloadForEntry(location, path);
+ if (!_.startsWith(url, "http://") && !_.startsWith(url, "https://")) {
+ url = this.base.getBaseUrl() + url;
+ }
+ return url;
+ };
+
OctoPrintFilesClient.prototype.pathForEntry = function (entry) {
if (!entry || !entry.hasOwnProperty("parent") || entry.parent == undefined) {
return "";