Merge branch 'maintenance' into fa-update

This commit is contained in:
Davide Depau 2017-05-17 17:46:01 +02:00 committed by GitHub
commit d8d9c7fe64
20 changed files with 131 additions and 54 deletions

View file

@ -10,13 +10,14 @@ This is a bug and feature tracker, please only use it to report bugs
or request features within OctoPrint (not OctoPi, not any OctoPrint
plugins and not unofficial OctoPrint versions).
Do not seek support here ("I need help with ..."), that belongs on
the mailing list or the G+ community (both linked in the "guidelines
for contributing" linked above, read it!), NOT here.
Do not seek support here ("I need help with ...", "I have a
question ..."), that belongs on the mailing list or the G+ community
(both linked in the "guidelines for contributing" linked above, read
them!), NOT here.
Mark requests with a "[Request]" prefix in the title please. Fully fill
out the bug reporting template for bug reports (if you don't know where
to find some information - it's all described in the Contribution
Mark requests with a "[Request]" prefix in the title please. For bug
reports fully fill out the bug reporting template (if you don't know
where to find some information - it's all described in the Contribution
Guidelines linked up there in the big yellow box).
When reporting a bug do NOT delete ANY lines from the template but
@ -91,7 +92,7 @@ reporting communication issues. Never truncate.
serial.log is usually not written due to performance reasons and must be
enabled explicitly. Provide at the very least the FULL contents of your
terminal tab at the time of the bug occurrence, even if you do not have
a serial.log.]
a serial.log (which the Contribution Guidelines tell you where to find).]
#### Link to contents of Javascript console in the browser

View file

@ -16,11 +16,14 @@ prerelease
HEAD
\(detached.*
# maintenance is currently the branch for preparation of maintenance release 1.3.3
# maintenance is currently the branch for preparation of maintenance release 1.3.4
# so are any fix/... and improve/... branches
maintenance 1.3.3 0a69dbeddb301d5a32827a3f0d561f875df24234 pep440-dev
fix/.* 1.3.3 0a69dbeddb301d5a32827a3f0d561f875df24234 pep440-dev
improve/.* 1.3.3 0a69dbeddb301d5a32827a3f0d561f875df24234 pep440-dev
maintenance 1.3.4 3fbd477d15b5776ca929ea578c5437720aaf7f31 pep440-dev
fix/.* 1.3.4 3fbd477d15b5776ca929ea578c5437720aaf7f31 pep440-dev
improve/.* 1.3.4 3fbd477d15b5776ca929ea578c5437720aaf7f31 pep440-dev
# staging/maintenance is currently the branch for preparation of 1.3.3rc3 (if we'll need that)
staging/maintenance 1.3.3rc2 3fbd477d15b5776ca929ea578c5437720aaf7f31 pep440-dev
# every other branch is a development branch and thus gets resolved to 1.4.0-dev for now
.* 1.4.0 7f5d03d0549bcbd26f40e7e4a3297ea5204fb1cc pep440-dev

View file

@ -1,11 +1,21 @@
# OctoPrint Changelog
## 1.3.3rc2 (2017-05-17)
### Bug fixes
* [#1917](https://github.com/foosel/OctoPrint/issues/1917) (regression) - Fix job data resetting on print job completion.
* [#1918](https://github.com/foosel/OctoPrint/issues/1918) (regression) - Fix "save as default" checkbox not being disabled when other controls are disabled.
* [#1919](https://github.com/foosel/OctoPrint/issues/1919) (regression) - Fix call to no longer existing function in Plugin Manager UI.
([Commits](https://github.com/foosel/OctoPrint/compare/1.3.3rc1...1.3.3rc2))
## 1.3.3rc1 (2017-05-11)
### Improvements
* [#478](https://github.com/foosel/OctoPrint/issues/478) - Made webcam stream contained fixed height (with selectable aspect ratio) to prevent jumps of the controls beneath it on load.
* [#748](https://github.com/foosel/OctoPrint/issues/748) - Added delete confirmation and bulk delete for timelapses. See also the discussion in brainstorming ticket [#1807].(https://github.com/foosel/OctoPrint/issues/1807).
* [#478](https://github.com/foosel/OctoPrint/issues/478) - Made webcam stream container fixed height (with selectable aspect ratio) to prevent jumps of the controls beneath it on load.
* [#748](https://github.com/foosel/OctoPrint/issues/748) - Added delete confirmation and bulk delete for timelapses. See also the discussion in brainstorming ticket [#1807](https://github.com/foosel/OctoPrint/issues/1807).
* [#1092](https://github.com/foosel/OctoPrint/issues/1092) - Added new event to the file manager: `FileAdded`, `FileRemoved`, `FolderAdded`, `FolderRemoved`. Contrary to the `Upload` event, `FileAdded` will always fire when a file was added to storage through the file manager, not only when added through the web interface. Extended documentation accordingly.
* [#1521](https://github.com/foosel/OctoPrint/issues/1521) - Software update plugin: Display timestamp of last version cache refresh in "Advanced options" area.
* [#1734](https://github.com/foosel/OctoPrint/issues/1734) - Treat default/initial printer profile like all other printer profiles, persisting it to disk instead of `config.yaml` and allowing deletion. OctoPrint will migrate the existing default profile to the new location on first start.

View file

@ -365,10 +365,16 @@ There are three main branches in OctoPrint:
the `maintenance` branch and are now being pushed on the "Maintenance"
pre release channel for further testing. Version number follows the scheme
`<x>.<y>.<z>rc<n>` (e.g. `1.2.9rc1`).
* `staging/maintenance`: Any preparation for potential follow-up RCs takes place here.
Version number follows the scheme `<x>.<y>.<z>rc<n+1>.dev<commits since increase of n>` (e.g.
`1.2.9rc1.dev3`) for a current Maintenance RC of `<x>.<y>.<z>rc<n>`.
* `rc/devel`: This branch is reserved for future releases that have graduated from
the `devel` branch and are now being pushed on the "Devel" pre release channel
for further testing. Version number follows the scheme `<x>.<y+1>.0rc<n>` (e.g. `1.3.0rc1`)
for a current stable OctoPrint version of `<x>.<y>.<z>`.
* `staging/devel`: Any preparation for potential follow-up Devel RCs takes place
here. Version number follows the scheme `<x>.<y>.0rc<n+1>.dev<commits since increase of n>` (e.g.
`1.3.0rc1.dev12`) for a current Devel RC of `<x>.<y>.0rc<n>`.
Additionally, from time to time you might see other branches pop up in the repository.
Those usually have one of the following prefixes:

View file

@ -64,4 +64,4 @@ thanks to everyone who contributed!
* Timeshell.ca
* Trent Shumay
and 1076 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)!
and 1084 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)!

View file

@ -258,22 +258,22 @@ $(function() {
buttons: [{
text: gettext("Later"),
click: function(notice) {
self.hiddenChannels.push(key);
notice.remove();
self.hiddenChannels.push(key);
}
}, {
text: gettext("Mark read"),
click: function(notice) {
self.markRead(key, value.last);
notice.remove();
self.markRead(key, value.last);
}
}, {
text: gettext("Read..."),
addClass: "btn-primary",
click: function(notice) {
notice.remove();
self.showAnnouncementDialog(key);
self.markRead(key, value.last);
notice.remove();
}
}]
},

View file

@ -526,13 +526,11 @@ $(function() {
if (reinstall) {
OctoPrint.plugins.pluginmanager.reinstall(reinstall, url, followDependencyLinks)
.done(onSuccess)
.fail(onError)
.always(onAlways);
.fail(onError);
} else {
OctoPrint.plugins.pluginmanager.install(url, followDependencyLinks)
.done(onSuccess)
.fail(onError)
.always(onAlways);
.fail(onError);
}
};
@ -687,17 +685,21 @@ $(function() {
hide: false
};
var restartClicked = false;
if (self.restartCommandSpec) {
options.confirm = {
confirm: true,
buttons: [{
text: gettext("Restart now"),
click: function () {
click: function (notice) {
if (restartClicked) return;
restartClicked = true;
showConfirmationDialog({
message: gettext("This will restart your OctoPrint server."),
onproceed: function() {
OctoPrint.system.executeCommand("core", "restart")
.done(function() {
notice.remove();
new PNotify({
title: gettext("Restart in progress"),
text: gettext("The server is now being restarted in the background")
@ -709,6 +711,9 @@ $(function() {
text: gettext("Trying to restart the server produced an error, please check octoprint.log for details. You'll have to restart manually.")
})
});
},
onclose: function() {
restartClicked = false;
}
});
}
@ -718,6 +723,7 @@ $(function() {
notification = PNotify.singleButtonNotify(options);
} else if (response.needs_refresh) {
var refreshClicked = false;
notification = PNotify.singleButtonNotify({
title: titleSuccess,
text: textReload,
@ -726,6 +732,8 @@ $(function() {
buttons: [{
text: gettext("Reload now"),
click: function () {
if (refreshClicked) return;
refreshClicked = true;
location.reload(true);
}
}]

View file

@ -868,7 +868,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
self._logger.info("Restarting...")
try:
util.execute(restart_command)
util.execute(restart_command, evaluate_returncode=False, async=True)
except exceptions.ScriptError as e:
self._logger.exception("Error while restarting via command {}".format(restart_command))
self._logger.warn("Restart stdout:\n{}".format(e.stdout))

View file

@ -123,8 +123,9 @@ $(function() {
self.config_updateMethod = ko.observable();
self.config_releaseChannel = ko.observable();
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
self.confirmationDialog = $("#softwareupdate_confirmation_dialog");
self.configurationDialog = undefined;
self.confirmationDialog = undefined;
self._updateClicked = false;
self.config_availableCheckTypes = ko.observableArray([]);
self.config_availableReleaseChannels = ko.observableArray([]);
@ -357,7 +358,11 @@ $(function() {
}, {
text: gettext("Update now"),
addClass: "btn-primary",
click: self.update
click: function() {
if (self._updateClicked) return;
self._updateClicked = true;
self.update();
}
}]
};
options["buttons"] = {
@ -501,8 +506,14 @@ $(function() {
};
self.update = function(force) {
if (self.updateInProgress) return;
if (!self.loginState.isAdmin()) return;
if (self.updateInProgress) {
self._updateClicked = false;
return;
}
if (!self.loginState.isAdmin()) {
self._updateClicked = false;
return;
}
if (self.printerState.isPrinting()) {
self._showPopup({
@ -510,6 +521,7 @@ $(function() {
text: gettext("A print job is currently in progress. Updating will be prevented until it is done."),
type: "error"
});
self._updateClicked = false;
} else {
self.forceUpdate = (force == true);
self.confirmationDialog.modal("show");
@ -518,9 +530,13 @@ $(function() {
};
self.confirmUpdate = function() {
self.confirmationDialog.modal("hide");
self.performUpdate(self.forceUpdate,
_.map(self.availableAndPossible(), function(info) { return info.key }));
self.confirmationDialog.modal("hide");
};
self.confirmationHidden = function() {
self._updateClicked = false;
};
self._showWorkingDialog = function(title) {
@ -578,6 +594,10 @@ $(function() {
self.onStartup = function() {
self.workingDialog = $("#settings_plugin_softwareupdate_workingdialog");
self.workingOutput = $("#settings_plugin_softwareupdate_workingdialog_output");
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
self.confirmationDialog = $("#softwareupdate_confirmation_dialog");
self.confirmationDialog.on("hidden", self.confirmationHidden);
};
self.onServerDisconnect = function() {

View file

@ -9,12 +9,12 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
from .exceptions import ScriptError
def execute(command, cwd=None, evaluate_returncode=True):
def execute(command, cwd=None, evaluate_returncode=True, async=False):
import sarge
p = None
try:
p = sarge.run(command, cwd=cwd, stdout=sarge.Capture(), stderr=sarge.Capture())
p = sarge.run(command, cwd=cwd, stdout=sarge.Capture(), stderr=sarge.Capture(), async=async)
except:
returncode = p.returncode if p is not None else None
stdout = p.stdout.text if p is not None and p.stdout is not None else ""

View file

@ -299,6 +299,7 @@ class PrinterInterface(object):
after the file is selected.
Raises:
InvalidFileType: if the file is not a machinecode file and hence cannot be printed
InvalidFileLocation: if an absolute path was provided and not contained within local storage or
doesn't exist
"""
@ -593,3 +594,6 @@ class UnknownScript(Exception):
class InvalidFileLocation(Exception):
pass
class InvalidFileType(Exception):
pass

View file

@ -17,9 +17,9 @@ import time
from octoprint import util as util
from octoprint.events import eventManager, Events
from octoprint.filemanager import FileDestinations, NoSuchStorage
from octoprint.filemanager import FileDestinations, NoSuchStorage, valid_file_type
from octoprint.plugin import plugin_manager, ProgressPlugin
from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript, InvalidFileLocation
from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript, InvalidFileLocation, InvalidFileType
from octoprint.printer.estimation import TimeEstimationHelper
from octoprint.settings import settings
from octoprint.util import comm as comm
@ -846,6 +846,9 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._stateMonitor.add_temperature(data)
def _validateJob(self, filename, sd):
if not valid_file_type(filename, type="machinecode"):
raise InvalidFileType("{} is not a machinecode file, cannot print".format(filename))
if sd:
return
@ -1070,13 +1073,17 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
must_be_set=False)
def on_comm_print_job_done(self):
self._updateProgressData()
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
self._fileManager.delete_recovery_data()
payload = self._payload_for_print_job_event()
if payload:
payload["time"] = self._comm.getPrintTime()
self._updateProgressData(completion=1.0,
filepos=payload["size"],
printTime=payload["time"],
printTimeLeft=0)
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
eventManager().fire(Events.PRINT_DONE, payload)
self.script("afterPrintDone",
context=dict(event=payload),
@ -1088,6 +1095,10 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
payload["time"],
True,
self._printerProfileManager.get_current_or_default()["id"])
else:
self._updateProgressData()
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
def on_comm_print_job_failed(self):
payload = self._payload_for_print_job_event()
@ -1134,7 +1145,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._sdStreaming = True
self._setJobData(filename, filesize, True)
self._updateProgressData()
self._updateProgressData(completion=0.0, filepos=0, printTime=0)
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
def on_comm_file_transfer_done(self, filename):
@ -1162,6 +1173,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._logger.exception("Error while trying to persist print recovery data")
def _payload_for_print_job_event(self, location=None, print_job_file=None, position=None):
print_job_size = None
if print_job_file is None:
with self._selectedFileMutex:
selected_file = self._selectedFile
@ -1169,9 +1181,10 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
return dict()
print_job_file = selected_file.get("filename", None)
print_job_size = selected_file.get("filesize", None)
location = FileDestinations.SDCARD if selected_file.get("sd", False) else FileDestinations.LOCAL
if not print_job_file or not location:
if not print_job_file or not print_job_size or not location:
return dict()
if location == FileDestinations.SDCARD:
@ -1190,6 +1203,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
result= dict(name=name,
path=path,
origin=origin,
size=print_job_size,
# TODO deprecated, remove in 1.4.0
file=full_path,

View file

@ -550,7 +550,7 @@ class Server(object):
"on_shutdown",
sorting_context="ShutdownPlugin.on_shutdown")
# wait for shutdown even to be processed, but maximally for 15s
# wait for shutdown event to be processed, but maximally for 15s
event_timeout = 15.0
if eventManager.join(timeout=event_timeout):
self._logger.warn("Event loop was still busy processing after {}s, shutting down anyhow".format(event_timeout))
@ -565,13 +565,16 @@ class Server(object):
def sigterm_handler(*args, **kwargs):
# will stop tornado on SIGTERM, making the program exit cleanly
def shutdown_tornado():
self._logger.debug("Shutting down tornado's IOLoop...")
ioloop.stop()
self._logger.debug("SIGTERM received...")
ioloop.add_callback_from_signal(shutdown_tornado)
signal.signal(signal.SIGTERM, sigterm_handler)
try:
# this is the main loop - as long as tornado is running, OctoPrint is running
ioloop.start()
self._logger.debug("Tornado's IOLoop stopped")
except (KeyboardInterrupt, SystemExit):
pass
except:

View file

@ -646,6 +646,7 @@ function showConfirmationDialog(msg, onacknowledge, options) {
var proceed = options.proceed || gettext("Proceed");
var proceedClass = options.proceedClass || "danger";
var onproceed = options.onproceed || undefined;
var onclose = options.onclose || undefined;
var dialogClass = options.dialogClass || "";
var modalHeader = $('<a href="javascript:void(0)" class="close" data-dismiss="modal" aria-hidden="true">&times;</a><h3>' + title + '</h3>');
@ -663,14 +664,19 @@ function showConfirmationDialog(msg, onacknowledge, options) {
.append($('<div></div>').addClass('modal-header').append(modalHeader))
.append($('<div></div>').addClass('modal-body').append(modalBody))
.append($('<div></div>').addClass('modal-footer').append(cancelButton).append(proceedButton));
modal.on('hidden', function(event) {
if (onclose && _.isFunction(onclose)) {
onclose(event);
}
});
modal.modal("show");
proceedButton.click(function(e) {
e.preventDefault();
modal.modal("hide");
if (onproceed && _.isFunction(onproceed)) {
onproceed(e);
}
modal.modal("hide");
});
return modal;

View file

@ -442,6 +442,7 @@ $(function() {
self.additionalControls = additionalControls;
self.rerenderControls();
}
self._enableWebcam();
};
self.onFocus = function (data, event) {

View file

@ -841,13 +841,14 @@ $(function() {
function evaluateDropzones() {
var enableLocal = self.loginState.isUser();
var enableSd = enableLocal && CONFIG_SD_SUPPORT && self.printerState.isSdReady();
var enableSd = enableLocal && CONFIG_SD_SUPPORT && self.printerState.isSdReady() && !self.isPrinting();
self._setDropzone("local", enableLocal);
self._setDropzone("sdcard", enableSd);
}
self.loginState.isUser.subscribe(evaluateDropzones);
self.printerState.isSdReady.subscribe(evaluateDropzones);
self.isPrinting.subscribe(evaluateDropzones);
evaluateDropzones();
self.requestData();
@ -1106,7 +1107,7 @@ $(function() {
if (foundLocal) {
self.dropZoneLocalBackground.addClass("hover");
self.dropZoneSdBackground.removeClass("hover");
} else if (foundSd && self.printerState.isSdReady()) {
} else if (foundSd && self.printerState.isSdReady() && !self.isPrinting()) {
self.dropZoneSdBackground.addClass("hover");
self.dropZoneLocalBackground.removeClass("hover");
} else if (found) {

View file

@ -48,7 +48,7 @@ $(function() {
return ("timed" == self.timelapseType());
});
self.saveButtonEnabled = ko.pureComputed(function() {
return self.isDirty() && self.isOperational() && !self.isPrinting() && self.loginState.isUser();
return self.isDirty() && !self.isPrinting() && self.loginState.isUser();
});
self.isOperational.subscribe(function() {

View file

@ -4,7 +4,7 @@
{% if enableSdSupport %}
<div class="dropzone" id="drop_locally"><span class="text"><i class="fa fa-upload"></i><br>{{ _('Upload locally') }}</span></div>
<div class="dropzone_background" id="drop_locally_background"></div>
<div class="dropzone" id="drop_sd"><span class="text"><i class="fa fa-upload"></i><br>{{ _('Upload to SD') }}<br><small data-bind="visible: !isSdReady()">({{ _('SD not initialized') }})</small></span></div>
<div class="dropzone" id="drop_sd"><span class="text"><i class="fa fa-upload"></i><br>{{ _('Upload to SD') }}<br><small data-bind="visible: isSdReady() && isPrinting()">({{ _('Cannot upload to printer\'s SD while printing') }})</small><small data-bind="visible: !isSdReady()">({{ _('SD not initialized') }})</small></span></div>
<div class="dropzone_background" id="drop_sd_background"></div>
{% else %}
<div class="dropzone" id="drop"><span class="text"><i class="fa fa-upload"></i><br>{{ _('Upload') }}</span></div>

View file

@ -55,18 +55,18 @@
</div>
<div class="row-fluid upload-buttons">
{% if enableSdSupport %}
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<span class="btn btn-primary fileinput-button span6" data-bind="enable: $root.loginState.isUser(), css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="fa fa-upload"></i>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" accept="{{ ",".join(supportedExtensions) }}" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
<span class="btn btn-primary fileinput-button span6" data-bind="enable: $root.loginState.isUser() && $root.isSdReady() && !$root.isPrinting(), css: {disabled: !$root.loginState.isUser() || !$root.isSdReady() || $root.isPrinting()}" style="margin-bottom: 10px">
<i class="fa fa-upload"></i>
<span>{{ _('Upload to SD') }}</span>
<input id="gcode_upload_sd" accept="{{ ",".join(supportedExtensions) }}" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser() && isSdReady()">
<input id="gcode_upload_sd" accept="{{ ",".join(supportedExtensions) }}" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser() && isSdReady() && !isPrinting()">
</span>
{% else %}
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<span class="btn btn-primary fileinput-button span12" data-bind="enable: $root.loginState.isUser(), css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="fa fa-upload"></i>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" accept="{{ ",".join(supportedExtensions) }}" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">

View file

@ -5,7 +5,7 @@
<h1>{{ _('Timelapse Configuration') }}</h1>
<label for="webcam_timelapse_mode">{{ _('Timelapse Mode') }}</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: !isPrinting() && loginState.isUser()">
<option value="off">{{ _('Off') }}</option>
<option value="timed">{{ _('Timed') }}</option>
<option value="zchange">{{ _('On Z Change') }}</option>
@ -16,25 +16,25 @@
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
<label for="webcam_timelapse_interval">{{ _('Interval between snapshots (in seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('sec') }}</span>
</div>
</div>
<label for="webcam_timelapse_fps">{{ _('Timelapse frame rate (in frames per second)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_fps" data-bind="value: timelapseFps, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<input type="text" class="input-mini" id="webcam_timelapse_fps" data-bind="value: timelapseFps, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">{{ _('fps') }}</span>
</div>
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">{{ _('sec') }}</span>
</div>
<div id="webcam_timelapse_capturePostRoll" data-bind="visible: timelapseType() == 'timed'">
<label class="checkbox">
<input type="checkbox" data-bind="checked: timelapseCapturePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()"> {{ _('Capture post roll images') }}
<input type="checkbox" data-bind="checked: timelapseCapturePostRoll, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()"> {{ _('Capture post roll images') }}
<span class="help-block">{{ _('If this is unchecked, OctoPrint will simply repeat the last frame for the post roll instead of continuing to capture new frames.') }}</span>
</label>
</div>
@ -42,7 +42,7 @@
<div id="webcam_timelapse_retractionsettings" data-bind="visible: timelapseType() == 'zchange'">
<label for="webcam_timelapse_retractionZHop">{{ _('Retraction Z-Hop (in mm)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_retractionZHop" data-bind="value: timelapseRetractionZHop, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
<input type="text" class="input-mini" id="webcam_timelapse_retractionZHop" data-bind="value: timelapseRetractionZHop, valueUpdate: 'afterkeydown', enable: !isPrinting() && loginState.isUser()">
<span class="add-on">{{ _('mm') }}</span>
</div>
<span class="help-block">{{ _('Enter the retraction z-hop used in the firmware or the gcode file to trigger snapshots for the timelapse only if a real layer change happens. For this to work properly your retraction z-hop has to be different from your layerheight!') }}</span>
@ -50,7 +50,7 @@
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist"> {{ _('Save as default') }}
<input type="checkbox" data-bind="checked: persist, enable: isOperational() && !isPrinting() && loginState.isUser()"> {{ _('Save as default') }}
<span class="help-block">{{ _('Check this to make your selected timelapse mode and options persist across restarts.') }}</span>
</label>
</div>