├── core
├── favicon.ico
├── js
│ ├── .tern-project
│ ├── 3rd-party
│ │ ├── jquery.flot.resize.js
│ │ ├── piecon.js
│ │ ├── FileSaver.js
│ │ ├── jquery.flot.dashes.js
│ │ ├── bootstrap-notify.js
│ │ └── jquery.flot.navigate.js
│ ├── i18n.js
│ ├── notify.js
│ ├── sliders.js
│ ├── charts.js
│ ├── modals.js
│ ├── utils.js
│ ├── settings.js
│ └── upload.js
├── favicon_red.ico
├── favicon_green.ico
├── fonts
│ ├── Homenaje-Regular.ttf
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── html404.htm
└── css
│ ├── layout-md.css
│ ├── layout-sm.css
│ ├── layout-xs.css
│ ├── layout-lg.css
│ ├── bootstrap-slider.css
│ ├── slate.css
│ └── defaults.css
├── DuetWebControl-1.21.zip
├── geti18n.sh
├── .gitattributes
└── README.md
/core/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/favicon.ico
--------------------------------------------------------------------------------
/core/js/.tern-project:
--------------------------------------------------------------------------------
1 | {
2 | "loadEagerly": [
3 | "./3rd-party/*.js"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/core/favicon_red.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/favicon_red.ico
--------------------------------------------------------------------------------
/DuetWebControl-1.21.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/DuetWebControl-1.21.zip
--------------------------------------------------------------------------------
/core/favicon_green.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/favicon_green.ico
--------------------------------------------------------------------------------
/core/fonts/Homenaje-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/fonts/Homenaje-Regular.ttf
--------------------------------------------------------------------------------
/core/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/core/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/core/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/core/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zen/DuetWebControl/master/core/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/geti18n.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script is supposed to generate a list of new XML translation strings for "language.xml"
4 | git diff HEAD | grep "^\+" | grep -Po "T\(['\"]([^\"]*)['\"]" | cut -d '"' -f 2 | sed -e 's/\</g' | sed -e 's/>/\>/g' | sort | uniq | while read translation
5 | do
6 | if [[ -z $(grep "\"$translation\"" ./core/language.xml) ]]
7 | then
8 | echo ""
9 | fi
10 | done
11 |
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/core/html404.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Duet Web Control - 404
12 |
13 |
14 |
15 | ERROR! ERROR! ERROR!
16 | 404 - The requested file could not be found.
17 |
18 |
--------------------------------------------------------------------------------
/core/css/layout-md.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 992px) and (max-width: 1199px) {
2 | /* Main content */
3 | #div_content row > div[class^='col-'] {
4 | padding-left: 8px !important;
5 | padding-right: 8px !important;
6 | }
7 |
8 | /* Control page */
9 | #page_control .col-right,
10 | #panel_extrude div.panel-body > div:first-child {
11 | padding-left: 0px;
12 | }
13 |
14 | /* Print status page */
15 | #page_print .col-right {
16 | padding-left: 0px;
17 | }
18 |
19 | /* Settings page */
20 | #div_add_tool div.row > div > label {
21 | margin-top: 9px;
22 | }
23 |
24 | #page_settings .col-left {
25 | padding-right: 0px;
26 | }
27 |
28 | #page_settings .col-right {
29 | padding-left: 0px;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/core/css/layout-sm.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 768px) and (max-width: 991px) {
2 | /* Navigation bar */
3 | .navbar-checkbox {
4 | margin-left: 15px;
5 | margin-top: 8px;
6 | margin-bottom: 8px;
7 | }
8 |
9 | /* Main content */
10 | .content-collapsed-padding {
11 | padding-top: 12px;
12 | }
13 |
14 | #div_content row > div[class^='col-'] {
15 | padding-left: 8px !important;
16 | padding-right: 8px !important;
17 | }
18 |
19 | /* Static sidebar */
20 | .nav-sidebar > li > a {
21 | padding-left: 15px;
22 | }
23 |
24 | /* Control page */
25 | #panel_head_movement > div.panel-heading {
26 | text-align: left;
27 | }
28 |
29 | /* Scanner page */
30 | #table_scan_files th:first-child {
31 | width: 100px;
32 | }
33 |
34 | /* Settings page */
35 | #div_add_tool div.row > div > label {
36 | margin-top: 9px;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/css/layout-xs.css:
--------------------------------------------------------------------------------
1 | @media (max-width: 767px) {
2 | /* Navigation bar */
3 | .navbar-brand {
4 | margin-left: 15px !important;
5 | }
6 |
7 | .navbar-checkbox {
8 | float: right;
9 | margin-right: 15px;
10 | margin-top: 8px;
11 | margin-bottom: 8px;
12 | }
13 |
14 | /* Main content */
15 | #div_content {
16 | padding-top: 12px;
17 | }
18 |
19 | .page {
20 | min-height: calc(100vh - 52px);
21 | }
22 |
23 | /* Static sidebar */
24 | .sidebar,
25 | .sidebar-continuaton {
26 | display: none;
27 | }
28 |
29 | /* Control page */
30 | #panel_head_movement > div.panel-heading {
31 | text-align: left;
32 | }
33 |
34 | #page_control {
35 | min-height: auto;
36 | }
37 |
38 | #page_control .col-right:last-child {
39 | padding-left: 15px;
40 | }
41 |
42 | /* Scanner page */
43 | #table_scan_files th:first-child {
44 | width: 100px;
45 | }
46 |
47 | /* G-Code console page */
48 | #page_console div.well {
49 | padding-left: 15px;
50 | padding-right: 15px;
51 | }
52 |
53 | /* Settings page */
54 | #div_add_tool div.row > div > label {
55 | margin-top: 9px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/core/css/layout-lg.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 1200px) {
2 | /* Workarounds (may be dropped in future releases) */
3 | .btn-group-justified .visible-lg {
4 | display: table-cell !important;
5 | }
6 |
7 | /* Navigation bar */
8 | .brand-sm {
9 | display: none !important;
10 | }
11 | .brand-lg {
12 | display: inline-block !important;
13 | }
14 |
15 | /* Sidebar */
16 | .nav-sidebar {
17 | margin-bottom: 20px;
18 | }
19 |
20 | .nav-sidebar > li > a {
21 | font-size: 16px;
22 | }
23 |
24 | /* Main content */
25 | #div_content row > div[class^='col-'] {
26 | padding-left: 8px !important;
27 | padding-right: 8px !important;
28 | }
29 |
30 | /* Control page */
31 | #page_control .col-right,
32 | #panel_extrude div.panel-body > div:first-child {
33 | padding-left: 0px;
34 | }
35 |
36 | #panel_head_movement .panel-heading,
37 | #panel_macro_buttons .panel-heading {
38 | padding-top: 6px;
39 | padding-bottom: 6px;
40 | }
41 |
42 | /* Print status page */
43 | #page_print .col-right {
44 | padding-left: 0px;
45 | }
46 |
47 | /* Settings page */
48 | #btn_language,
49 | #btn_theme {
50 | margin-left: 9px;
51 | }
52 |
53 | #dropdown_language,
54 | #dropdown_theme {
55 | display: inline-block;
56 | }
57 |
58 | #page_settings .col-left {
59 | padding-right: 0px;
60 | }
61 |
62 | #page_settings .col-right {
63 | padding-left: 0px;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/core/js/3rd-party/jquery.flot.resize.js:
--------------------------------------------------------------------------------
1 | /* Flot plugin for automatically redrawing plots as the placeholder resizes.
2 |
3 | Copyright (c) 2007-2014 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | It works by listening for changes on the placeholder div (through the jQuery
7 | resize event plugin) - if the size changes, it will redraw the plot.
8 |
9 | There are no options. If you need to disable the plugin for some plots, you
10 | can just fix the size of their placeholders.
11 |
12 | */
13 |
14 | /* Inline dependency:
15 | * jQuery resize event - v1.1 - 3/14/2010
16 | * http://benalman.com/projects/jquery-resize-plugin/
17 | *
18 | * Copyright (c) 2010 "Cowboy" Ben Alman
19 | * Dual licensed under the MIT and GPL licenses.
20 | * http://benalman.com/about/license/
21 | */
22 | (function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);
23 |
24 | (function ($) {
25 | var options = { }; // no options
26 |
27 | function init(plot) {
28 | function onResize() {
29 | var placeholder = plot.getPlaceholder();
30 |
31 | // somebody might have hidden us and we can't plot
32 | // when we don't have the dimensions
33 | if (placeholder.width() == 0 || placeholder.height() == 0)
34 | return;
35 |
36 | plot.resize();
37 | plot.setupGrid();
38 | plot.draw();
39 | }
40 |
41 | function bindEvents(plot, eventHolder) {
42 | plot.getPlaceholder().resize(onResize);
43 | }
44 |
45 | function shutdown(plot, eventHolder) {
46 | plot.getPlaceholder().unbind("resize", onResize);
47 | }
48 |
49 | plot.hooks.bindEvents.push(bindEvents);
50 | plot.hooks.shutdown.push(shutdown);
51 | }
52 |
53 | $.plot.plugins.push({
54 | init: init,
55 | options: options,
56 | name: 'resize',
57 | version: '1.0'
58 | });
59 | })(jQuery);
60 |
--------------------------------------------------------------------------------
/core/js/i18n.js:
--------------------------------------------------------------------------------
1 | /* Internationalization routines for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | var showTranslationWarning = false; // Set this to "true" if you want to look for missing translation entries
11 |
12 | var translationData;
13 |
14 |
15 | // Called to look up English strings and to translate them into the configured language.
16 | // Variable arguments may be passed like T("Hello {0}, are you doing {1}?", "user", "well")
17 | function T(text) {
18 | var entry = text;
19 | if (translationData != undefined) {
20 | // Generate a regex to check with
21 | text = text.replace(/{(\d+)}/g, "{\\d+}").replace("(", "\\(").replace(")", "\\)");
22 | text = text.replace("?", "[?]").replace(".", "[.]");
23 | var regex = new RegExp("^" + text + "$");
24 |
25 | // Get the translation node and see if we can find an entry
26 | var root = translationData.getElementsByTagName(settings.language).item(settings.language);
27 | if (root != null) {
28 | for(var i = 0; i < root.children.length; i++) {
29 | if (regex.test(root.children[i].attributes["t"].value)) {
30 | entry = root.children[i].textContent;
31 | break;
32 | }
33 | }
34 |
35 | // Log translation text if we couldn't find a suitable text
36 | if (showTranslationWarning && entry == text) {
37 | console.log("WARNING: Could not translate '" + entry + "'");
38 | }
39 | }
40 | }
41 |
42 | // Format it with the given arguments
43 | var args = arguments;
44 | return entry.replace(/{(\d+)}/g, function(match, number) {
45 | number = parseInt(number) + 1;
46 | return typeof args[number] != 'undefined' ? args[number] : match;
47 | });
48 | }
49 |
50 | // May be called only once on page load to translate the page
51 | function translatePage() {
52 | if (translationData != undefined) {
53 | var root = translationData.getElementsByTagName(settings.language).item(settings.language);
54 | if (root != null) {
55 | // Translate HTML attributes
56 | translateEntries(root, $("p, span, strong, button, li.dropdown-header"), "textContent");
57 | translateEntries(root, $("th, td, dt"), "textContent");
58 | translateEntries(root, $("h1, h4, label, a, #div_content ol > li:first-child, ol.breadcrumb-directory > li:last-child"), "textContent");
59 | translateEntries(root, $("input[type='text']"), "placeholder");
60 | translateEntries(root, $("a, abbr, button, label, li, #chart_temp, input, td"), "title");
61 | translateEntries(root, $("img"), "alt");
62 |
63 | // This doesn't work with data attributes though
64 | $("button[data-content]").each(function() {
65 | $(this).attr("data-content", T($(this).attr("data-content")));
66 | });
67 |
68 | // Update SD Card button caption
69 | $("#btn_volume > span.content").text(T("SD Card {0}", currentGCodeVolume));
70 |
71 | // Set new language on Settings page
72 | $("#btn_language").data("language", settings.language).children("span:first-child").text(root.attributes["name"].value);
73 | $("html").attr("lang", settings.language);
74 | }
75 | }
76 | }
77 |
78 | function translateEntries(root, entries, key) {
79 | var doNodeCheck = (key == "textContent");
80 | $.each(entries, function() {
81 | // If this node has no children, we can safely use it
82 | if (!doNodeCheck || this.childNodes.length < 2) {
83 | translateEntry(root, this, key);
84 | // Otherwise we need to check for non-empty text nodes
85 | } else {
86 | for(var i = 0; i < this.childNodes.length; i++) {
87 | var val = this.childNodes[i][key];
88 | if (this.childNodes[i].nodeType == 3 && val != undefined && this.childNodes[i].childNodes.length == 0 && val.trim().length > 0) {
89 | translateEntry(root, this.childNodes[i], key);
90 | }
91 | }
92 | }
93 | });
94 | }
95 |
96 | function translateEntry(root, item, key) {
97 | if (item != undefined) {
98 | var originalText = item[key];
99 | if (originalText != undefined && originalText.trim() != "") {
100 | var text = originalText.trim();
101 | for(var i=0; i. All rights reserved.
7 | //
8 |
9 | (function(){
10 | var Piecon = {};
11 |
12 | var currentFavicon = null;
13 | var originalFavicon = null;
14 | var originalTitle = null;
15 | var canvas = null;
16 | var options = {};
17 | var defaults = {
18 | color: '#ff0084',
19 | background: '#bbb',
20 | shadow: '#fff',
21 | fallback: false
22 | };
23 |
24 | var isRetina = window.devicePixelRatio > 1;
25 |
26 | var ua = (function () {
27 | var agent = navigator.userAgent.toLowerCase();
28 | return function (browser) {
29 | return agent.indexOf(browser) !== -1;
30 | };
31 | }());
32 |
33 | var browser = {
34 | ie: ua('msie'),
35 | chrome: ua('chrome'),
36 | webkit: ua('chrome') || ua('safari'),
37 | safari: ua('safari') && !ua('chrome'),
38 | mozilla: ua('mozilla') && !ua('chrome') && !ua('safari')
39 | };
40 |
41 | var getFaviconTag = function() {
42 | var links = document.getElementsByTagName('link');
43 |
44 | for (var i = 0, l = links.length; i < l; i++) {
45 | if (links[i].getAttribute('rel') === 'icon' || links[i].getAttribute('rel') === 'shortcut icon') {
46 | return links[i];
47 | }
48 | }
49 |
50 | return false;
51 | };
52 |
53 | var removeFaviconTag = function() {
54 | var links = Array.prototype.slice.call(document.getElementsByTagName('link'), 0);
55 | var head = document.getElementsByTagName('head')[0];
56 |
57 | for (var i = 0, l = links.length; i < l; i++) {
58 | if (links[i].getAttribute('rel') === 'icon' || links[i].getAttribute('rel') === 'shortcut icon') {
59 | head.removeChild(links[i]);
60 | }
61 | }
62 | };
63 |
64 | var setFaviconTag = function(url) {
65 | removeFaviconTag();
66 |
67 | var link = document.createElement('link');
68 | link.type = 'image/x-icon';
69 | link.rel = 'icon';
70 | link.href = url;
71 |
72 | document.getElementsByTagName('head')[0].appendChild(link);
73 | };
74 |
75 | var getCanvas = function () {
76 | if (!canvas) {
77 | canvas = document.createElement("canvas");
78 | if (isRetina) {
79 | canvas.width = 32;
80 | canvas.height = 32;
81 | } else {
82 | canvas.width = 16;
83 | canvas.height = 16;
84 | }
85 | }
86 |
87 | return canvas;
88 | };
89 |
90 | var drawFavicon = function(percentage) {
91 | var canvas = getCanvas();
92 | var context = canvas.getContext("2d");
93 |
94 | percentage = percentage || 0;
95 |
96 | if (context) {
97 | context.clearRect(0, 0, canvas.width, canvas.height);
98 |
99 | // Draw shadow
100 | context.beginPath();
101 | context.moveTo(canvas.width / 2, canvas.height / 2);
102 | context.arc(canvas.width / 2, canvas.height / 2, Math.min(canvas.width / 2, canvas.height / 2), 0, Math.PI * 2, false);
103 | context.fillStyle = options.shadow;
104 | context.fill();
105 |
106 | // Draw background
107 | context.beginPath();
108 | context.moveTo(canvas.width / 2, canvas.height / 2);
109 | context.arc(canvas.width / 2, canvas.height / 2, Math.min(canvas.width / 2, canvas.height / 2) - 2, 0, Math.PI * 2, false);
110 | context.fillStyle = options.background;
111 | context.fill();
112 |
113 | // Draw pie
114 | if (percentage > 0) {
115 | context.beginPath();
116 | context.moveTo(canvas.width / 2, canvas.height / 2);
117 | context.arc(canvas.width / 2, canvas.height / 2, Math.min(canvas.width / 2, canvas.height / 2) - 2, (-0.5) * Math.PI, (-0.5 + 2 * percentage / 100) * Math.PI, false);
118 | context.lineTo(canvas.width / 2, canvas.height / 2);
119 | context.fillStyle = options.color;
120 | context.fill();
121 | }
122 |
123 | setFaviconTag(canvas.toDataURL());
124 | }
125 | };
126 |
127 | var updateTitle = function(percentage) {
128 | if (percentage > 0) {
129 | document.title = '(' + percentage + '%) ' + originalTitle;
130 | } else {
131 | document.title = originalTitle;
132 | }
133 | };
134 |
135 | Piecon.setOptions = function(custom) {
136 | options = {};
137 |
138 | for (var key in defaults){
139 | options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key];
140 | }
141 |
142 | return this;
143 | };
144 |
145 | Piecon.setProgress = function(percentage) {
146 | if (!originalTitle) {
147 | originalTitle = document.title;
148 | }
149 |
150 | if (!originalFavicon || !currentFavicon) {
151 | var tag = getFaviconTag();
152 | originalFavicon = currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico';
153 | }
154 |
155 | if (!isNaN(parseFloat(percentage)) && isFinite(percentage)) {
156 | if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === true) {
157 | // Fallback to updating the browser title if unsupported
158 | return updateTitle(percentage);
159 | } else if (options.fallback === 'force') {
160 | updateTitle(percentage);
161 | }
162 |
163 | return drawFavicon(percentage);
164 | }
165 |
166 | return false;
167 | };
168 |
169 | Piecon.reset = function() {
170 | if (originalTitle) {
171 | document.title = originalTitle;
172 | }
173 |
174 | if (originalFavicon) {
175 | currentFavicon = originalFavicon;
176 | setFaviconTag(currentFavicon);
177 | }
178 | };
179 |
180 | Piecon.setOptions(defaults);
181 |
182 | if(typeof define === 'function' && define.amd) {
183 | define(Piecon);
184 | } else if (typeof module !== 'undefined') {
185 | module.exports = Piecon;
186 | } else {
187 | window.Piecon = Piecon;
188 | }
189 | })();
190 |
--------------------------------------------------------------------------------
/core/js/notify.js:
--------------------------------------------------------------------------------
1 | /* Notification subsystem for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | var jsNotificationOptions = {
11 | animate: {
12 | enter: "animated fadeInDown",
13 | exit: "animated fadeOutDown"
14 | },
15 | placement: {
16 | from: "bottom",
17 | align: "center"
18 | },
19 | template: '' +
20 | '
' +
21 | '
' +
22 | '
{1} ' +
23 | '
{2}' +
24 | '
' +
27 | '
' +
28 | '
'
29 | };
30 |
31 | // Apply custom options for JS plugin
32 | $.notifyDefaults(jsNotificationOptions);
33 |
34 | function showDownloadMessage() {
35 | var notifySettings = { icon: "glyphicon glyphicon-compressed",
36 | title: "" + T("Downloading Files") + "
",
37 | message: T("Please wait while the selected files are being downloaded...")
38 | };
39 | var options = { type: "info",
40 | allow_dismiss: false,
41 | delay: -1,
42 | showProgressbar: true
43 | };
44 | return $.notify(notifySettings, options);
45 | }
46 |
47 | function showHaltMessage() {
48 | // Display backdrop and hide it when ready
49 | $("#modal_backdrop").modal("show");
50 | setTimeout(function() {
51 | $("#modal_backdrop").modal("hide");
52 | }, settings.haltedReconnectDelay);
53 |
54 | // Display notification
55 | var notifySettings = { icon: "glyphicon glyphicon-flash",
56 | title: "" + T("Emergency STOP") + "
",
57 | message: T("Please wait while the firmware is restarting..."),
58 | progress: settings.haltedReconnectDelay / 100,
59 | };
60 | var options = { type: "warning",
61 | allow_dismiss: false,
62 | delay: settings.haltedReconnectDelay,
63 | timeout: 1000,
64 | showProgressbar: true
65 | };
66 | var notification = $.notify(notifySettings, options);
67 | $(notification.$ele).css("z-index", 9999);
68 | }
69 |
70 | function showMessage(type, title, message, timeout, allowDismiss) {
71 | // Set default arguments
72 | if (timeout == undefined) { timeout = settings.notificationTimeout; }
73 | if (allowDismiss == undefined) { allowDismiss = true; }
74 |
75 | // Check if an HTML5 notification shall be displayed
76 | if (document.hidden && settings.useHtmlNotifications) {
77 | // HTML5 notifications expect plain strings. Remove potential styles first
78 | if (title.indexOf("<") != -1) { title = $("" + title + "").text(); }
79 | if (message.indexOf("<") != -1) { message = $("" + message + "").text(); }
80 |
81 | // Create new notification and display it
82 | var options = {
83 | body: message,
84 | icon: "favicon.ico",
85 | dir : "ltr",
86 | requireInteraction: allowDismiss
87 | };
88 | var notification = new Notification(title, options);
89 | if (allowDismiss) { notification.onclick = function() { this.close(); }; }
90 | if (timeout > 0) { setTimeout(function() { notification.close(); }, timeout); }
91 | return notification;
92 | }
93 |
94 | // Otherwise display a JQuery notification. Find a suitable icon first
95 | var icon = "glyphicon glyphicon-info-sign";
96 | if (type == "warning") {
97 | icon = "glyphicon glyphicon-warning-sign";
98 | } else if (type == "danger") {
99 | icon = "glyphicon glyphicon-exclamation-sign";
100 | }
101 |
102 | // Check if the title can be displayed as bold text
103 | if (title != "")
104 | {
105 | if (title.indexOf("") == -1)
106 | {
107 | title = "" + title + "";
108 | }
109 | title += "
";
110 | }
111 |
112 | // Show the notification
113 | var notifySettings = { icon: icon,
114 | title: title,
115 | message: message};
116 | var options = { type: type,
117 | allow_dismiss: allowDismiss,
118 | delay: (timeout == undefined) ? settings.notificationTimeout : timeout };
119 | var notification = $.notify(notifySettings, options);
120 | if (allowDismiss == undefined || allowDismiss) {
121 | $(notification.$ele).click(function() {
122 | notification.close();
123 | });
124 | }
125 | return notification;
126 | }
127 |
128 | function showUpdateMessage(type, customTimeout) {
129 | // Determine the message and timespan for the notification
130 | var title, message, timeout;
131 | switch (type) {
132 | case 0: // Firmware
133 | title = T("Updating Firmware...");
134 | message = T("Please wait while the firmware is being updated...");
135 | timeout = settings.updateReconnectDelay;
136 | break;
137 |
138 | case 1: // Duet WiFi Server
139 | title = T("Updating WiFi Server...");
140 | message = T("Please wait while the Duet WiFi Server firmware is being updated...");
141 | timeout = settings.dwsReconnectDelay;
142 | break;
143 |
144 | case 2: // Duet Web Control
145 | title = T("Updating Web Interface...");
146 | message = T("Please wait while Duet Web Control is being updated...");
147 | timeout = settings.dwcReconnectDelay;
148 | break;
149 |
150 | case 3: // Multiple Updates
151 | title = T("Updating Firmware...");
152 | message = T("Please wait while multiple firmware updates are being installed...");
153 | timeout = customTimeout;
154 | break;
155 |
156 | default: // Unknown
157 | alert(T("Error! Unknown update parameter!"));
158 | return;
159 | }
160 |
161 | // Display backdrop and hide it when ready
162 | $("#modal_backdrop").modal("show");
163 | setTimeout(function() {
164 | $("#modal_backdrop").modal("hide");
165 | }, timeout);
166 |
167 | // Display notification
168 | var notifySettings = { icon: "glyphicon glyphicon-time",
169 | title: "" + title + "
",
170 | message: message,
171 | progress: timeout / 100,
172 | };
173 | var options = { type: "success",
174 | allow_dismiss: false,
175 | delay: timeout,
176 | timeout: 1000,
177 | showProgressbar: true
178 | };
179 | var notification = $.notify(notifySettings, options);
180 | $(notification.$ele).css("z-index", 9999);
181 | return notification;
182 | }
183 |
184 |
--------------------------------------------------------------------------------
/core/js/3rd-party/FileSaver.js:
--------------------------------------------------------------------------------
1 | /* FileSaver.js
2 | * A saveAs() FileSaver implementation.
3 | * 1.3.2
4 | * 2016-06-16 18:25:19
5 | *
6 | * By Eli Grey, http://eligrey.com
7 | * License: MIT
8 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
9 | */
10 |
11 | /*global self */
12 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
13 |
14 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
15 |
16 | var saveAs = saveAs || (function(view) {
17 | "use strict";
18 | // IE <10 is explicitly unsupported
19 | if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
20 | return;
21 | }
22 | var
23 | doc = view.document
24 | // only get URL when necessary in case Blob.js hasn't overridden it yet
25 | , get_URL = function() {
26 | return view.URL || view.webkitURL || view;
27 | }
28 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
29 | , can_use_save_link = "download" in save_link
30 | , click = function(node) {
31 | var event = new MouseEvent("click");
32 | node.dispatchEvent(event);
33 | }
34 | , is_safari = /constructor/i.test(view.HTMLElement) || view.safari
35 | , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
36 | , throw_outside = function(ex) {
37 | (view.setImmediate || view.setTimeout)(function() {
38 | throw ex;
39 | }, 0);
40 | }
41 | , force_saveable_type = "application/octet-stream"
42 | // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
43 | , arbitrary_revoke_timeout = 1000 * 40 // in ms
44 | , revoke = function(file) {
45 | var revoker = function() {
46 | if (typeof file === "string") { // file is an object URL
47 | get_URL().revokeObjectURL(file);
48 | } else { // file is a File
49 | file.remove();
50 | }
51 | };
52 | setTimeout(revoker, arbitrary_revoke_timeout);
53 | }
54 | , dispatch = function(filesaver, event_types, event) {
55 | event_types = [].concat(event_types);
56 | var i = event_types.length;
57 | while (i--) {
58 | var listener = filesaver["on" + event_types[i]];
59 | if (typeof listener === "function") {
60 | try {
61 | listener.call(filesaver, event || filesaver);
62 | } catch (ex) {
63 | throw_outside(ex);
64 | }
65 | }
66 | }
67 | }
68 | , auto_bom = function(blob) {
69 | // prepend BOM for UTF-8 XML and text/* types (including HTML)
70 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
71 | if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
72 | return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
73 | }
74 | return blob;
75 | }
76 | , FileSaver = function(blob, name, no_auto_bom) {
77 | if (!no_auto_bom) {
78 | blob = auto_bom(blob);
79 | }
80 | // First try a.download, then web filesystem, then object URLs
81 | var
82 | filesaver = this
83 | , type = blob.type
84 | , force = type === force_saveable_type
85 | , object_url
86 | , dispatch_all = function() {
87 | dispatch(filesaver, "writestart progress write writeend".split(" "));
88 | }
89 | // on any filesys errors revert to saving with object URLs
90 | , fs_error = function() {
91 | if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
92 | // Safari doesn't allow downloading of blob urls
93 | var reader = new FileReader();
94 | reader.onloadend = function() {
95 | var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
96 | var popup = view.open(url, '_blank');
97 | if(!popup) view.location.href = url;
98 | url=undefined; // release reference before dispatching
99 | filesaver.readyState = filesaver.DONE;
100 | dispatch_all();
101 | };
102 | reader.readAsDataURL(blob);
103 | filesaver.readyState = filesaver.INIT;
104 | return;
105 | }
106 | // don't create more object URLs than needed
107 | if (!object_url) {
108 | object_url = get_URL().createObjectURL(blob);
109 | }
110 | if (force) {
111 | view.location.href = object_url;
112 | } else {
113 | var opened = view.open(object_url, "_blank");
114 | if (!opened) {
115 | // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
116 | view.location.href = object_url;
117 | }
118 | }
119 | filesaver.readyState = filesaver.DONE;
120 | dispatch_all();
121 | revoke(object_url);
122 | }
123 | ;
124 | filesaver.readyState = filesaver.INIT;
125 |
126 | if (can_use_save_link) {
127 | object_url = get_URL().createObjectURL(blob);
128 | setTimeout(function() {
129 | save_link.href = object_url;
130 | save_link.download = name;
131 | click(save_link);
132 | dispatch_all();
133 | revoke(object_url);
134 | filesaver.readyState = filesaver.DONE;
135 | });
136 | return;
137 | }
138 |
139 | fs_error();
140 | }
141 | , FS_proto = FileSaver.prototype
142 | , saveAs = function(blob, name, no_auto_bom) {
143 | return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
144 | }
145 | ;
146 | // IE 10+ (native saveAs)
147 | if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
148 | return function(blob, name, no_auto_bom) {
149 | name = name || blob.name || "download";
150 |
151 | if (!no_auto_bom) {
152 | blob = auto_bom(blob);
153 | }
154 | return navigator.msSaveOrOpenBlob(blob, name);
155 | };
156 | }
157 |
158 | FS_proto.abort = function(){};
159 | FS_proto.readyState = FS_proto.INIT = 0;
160 | FS_proto.WRITING = 1;
161 | FS_proto.DONE = 2;
162 |
163 | FS_proto.error =
164 | FS_proto.onwritestart =
165 | FS_proto.onprogress =
166 | FS_proto.onwrite =
167 | FS_proto.onabort =
168 | FS_proto.onerror =
169 | FS_proto.onwriteend =
170 | null;
171 |
172 | return saveAs;
173 | }(
174 | typeof self !== "undefined" && self
175 | || typeof window !== "undefined" && window
176 | || this.content
177 | ));
178 | // `self` is undefined in Firefox for Android content script context
179 | // while `this` is nsIContentFrameMessageManager
180 | // with an attribute `content` that corresponds to the window
181 |
182 | if (typeof module !== "undefined" && module.exports) {
183 | module.exports.saveAs = saveAs;
184 | } else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) {
185 | define("FileSaver.js", function() {
186 | return saveAs;
187 | });
188 | }
189 |
--------------------------------------------------------------------------------
/core/js/sliders.js:
--------------------------------------------------------------------------------
1 | /* Slider implementation for the Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | var fanSliderActive, speedSliderActive, extrSliderActive;
11 | var overriddenFanValues = [undefined, undefined, undefined]; // this must hold maxFans items
12 |
13 |
14 | /* Fan Control */
15 |
16 | $('#slider_fan_control').slider({
17 | enabled: false,
18 | id: "fan_control",
19 | min: 0,
20 | max: 100,
21 | step: 1,
22 | value: 35,
23 | tooltip: "always",
24 | formatter: function(value) {
25 | return value + " %";
26 | }
27 | }).on("slideStart", function() {
28 | fanSliderActive = true;
29 | }).on("slideStop", function(slideEvt) {
30 | if (isConnected && !isNaN(slideEvt.value)) {
31 | var fan = getFanSelection();
32 | var fanValue = slideEvt.value / 100.0;
33 | if (fan == undefined) {
34 | // Generic print fan is selected
35 | sendGCode("M106 S" + fanValue);
36 | } else {
37 | // Specific fan is selected
38 | if (overriddenFanValues[fan] != undefined) {
39 | overriddenFanValues[fan] = fanValue;
40 | }
41 | sendGCode("M106 P" + fan + " S" + fanValue);
42 | }
43 | $("#slider_fan_print").slider("setValue", slideEvt.value);
44 | }
45 | fanSliderActive = false;
46 | });
47 |
48 | $('#slider_fan_print').slider({
49 | enabled: false,
50 | id: "fan_print",
51 | min: 0,
52 | max: 100,
53 | step: 1,
54 | value: 35,
55 | tooltip: "always",
56 | formatter: function(value) {
57 | return value + " %";
58 | }
59 | }).on("slideStart", function() {
60 | fanSliderActive = true;
61 | }).on("slideStop", function(slideEvt) {
62 | if (isConnected && !isNaN(slideEvt.value)) {
63 | var fan = getFanSelection();
64 | var fanValue = slideEvt.value / 100.0;
65 | if (fan == undefined) {
66 | // Generic print fan is selected
67 | sendGCode("M106 S" + fanValue);
68 | } else {
69 | // Specific fan is selected
70 | if (overriddenFanValues[fan] != undefined) {
71 | overriddenFanValues[fan] = fanValue;
72 | }
73 | sendGCode("M106 P" + fan + " S" + fanValue);
74 | }
75 | $("#slider_fan_control").slider("setValue", slideEvt.value);
76 | }
77 | fanSliderActive = false;
78 | });
79 |
80 | function setFanVisibility(fan, visible) {
81 | var visibleFans = $(".table-fan-control [data-fan]:not(.hidden)");
82 |
83 | // update selection and check if the whole panel can be hidden
84 | var hideFanControl = false;
85 | if (visible) {
86 | /*if (visibleFans.length == 0) {
87 | // set selected fan to this fan
88 | setFanSelection(fan);
89 | }*/
90 | } else {
91 | var firstVisibleFan;
92 | visibleFans.each(function() {
93 | if ($(this).data("fan") != fan) {
94 | firstVisibleFan = $(this);
95 | return false;
96 | }
97 | });
98 |
99 | hideFanControl = (firstVisibleFan == undefined);
100 | /*if (!hideFanControl) {
101 | // set selected fan to first visible fan
102 | setFanSelection(firstVisibleFan.data("fan"));
103 | }*/
104 | }
105 |
106 | $(".fan-control").toggleClass("hidden", hideFanControl);
107 | if (hideFanControl) {
108 | // Hide entire misc control panel if ATX control is hidden
109 | $("#panel_control_misc").toggleClass("hidden", !settings.showATXControl);
110 | }
111 |
112 | // set visibility of control buttons
113 | $('.table-fan-control [data-fan="' + fan + '"]').toggleClass("hidden", !visible);
114 |
115 | // if this fan value is being enforced, undo it
116 | setFanOverride(fan, undefined);
117 | }
118 |
119 | function getFanSelection() {
120 | var selection = $(".table-fan-control button.btn-primary.fan-selection");
121 | return (selection.length == 0) ? undefined : selection.data("fan");
122 | }
123 |
124 | function setFanSelection(fan) {
125 | $(".table-fan-control button.fan-selection").removeClass("btn-primary").removeClass("active").addClass("btn-default");
126 | if (fan != undefined) {
127 | $('.table-fan-control button.fan-selection[data-fan="' + fan + '"]').removeClass("btn-default").addClass("btn-primary").addClass("active");
128 | }
129 | }
130 |
131 | function setFanOverride(fan, overriddenValue) {
132 | // set model value
133 | overriddenFanValues[fan] = overriddenValue;
134 |
135 | // update UI
136 | var overridden = (overriddenValue != undefined);
137 | var toggleButton = $('.table-fan-control button.fan-override[data-fan="' + fan + '"]');
138 | toggleButton.toggleClass("btn-primary", overridden).toggleClass("btn-default", !overridden);
139 | toggleButton.toggleClass("active", overridden);
140 | }
141 |
142 | $("button.fan-selection").click(function() {
143 | var fan = $(this).data("fan");
144 | if ($(this).hasClass("disabled")) {
145 | // only apply other values if the selection has changed
146 | return;
147 | }
148 |
149 | if (fan == getFanSelection()) {
150 | setFanSelection(undefined);
151 | return;
152 | }
153 | setFanSelection(fan);
154 |
155 | var fanValue = overriddenFanValues[fan];
156 | if (fanValue == undefined && lastStatusResponse != undefined) {
157 | // this is only called if the firmware reports fan values as an array
158 | fanValue = lastStatusResponse.params.fanPercent[fan] / 100.0;
159 | }
160 | if (fanValue != undefined) {
161 | $(".fan-slider").children("input").slider("setValue", fanValue * 100.0);
162 | }
163 | });
164 |
165 | $("button.fan-override").click(function() {
166 | var fan = $(this).data("fan");
167 | if ($(this).hasClass("disabled")) {
168 | return;
169 | }
170 |
171 | if (overriddenFanValues[fan] == undefined) {
172 | if (fan == getFanSelection()) {
173 | var sliderValue = $(".fan-slider").children("input").slider("getValue") / 100.0;
174 | setFanOverride(fan, sliderValue);
175 | }
176 | } else {
177 | setFanOverride(fan, undefined);
178 | }
179 | });
180 |
181 |
182 | /* Extrusion Multiplier */
183 |
184 | for(var extr = 0; extr < maxExtruders; extr++) {
185 | $('#slider_extr_' + extr).slider({
186 | enabled: false,
187 | id: "extr-" + extr,
188 | min: 50,
189 | max: 150,
190 | step: 1,
191 | value: 100,
192 | tooltip: "always",
193 | formatter: function(value) {
194 | return value + " %";
195 | }
196 | }).on("slideStart", function() {
197 | extrSliderActive = true;
198 | }).on("slideStop", function(slideEvt) {
199 | if (isConnected && !isNaN(slideEvt.value)) {
200 | sendGCode("M221 D" + $(this).data("drive") + " S" + slideEvt.value);
201 | }
202 | extrSliderActive = false;
203 | });
204 | }
205 |
206 |
207 | /* Speed slider */
208 |
209 | $('#slider_speed').slider({
210 | enabled: false,
211 | id: "speed",
212 | min: 20,
213 | max: 300,
214 | step: 1,
215 | value: 100,
216 | tooltip: "always",
217 | formatter: function(value) {
218 | return value + " %";
219 | }
220 | }).on("slideStart", function() {
221 | speedSliderActive = true;
222 | }).on("slideStop", function(slideEvt) {
223 | if (isConnected && !isNaN(slideEvt.value)) {
224 | sendGCode("M220 S" + slideEvt.value);
225 | }
226 | speedSliderActive = false;
227 | });
228 |
--------------------------------------------------------------------------------
/core/js/3rd-party/jquery.flot.dashes.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery.flot.dashes
3 | *
4 | * options = {
5 | * series: {
6 | * dashes: {
7 | *
8 | * // show
9 | * // default: false
10 | * // Whether to show dashes for the series.
11 | * show: ,
12 | *
13 | * // lineWidth
14 | * // default: 2
15 | * // The width of the dashed line in pixels.
16 | * lineWidth: ,
17 | *
18 | * // dashLength
19 | * // default: 10
20 | * // Controls the length of the individual dashes and the amount of
21 | * // space between them.
22 | * // If this is a number, the dashes and spaces will have that length.
23 | * // If this is an array, it is read as [ dashLength, spaceLength ]
24 | * dashLength: or
25 | * }
26 | * }
27 | * }
28 | */
29 | (function($){
30 |
31 | function init(plot) {
32 | plot.hooks.drawSeries.push(function(plot, ctx, series) {
33 | if (!series.dashes.show) return;
34 |
35 | var plotOffset = plot.getPlotOffset(),
36 | axisx = series.xaxis,
37 | axisy = series.yaxis;
38 |
39 | function plotDashes(xoffset, yoffset) {
40 |
41 | var points = series.datapoints.points,
42 | ps = series.datapoints.pointsize,
43 | prevx = null,
44 | prevy = null,
45 | dashRemainder = 0,
46 | dashOn = true,
47 | dashOnLength,
48 | dashOffLength;
49 |
50 | if (series.dashes.dashLength[0]) {
51 | dashOnLength = series.dashes.dashLength[0];
52 | if (series.dashes.dashLength[1]) {
53 | dashOffLength = series.dashes.dashLength[1];
54 | } else {
55 | dashOffLength = dashOnLength;
56 | }
57 | } else {
58 | dashOffLength = dashOnLength = series.dashes.dashLength;
59 | }
60 |
61 | ctx.beginPath();
62 |
63 | for (var i = ps; i < points.length; i += ps) {
64 |
65 | var x1 = points[i - ps],
66 | y1 = points[i - ps + 1],
67 | x2 = points[i],
68 | y2 = points[i + 1];
69 |
70 | if (x1 == null || x2 == null) continue;
71 |
72 | // clip with ymin
73 | if (y1 <= y2 && y1 < axisy.min) {
74 | if (y2 < axisy.min) continue; // line segment is outside
75 | // compute new intersection point
76 | x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
77 | y1 = axisy.min;
78 | } else if (y2 <= y1 && y2 < axisy.min) {
79 | if (y1 < axisy.min) continue;
80 | x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
81 | y2 = axisy.min;
82 | }
83 |
84 | // clip with ymax
85 | if (y1 >= y2 && y1 > axisy.max) {
86 | if (y2 > axisy.max) continue;
87 | x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
88 | y1 = axisy.max;
89 | } else if (y2 >= y1 && y2 > axisy.max) {
90 | if (y1 > axisy.max) continue;
91 | x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
92 | y2 = axisy.max;
93 | }
94 |
95 | // clip with xmin
96 | if (x1 <= x2 && x1 < axisx.min) {
97 | if (x2 < axisx.min) continue;
98 | y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
99 | x1 = axisx.min;
100 | } else if (x2 <= x1 && x2 < axisx.min) {
101 | if (x1 < axisx.min) continue;
102 | y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
103 | x2 = axisx.min;
104 | }
105 |
106 | // clip with xmax
107 | if (x1 >= x2 && x1 > axisx.max) {
108 | if (x2 > axisx.max) continue;
109 | y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
110 | x1 = axisx.max;
111 | } else if (x2 >= x1 && x2 > axisx.max) {
112 | if (x1 > axisx.max) continue;
113 | y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
114 | x2 = axisx.max;
115 | }
116 |
117 | if (x1 != prevx || y1 != prevy) {
118 | ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
119 | }
120 |
121 | var ax1 = axisx.p2c(x1) + xoffset,
122 | ay1 = axisy.p2c(y1) + yoffset,
123 | ax2 = axisx.p2c(x2) + xoffset,
124 | ay2 = axisy.p2c(y2) + yoffset,
125 | dashOffset;
126 |
127 | function lineSegmentOffset(segmentLength) {
128 |
129 | var c = Math.sqrt(Math.pow(ax2 - ax1, 2) + Math.pow(ay2 - ay1, 2));
130 |
131 | if (c <= segmentLength) {
132 | return {
133 | deltaX: ax2 - ax1,
134 | deltaY: ay2 - ay1,
135 | distance: c,
136 | remainder: segmentLength - c
137 | }
138 | } else {
139 | var xsign = ax2 > ax1 ? 1 : -1,
140 | ysign = ay2 > ay1 ? 1 : -1;
141 | return {
142 | deltaX: xsign * Math.sqrt(Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))),
143 | deltaY: ysign * Math.sqrt(Math.pow(segmentLength, 2) - Math.pow(segmentLength, 2) / (1 + Math.pow((ay2 - ay1)/(ax2 - ax1), 2))),
144 | distance: segmentLength,
145 | remainder: 0
146 | };
147 | }
148 | }
149 | //-end lineSegmentOffset
150 |
151 | do {
152 |
153 | dashOffset = lineSegmentOffset(
154 | dashRemainder > 0 ? dashRemainder :
155 | dashOn ? dashOnLength : dashOffLength);
156 |
157 | if (dashOffset.deltaX != 0 || dashOffset.deltaY != 0) {
158 | if (dashOn) {
159 | ctx.lineTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY);
160 | } else {
161 | ctx.moveTo(ax1 + dashOffset.deltaX, ay1 + dashOffset.deltaY);
162 | }
163 | }
164 |
165 | dashOn = !dashOn;
166 | dashRemainder = dashOffset.remainder;
167 | ax1 += dashOffset.deltaX;
168 | ay1 += dashOffset.deltaY;
169 |
170 | } while (dashOffset.distance > 0);
171 |
172 | prevx = x2;
173 | prevy = y2;
174 | }
175 |
176 | ctx.stroke();
177 | }
178 | //-end plotDashes
179 |
180 | ctx.save();
181 | ctx.translate(plotOffset.left, plotOffset.top);
182 | ctx.lineJoin = 'round';
183 |
184 | var lw = series.dashes.lineWidth,
185 | sw = series.shadowSize;
186 |
187 | // FIXME: consider another form of shadow when filling is turned on
188 | if (lw > 0 && sw > 0) {
189 | // draw shadow as a thick and thin line with transparency
190 | ctx.lineWidth = sw;
191 | ctx.strokeStyle = "rgba(0,0,0,0.1)";
192 | // position shadow at angle from the mid of line
193 | var angle = Math.PI/18;
194 | plotDashes(Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2));
195 | ctx.lineWidth = sw/2;
196 | plotDashes(Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4));
197 | }
198 |
199 | ctx.lineWidth = lw;
200 | ctx.strokeStyle = series.color;
201 |
202 | if (lw > 0) {
203 | plotDashes(0, 0);
204 | }
205 |
206 | ctx.restore();
207 |
208 | });
209 | //-end draw hook
210 | }
211 | //-end init
212 |
213 | $.plot.plugins.push({
214 | init: init,
215 | options: {
216 | series: {
217 | dashes: {
218 | show: false,
219 | lineWidth: 2,
220 | dashLength: 10
221 | }
222 | }
223 | },
224 | name: 'dashes',
225 | version: '0.1'
226 | });
227 |
228 | })(jQuery)
229 |
--------------------------------------------------------------------------------
/core/js/charts.js:
--------------------------------------------------------------------------------
1 | /* Chart interface logic for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | var tempChart;
11 | var tempChartOptions = {
12 | // This array should hold maxHeaters + maxTempSensors items
13 | colors: ["#0000FF", "#FF0000", "#00DD00", "#FFA000", "#FF00FF", "#337AB7", "#000000", "#E0E000", // Heater colors
14 | "#AEAEAE", "#BC0000", "#00CB00", "#0000DC", "#FEABEF", "#A0A000", "#DDDD00", "#00BDBD", "#CCBBAA", "#AA00AA"], // Virtual heater colors
15 | grid: {
16 | borderWidth: 0
17 | },
18 | xaxis: {
19 | show: false
20 | /*labelWidth: 0,
21 | labelHeight: 0,
22 | tickSize: 30000,
23 | tickFormatter: function() { return ""; },
24 | reserveSpace: false*/
25 | },
26 | yaxis: {
27 | min: 0,
28 | max: 280
29 | }
30 | };
31 | var tempChartPadding = 15;
32 |
33 | var maxTemperatureSamples = 1000;
34 | var maxLayerTime = 0;
35 |
36 |
37 | var printChart;
38 | var printChartOptions = {
39 | colors: ["#EDC240"],
40 | grid: {
41 | borderWidth: 0,
42 | hoverable: true,
43 | clickable: true
44 | },
45 | pan: {
46 | interactive: true
47 | },
48 | series: {
49 | lines: {
50 | show: true
51 | },
52 | points: {
53 | show: true
54 | }
55 | },
56 | xaxis: {
57 | min: 1,
58 | tickDecimals: 0,
59 | },
60 | yaxis: {
61 | min: 0,
62 | max: 30,
63 | ticks: 5,
64 | tickDecimals: 0,
65 | tickFormatter: function(val) {
66 | if (!val) {
67 | return "";
68 | } else {
69 | return formatTime(val);
70 | }
71 | }
72 | },
73 | zoom: {
74 | interactive: true
75 | }
76 | };
77 |
78 | var refreshTempChart = false, refreshPrintChart = false;
79 | var recordedTemperatures, layerData;
80 |
81 |
82 | /* Temperature chart */
83 |
84 | function recordCurrentTemperatures(temps) {
85 | var timeNow = (new Date()).getTime();
86 |
87 | // Add temperatures for each heater
88 | // Also cut off the last one if there are too many temperature samples
89 | for(var heater = 0; heater < maxHeaters; heater++) {
90 | if (heater < temps.length && heatersInUse[heater]) {
91 | recordedTemperatures[heater].push([timeNow, temps[heater]]);
92 | } else {
93 | recordedTemperatures[heater].push([]);
94 | }
95 |
96 | if (recordedTemperatures[heater].length > maxTemperatureSamples) {
97 | recordedTemperatures[heater].shift();
98 | }
99 | }
100 | }
101 |
102 | function recordExtraTemperatures(temps) {
103 | var timeNow = (new Date()).getTime();
104 |
105 | // Add dashed series for each temperature sensor
106 | for(var i = 0; i < maxTempSensors; i++)
107 | {
108 | if (i < temps.length) {
109 | recordedTemperatures[maxHeaters + i].data.push([timeNow, temps[i].temp]);
110 | } else {
111 | recordedTemperatures[maxHeaters + i].data.push([]);
112 | }
113 |
114 | if (recordedTemperatures[maxHeaters + i].data.length > maxTemperatureSamples) {
115 | recordedTemperatures[maxHeaters + i].data.shift();
116 | }
117 | }
118 | }
119 |
120 | function drawTemperatureChart() {
121 | // Only draw the chart if it's possible
122 | if ($("#chart_temp").width() === 0) {
123 | refreshTempChart = true;
124 | return;
125 | }
126 |
127 | // Check if we need to recreate the chart
128 | var recreateChart = false;
129 | if (tempLimit != tempChartOptions.yaxis.max) {
130 | tempChartOptions.yaxis.max = tempLimit;
131 | recreateChart = true;
132 | }
133 |
134 | // Draw it
135 | if (tempChart == undefined || recreateChart) {
136 | tempChart = $.plot("#chart_temp", recordedTemperatures, tempChartOptions);
137 | } else {
138 | tempChart.setData(recordedTemperatures);
139 | tempChart.setupGrid();
140 | tempChart.draw();
141 | }
142 |
143 | refreshTempChart = false;
144 | }
145 |
146 | function setExtraTemperatureVisibility(sensor, visible) {
147 | // Update visibility
148 | recordedTemperatures[maxHeaters + sensor].dashes.show = visible;
149 |
150 | // Save state in localStorage
151 | var extraSensorVisibility = JSON.parse(localStorage.getItem("extraSensorVisibility"));
152 | extraSensorVisibility[sensor] = visible;
153 | localStorage.setItem("extraSensorVisibility", JSON.stringify(extraSensorVisibility));
154 | }
155 |
156 |
157 | /* Print statistics chart */
158 |
159 | function addLayerData(lastLayerTime, filamentUsed, updateGui) {
160 | layerData.push([layerData.length + 1, lastLayerTime, filamentUsed]);
161 | if (lastLayerTime > maxLayerTime) {
162 | maxLayerTime = lastLayerTime;
163 | }
164 |
165 | if (updateGui) {
166 | $("#td_last_layertime").html(formatTime(lastLayerTime)).addClass("layer-done-animation");
167 | setTimeout(function() {
168 | $("#td_last_layertime").removeClass("layer-done-animation");
169 | }, 2000);
170 |
171 | drawPrintChart();
172 | }
173 | }
174 |
175 | function drawPrintChart() {
176 | // Only draw the chart if it's possible
177 | if ($("#chart_print").width() === 0) {
178 | refreshPrintChart = true;
179 | return;
180 | }
181 |
182 | // Find absolute maximum values for the X axis
183 | var maxX = 25, maxPanX = 25;
184 | if (layerData.length < 21) {
185 | maxX = maxPanX = 20;
186 | } else if (layerData.length < 25) {
187 | maxX = maxPanX = layerData.length;
188 | } else {
189 | maxPanX = layerData.length;
190 | }
191 | printChartOptions.xaxis.max = maxX;
192 | printChartOptions.xaxis.panRange = [1, maxPanX];
193 | printChartOptions.xaxis.zoomRange = [25, maxPanX];
194 |
195 | // Find max visible value for Y axis
196 | var maxY = 30;
197 | if (layerData.length > 1) {
198 | var firstLayerToCheck = (layerData.length > 26) ? layerData.length - 25 : 1;
199 | for(var i = firstLayerToCheck; i < layerData.length; i++) {
200 | var layerVal = layerData[i][1] * 1.1;
201 | if (maxY < layerVal) {
202 | maxY = layerVal;
203 | }
204 | }
205 | }
206 | printChartOptions.yaxis.max = maxY;
207 | printChartOptions.yaxis.panRange = [0, (maxLayerTime < maxY) ? maxY : maxLayerTime];
208 | printChartOptions.yaxis.zoomRange = [30, (maxLayerTime < maxY) ? maxY : maxLayerTime];
209 |
210 | // Update chart and pan to the right
211 | printChart = $.plot("#chart_print", [layerData], printChartOptions);
212 | printChart.pan({ left: 99999 });
213 | refreshPrintChart = false;
214 |
215 | // Add hover events to chart
216 | $("#chart_print").unbind("plothover").bind("plothover", function(e, pos, item) {
217 | if (item) {
218 | // Get layer number and time used
219 | var layer = item.datapoint[0];
220 | var timeUsed = item.datapoint[1];
221 |
222 | // Get filament usage
223 | var filamentUsed;
224 | if (layer > 1) {
225 | filamentUsed = layerData[layer - 1][2] - layerData[layer - 2][2];
226 | } else {
227 | filamentUsed = layerData[layer - 1][2];
228 | }
229 |
230 | // Build tool tip
231 | var toolTip = "" + T("Layer {0}", layer) + "
";
232 | toolTip += T("Time: {0}", formatTime(timeUsed)) + "
";
233 | toolTip += T("Filament usage: {0} mm", filamentUsed.toFixed(1));
234 |
235 | // Show it
236 | $("#layer_tooltip").html(toolTip).css({top: item.pageY + 5, left: item.pageX + 5}).fadeIn(200);
237 | } else {
238 | $("#layer_tooltip").hide();
239 | }
240 | });
241 | }
242 |
243 |
244 | /* Common functions */
245 |
246 | function resizeCharts() {
247 | var contentHeight = $("#table_tools").height();
248 | if (!$("#div_heaters").hasClass("hidden")) {
249 | contentHeight = $("#table_heaters").height();
250 | } else if (!$("#div_extra").hasClass("hidden")) {
251 | contentHeight = $("#table_extra").height();
252 | }
253 |
254 | var statusHeight = 0;
255 | $("#div_status table").each(function() {
256 | statusHeight += $(this).outerHeight();
257 | });
258 |
259 | var max = (contentHeight > statusHeight) ? contentHeight : statusHeight;
260 | max -= tempChartPadding;
261 |
262 | if (max > 0) {
263 | $("#chart_temp").css("height", max);
264 | }
265 |
266 | if (refreshTempChart) {
267 | drawTemperatureChart();
268 | }
269 | if (refreshPrintChart) {
270 | drawPrintChart();
271 | }
272 | }
273 |
274 | $(".panel-chart").resize(function() {
275 | resizeCharts();
276 | });
277 |
278 | function resetChartData() {
279 | // Make sure the visibility state can be saved in localStorage
280 | var extraSensorVisibility = localStorage.getItem("extraSensorVisibility");
281 | if (extraSensorVisibility == null) {
282 | extraSensorVisibility = [];
283 | for(var i = 0; i < maxTempSensors; i++) {
284 | // Don't show any extra temperatures in the chart by default
285 | extraSensorVisibility.push(false);
286 | }
287 | localStorage.setItem("extraSensorVisibility", JSON.stringify(extraSensorVisibility));
288 | } else {
289 | extraSensorVisibility = JSON.parse(extraSensorVisibility);
290 | }
291 |
292 | // Reset data of the temperature chart
293 | recordedTemperatures = [];
294 | for(var i = 0; i < maxHeaters; i++) {
295 | recordedTemperatures.push([]);
296 | }
297 |
298 | for(var i = 0; i < maxTempSensors; i++) {
299 | recordedTemperatures.push({
300 | dashes: { show: extraSensorVisibility[i] },
301 | lines: { show: false },
302 | data: []
303 | });
304 |
305 | $("#table_extra tr input[type='checkbox']").eq(i).prop("checked", extraSensorVisibility[i]);
306 | }
307 |
308 | // Reset data of the layer chart
309 | layerData = [];
310 | maxLayerTime = 0;
311 | }
312 |
--------------------------------------------------------------------------------
/core/css/bootstrap-slider.css:
--------------------------------------------------------------------------------
1 | /*! =======================================================
2 | VERSION 6.1.4
3 | ========================================================= */
4 | /*! =========================================================
5 | * bootstrap-slider.js
6 | *
7 | * Maintainers:
8 | * Kyle Kemp
9 | * - Twitter: @seiyria
10 | * - Github: seiyria
11 | * Rohit Kalkur
12 | * - Twitter: @Rovolutionary
13 | * - Github: rovolution
14 | *
15 | * =========================================================
16 | *
17 | * Licensed under the Apache License, Version 2.0 (the "License");
18 | * you may not use this file except in compliance with the License.
19 | * You may obtain a copy of the License at
20 | *
21 | * http://www.apache.org/licenses/LICENSE-2.0
22 | *
23 | * Unless required by applicable law or agreed to in writing, software
24 | * distributed under the License is distributed on an "AS IS" BASIS,
25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26 | * See the License for the specific language governing permissions and
27 | * limitations under the License.
28 | * ========================================================= */
29 | .slider {
30 | display: inline-block;
31 | vertical-align: middle;
32 | position: relative;
33 | }
34 | .slider.slider-horizontal {
35 | width: 210px;
36 | height: 20px;
37 | }
38 | .slider.slider-horizontal .slider-track {
39 | height: 10px;
40 | width: 100%;
41 | margin-top: -5px;
42 | top: 50%;
43 | left: 0;
44 | }
45 | .slider.slider-horizontal .slider-selection,
46 | .slider.slider-horizontal .slider-track-low,
47 | .slider.slider-horizontal .slider-track-high {
48 | height: 100%;
49 | top: 0;
50 | bottom: 0;
51 | }
52 | .slider.slider-horizontal .slider-tick,
53 | .slider.slider-horizontal .slider-handle {
54 | margin-left: -10px;
55 | margin-top: -5px;
56 | }
57 | .slider.slider-horizontal .slider-tick.triangle,
58 | .slider.slider-horizontal .slider-handle.triangle {
59 | border-width: 0 10px 10px 10px;
60 | width: 0;
61 | height: 0;
62 | border-bottom-color: #0480be;
63 | margin-top: 0;
64 | }
65 | .slider.slider-horizontal .slider-tick-label-container {
66 | white-space: nowrap;
67 | margin-top: 20px;
68 | }
69 | .slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
70 | padding-top: 4px;
71 | display: inline-block;
72 | text-align: center;
73 | }
74 | .slider.slider-vertical {
75 | height: 210px;
76 | width: 20px;
77 | }
78 | .slider.slider-vertical .slider-track {
79 | width: 10px;
80 | height: 100%;
81 | margin-left: -5px;
82 | left: 50%;
83 | top: 0;
84 | }
85 | .slider.slider-vertical .slider-selection {
86 | width: 100%;
87 | left: 0;
88 | top: 0;
89 | bottom: 0;
90 | }
91 | .slider.slider-vertical .slider-track-low,
92 | .slider.slider-vertical .slider-track-high {
93 | width: 100%;
94 | left: 0;
95 | right: 0;
96 | }
97 | .slider.slider-vertical .slider-tick,
98 | .slider.slider-vertical .slider-handle {
99 | margin-left: -5px;
100 | margin-top: -10px;
101 | }
102 | .slider.slider-vertical .slider-tick.triangle,
103 | .slider.slider-vertical .slider-handle.triangle {
104 | border-width: 10px 0 10px 10px;
105 | width: 1px;
106 | height: 1px;
107 | border-left-color: #0480be;
108 | margin-left: 0;
109 | }
110 | .slider.slider-vertical .slider-tick-label-container {
111 | white-space: nowrap;
112 | }
113 | .slider.slider-vertical .slider-tick-label-container .slider-tick-label {
114 | padding-left: 4px;
115 | }
116 | .slider.slider-disabled .slider-handle {
117 | background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
118 | background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
119 | background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
120 | background-repeat: repeat-x;
121 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
122 | }
123 | .slider.slider-disabled .slider-track {
124 | background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
125 | background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
126 | background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
127 | background-repeat: repeat-x;
128 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
129 | cursor: not-allowed;
130 | }
131 | .slider input {
132 | display: none;
133 | }
134 | .slider .tooltip.top {
135 | margin-top: -36px;
136 | }
137 | .slider .tooltip-inner {
138 | white-space: nowrap;
139 | max-width: none;
140 | }
141 | .slider .hide {
142 | display: none;
143 | }
144 | .slider-track {
145 | position: absolute;
146 | cursor: pointer;
147 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
148 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
149 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
150 | background-repeat: repeat-x;
151 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
152 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
153 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
154 | border-radius: 4px;
155 | }
156 | .slider-selection {
157 | position: absolute;
158 | background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
159 | background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
160 | background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
161 | background-repeat: repeat-x;
162 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
163 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
164 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
165 | -webkit-box-sizing: border-box;
166 | -moz-box-sizing: border-box;
167 | box-sizing: border-box;
168 | border-radius: 4px;
169 | }
170 | .slider-selection.tick-slider-selection {
171 | background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
172 | background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
173 | background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
174 | background-repeat: repeat-x;
175 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
176 | }
177 | .slider-track-low,
178 | .slider-track-high {
179 | position: absolute;
180 | background: transparent;
181 | -webkit-box-sizing: border-box;
182 | -moz-box-sizing: border-box;
183 | box-sizing: border-box;
184 | border-radius: 4px;
185 | }
186 | .slider-handle {
187 | position: absolute;
188 | width: 20px;
189 | height: 20px;
190 | background-color: #337ab7;
191 | background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
192 | background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
193 | background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
194 | background-repeat: repeat-x;
195 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
196 | filter: none;
197 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
198 | box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
199 | border: 0px solid transparent;
200 | }
201 | .slider-handle.round {
202 | border-radius: 50%;
203 | }
204 | .slider-handle.triangle {
205 | background: transparent none;
206 | }
207 | .slider-handle.custom {
208 | background: transparent none;
209 | }
210 | .slider-handle.custom::before {
211 | line-height: 20px;
212 | font-size: 20px;
213 | content: '\2605';
214 | color: #726204;
215 | }
216 | .slider-tick {
217 | position: absolute;
218 | width: 20px;
219 | height: 20px;
220 | background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
221 | background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
222 | background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
223 | background-repeat: repeat-x;
224 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
225 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
226 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
227 | -webkit-box-sizing: border-box;
228 | -moz-box-sizing: border-box;
229 | box-sizing: border-box;
230 | filter: none;
231 | opacity: 0.8;
232 | border: 0px solid transparent;
233 | }
234 | .slider-tick.round {
235 | border-radius: 50%;
236 | }
237 | .slider-tick.triangle {
238 | background: transparent none;
239 | }
240 | .slider-tick.custom {
241 | background: transparent none;
242 | }
243 | .slider-tick.custom::before {
244 | line-height: 20px;
245 | font-size: 20px;
246 | content: '\2605';
247 | color: #726204;
248 | }
249 | .slider-tick.in-selection {
250 | background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
251 | background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
252 | background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
253 | background-repeat: repeat-x;
254 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
255 | opacity: 1;
256 | }
257 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DuetWebControl
2 |
3 | Duet Web Control is a fully-responsive HTML5-based web interface for RepRapFirmware which utilizes the Bootstrap framework, JQuery and a few other libraries to allow easy control of Duet-based 3D printer electronics.
4 |
5 | It is designed to communicate with RepRapFirmware using HTTP GET requests and to upload single files using an HTTP POST request. One goal of the core application is to keep things compact, so a good loading speed can be achieved even on slow networks. Another one is to communicate to the firmware using only AJAX calls, which either return JSON objects, plain texts or binary blobs.
6 |
7 | Duet Web Control is free software; it is licensed under the terms of the GNU Public License v2.
8 |
9 | ## Supported electronics
10 |
11 | At this time the following platforms are supported:
12 |
13 | * Duet 0.6
14 | * Duet 0.8.5
15 | * Duet WiFi
16 | * Duet Ethernet
17 |
18 | ## Communication to the firmware
19 |
20 | Since RepRapFirmware can only process one HTTP request at a time (excluding rr_fileinfo and rr_upload on certain platforms), DuetWebControl should attempt to avoid parallel requests. In general, the communication between the web interface and RepRapFirmware looks like this:
21 |
22 | - Establish a connection (via rr_connect)
23 | - Send an extended status request (rr_status?type=2) and start status update loop
24 | - Load macros (rr_filelist?dir=/macros)
25 | - (User switches to "G-Code Files" tab)
26 | - Stop automatic status updates
27 | - Load G-code filelist (rr_filelist?dir=/gcodes)
28 | - File info about each file is requested (unless cached values are available)
29 | - Start automatic status requests again
30 | - (User does something else)
31 | - DWC disconnects (via rr_disconnect)
32 |
33 | Note the interrupt of live updates while multiple long requests are processed. DWC implements two particular functions (stopUpdates and startUpdates) which can - and should - be used to stop status requests while long-running HTTP requests are being executed. The update loop is stopped when file uploads are started, too, however it is not required to interrupt the update loop while short requests (e.g. rr_gcode) are sent.
34 |
35 | Some requests may send or expect date and time values. These values are represented by the format "YYYY-MM-DDTHH:MM:SS" similar to the ISO-8601 format.
36 |
37 | ## List of HTTP requests
38 |
39 | All HTTP requests, except for rr_upload, are simple GET requests that return JSON objects, which makes it easy to deal with them using JavaScript code. Here the list of all currently used requests:
40 |
41 | #### rr_connect?password=XXX&time=YYY
42 | Create an initial connection between DWC and RRF.
43 | - On success, the firmware sends out a response like: {"err":0,"sessionTimeout":[time in ms],"boardType":"[board type]"} This way DWC can adjust the AJAX timeout value and set board-specific options. The "time" value should represent the client's date and time to set the on-board RTC if necessary.
44 | - If anything goes wrong, the firmware only responds with an {"err":[code]} object. If code is 1, then the specified password is wrong. If it is 2, then the firmware cannot allocate enough resources to accomodate another session.
45 |
46 | #### rr_disconnect
47 | Delete an existing HTTP session. This should be used to log off manually, however sessions are usually purged automatically if no communication takes place within the time specified in "sessionTimeout" above.
48 |
49 | #### rr_status?type=XXX
50 | Request a status response from the firmware which usually includes all the machine parameters that are expected to change from time to time. This makes it possible to display live values like XYZ position and heater temperatures. This type of request is usually sent to the firmware in rather short intervals (250ms by default). At this time there are three different supported status request types, which may be polled in different intervals:
51 |
52 | - Type 1: Regular status request. The response for this is usually rather compact and only includes values that are expected to change quickly. The following types 2 and 3 include those values under any circumstances to keep the web interface up-to-date.
53 | - Type 2: Extended status request. This type of request is polled right after a connection has been established. This response provides information about the tool mapping and values that can change.
54 | - Type 3: Print status request. Unlike type 2, this type of request is always polled when a file print is in progress. It provides print time estimations and other print-related information which can be shown on the print status page.
55 |
56 | #### rr_gcode?gcode=XXX
57 | Send a G-code to the firmware. Since RepRapFirmware is generally only controlled by G-codes, this provides an interface to transmit codes from the web interface. This request returns the amount of currently available buffer space for incoming G-codes, however DWC does not actively use this response yet.
58 |
59 | #### rr_upload?name=XXX&time=YYY
60 | Upload a file to path XXX with the last modified date and time using an HTTP POST request. This is the only supported POST request in RepRapFirmware, however be aware that the POST request is no standard HTTP request. To make this work in the firmware, the payload (ie. file) has to be send in one chunk right after the HTTP header without any encapsulation. This mechanism is used to speed up transfers. Once complete, the firmware responds with {"err":[code]}. If everything goes well, the error code will be 0 and 1 on failure.
61 |
62 | #### rr_download?name=XXX
63 | Download a specified file from the SD card.
64 |
65 | #### rr_delete?name=XXX
66 | Delete a file from the SD card. The firmware responds again with `{"err":[code]}` and the error code will be 0 on success.
67 |
68 | #### rr_filelist?dir=XXX
69 | Request a file list from the directory XXX. Unlike rr_files, which was used in past web interface versions, this request returns a JSON object which encapsulates each file in the following format:
70 |
71 | `{"type":[type],"name":"[name]","size":[size],"lastModified":"[datetime]"}`
72 |
73 | Type can be either 'd' if it is a directory or 'f' if it is a regular file. The size is reported in bytes.
74 |
75 | If an error occurs, the firmware will respond with `{"err":[code]}`. If the code is 1, the directory doesn't exist. If it is 2, the requested volume is not mounted.
76 |
77 | #### rr_fileinfo?name=XXX
78 | Parse G-code file information from file XXX or return file information about the file being printed if the key is omitted. RepRapFirmware implements a dedicate function to retrieve information from a G-code file (see also M36) which may be used on the G-code file list and on the print status page.
79 |
80 | #### rr_move?old=XXX&new=YYY
81 | Move a file on the SD card from XXX to YYY. Returns {"err":[code]} after completion where code will be 0 if the request was successful.
82 |
83 | #### rr_mkdir?dir=XXX
84 | Create a new directory. Returns {"err":[code]} with code being 0 if the directory could be created.
85 |
86 | #### rr_config
87 | Get the configuration response. Some printer information do not need to be requested for regular usage but to obtain machine properties and firmware versions this request can be used.
88 |
89 | ## Building Duet Web Control
90 |
91 | The final file structure of a Duet Web Control package may differ from the structure in the "core" directory. For example, the Duet WiFi has a filename length limit of 32 characters, so the existing paths must be adjusted to meet this limitation. Apart from that, it may be required to compress the target files for webservers that cannot send source files in parallel. In addition, web files on the Duet are not stored in sub-directories, so the paths must be changed for this board as well.
92 |
93 | For these purposes a build script has been introduced which can be run on Linux (and possibly OS X). To do so, open a terminal in the DWC root directory and run `./build.sh`. Refer to the build script header to see which other tools you will need.
94 |
95 | Once the script has completed, you should get two files:
96 |
97 | - DuetWebControl-$VERSION.bin (SPIFFS image for Duet WiFi)
98 | - DuetWebControl-$VERSION.zip (ZIP package for first-generation Duets)
99 |
100 | These packages can be uploaded via Duet Web Control to update the web interface. Due to the extra compression on the Duet WiFi, it is recommended to test new features on first-generation Duets first.
101 |
102 | ## Internationalization
103 |
104 | Duet Web Control is capable of translating basically every text to a custom language. The translated entries are stored in an extra (and yet optional) XML file called "language.xml". Each language has its own section and if you want to add support for your own language, just follow the following tasks:
105 |
106 | 1. Copy the first section containing the German translations, i.e. the whole text between and and paste it before the last line of the file. It is explicitly recommended to use this section, because it will be up-to-date on every official release.
107 | 2. Change "de" to your own country code and replace "Deutsch" with your own language.
108 | 3. Replace the content of each "string" tag with your own translated text. Dynamic arguments may be specified in curly braces as in "Uploading File(s), {0}% Complete".
109 |
110 | If your language is supported, but you are missing entries for your own language, you can easily extend the existing translations. The list of translations is sequential, so you can always compare your own language section with the "de" language section. To extend them, check the length of your own language section, copy the missing entries from the "de" tag to your own language section and update the missing translations. In case some texts are not covered by the German translations, you can always create your own `...` tags, too.
111 |
112 | When you are done and would like to contribute your changes, feel free to send a pull request on GitHub or send me your updated language.xml file via e-mail.
113 |
--------------------------------------------------------------------------------
/core/css/slate.css:
--------------------------------------------------------------------------------
1 | /* Duet Web Control dark theme (v1.2) created by Tomas P. */
2 | @font-face {
3 | /* This is a Google font by Constanza Artigas Preller and Agustina Mingote
4 | * which is licensed under the terms of the SIL OFL.
5 | * See https://github.com/google/fonts/tree/master/ofl/homenaje */
6 | font-family: Homenaje-Regular;
7 | src: url('../fonts/Homenaje-Regular.ttf') format('truetype');
8 | }
9 |
10 | /*Colors:
11 | Menu background light: #555555
12 | Menu background dark: #444444
13 |
14 | Menu text light: #ececec
15 | Menu text dark: #aaaa93
16 |
17 | Medium gray data area: #2a2a2a
18 | Background black: # 1c1c1c
19 |
20 | Bar / Graph dark: #226699
21 | Bar / Graph light: #93a8c3
22 |
23 | Text: #f0f0f0
24 | Text gray: #828282
25 |
26 | Green: #63b03c
27 | Red: #fe4d4d
28 |
29 | */
30 |
31 | body {
32 | font-family: Homenaje-Regular, Arial, sans-serif;
33 | background-color: #222222;
34 | color: #f0f0f0;
35 | font-size: 18px;
36 | }
37 |
38 | #main_content {
39 | background-color: #222222;
40 | }
41 |
42 | a:focus, a:hover {
43 | color: #0fa4f4;
44 | }
45 | #div_info_panels {
46 | background-color: #1c1c1c;
47 | padding-top: 12px;
48 | }
49 |
50 | .panel-default > .panel-heading {
51 | color: #f0f0f0;
52 | background-color: #353535;
53 | background-image: none;
54 | }
55 |
56 | .panel, .panel-info > .panel-heading, .table-bordered > tbody > tr > td, .table-bordered > tbody > tr > th, .table-bordered > tfoot > tr > td, .table-bordered > tfoot > tr > th, .table-bordered > thead > tr > td, .table-bordered > thead > tr > th {
57 | color: #828282;
58 | font-size: 16px;
59 | background-color: #353535;
60 | background-image: none;
61 | border-color: #444444 !important;
62 | border-top-color: #828282 !important;
63 | }
64 |
65 | .alert-warning {
66 | color: #f0f0f0;
67 | background-color: #fe4d4d;
68 | border-color: #353535;
69 | }
70 | .table-striped>tbody>tr:nth-of-type(odd)>td {
71 | background-color: #555555;
72 | }
73 |
74 |
75 | #ul_active_temps>li:nth-of-type(odd),#ul_bed_temps>li:nth-of-type(odd),#ul_standby_temps>li:nth-of-type(odd)
76 | {
77 | background-color: #555555;
78 | color:#888888;
79 | }
80 | #ul_active_temps>li:nth-of-type(even),#ul_bed_temps>li:nth-of-type(even),#ul_standby_temps>li:nth-of-type(even)
81 | {
82 | background-color: #353535;
83 | }
84 | #page_machine > div.col-xs-12.col-sm-12.col-md-9.col-lg-10 > div > table{
85 | color:#0fa4f4;
86 | }
87 |
88 | #page_machine > div.col-xs-12.col-sm-12.col-md-9.col-lg-10 > div > table > tbody > tr:nth-child(1)
89 | {
90 | color:#828282;
91 | }
92 |
93 | .list-group-item {
94 | border-color: #666666;
95 | }
96 |
97 | .table > tbody > tr > td
98 | {
99 | border-top-color: #555555;
100 | }
101 |
102 | /*Navigation bar (top)----------------------------------------*/
103 | .navbar-default {
104 | background-image: none;
105 | background-color: #555555;
106 | border: #555555;
107 | }
108 |
109 | /* Navigation menu (left) ----------------------------------*/
110 | .sidebar {
111 | background-color: #555555;
112 | border-right: 1px solid transparent;
113 | }
114 |
115 | .nav > li > a:hover {
116 | text-decoration: none;
117 | background-color: #828282;
118 | color: #f0f0f0;
119 | }
120 |
121 | .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover {
122 | background-color: #ed8d29 !important;
123 | }
124 |
125 | .nav-sidebar > .active > a, .nav-sidebar > .active > a:hover, .nav-sidebar > .active > a:focus {
126 | color: #fff;
127 | background-color: #226699;
128 | }
129 |
130 | /* Navigation menu (mobile) --------------------------------*/
131 | .slideout-menu {
132 | background-color: #555555;
133 | border-right: 1px solid transparent;
134 | }
135 |
136 | /*G-Code File list------------------------------------------*/
137 | .breadcrumb, .breadcrumb>.active{
138 | background-color: #555555;
139 | color: #f0f0f0;
140 | }
141 |
142 | .table-file-navigation button {
143 | font-size: 18px !important;
144 | }
145 | .table-files tr.info > td {
146 | background-color: #A0A0A0 !important;
147 | }
148 |
149 | /*Layer statistics value ------------------------------------------*/
150 | #span_progress_right{
151 | color:#0fa4f4;
152 |
153 | }
154 | .chart-print-line {
155 | color: #0fa4f4;
156 | }
157 |
158 | #panel_print_info > table > tbody > tr:nth-child(2) > td {
159 | color: #0fa4f4;
160 | }
161 |
162 | /*Estimations values ------------------------------------------*/
163 | #table_estimations > tbody > tr:nth-child(2) > td,
164 | #table_estimations > tbody > tr:nth-child(3) > td {
165 | color: #0fa4f4; /*#d0d0d0;*/
166 | }
167 |
168 | .panel-default > .panel-heading, .panel-heading, {
169 | color: #d0d0d0;
170 | }
171 |
172 | .panel-info > .panel-heading {
173 | color: #e5e5e5;
174 | }
175 |
176 | /*General values*/
177 | * {
178 | -webkit-border-radius: 0 !important;
179 | -moz-border-radius: 0 !important;
180 | border-radius: 0 !important;
181 | }
182 |
183 | a {
184 | color: #e5e5e5;
185 | text-decoration: none;
186 | }
187 |
188 | .form-control {
189 | background-color: orange;
190 | color: #0fa4f4;
191 | font-size: 14px !important;
192 | }
193 |
194 | .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control {
195 | cursor: not-allowed;
196 | background-color: #353535;
197 | opacity: 1;
198 | }
199 |
200 | .form-control, .form-control, fieldset.form-control {
201 | background-color: #e5e5e5;
202 | opacity: 1;
203 | border: 1px solid #555555;
204 | }
205 |
206 |
207 |
208 | /* Dropdown menu -------------------------------------------------------*/
209 | .dropdown-menu, .popover {
210 | background-color: #555;
211 | }
212 |
213 | .dropdown-menu > li > a {
214 | display: block;
215 | padding: 3px 20px;
216 | clear: both;
217 | font-weight: 400;
218 | line-height: 1.42857143;
219 | color: #e5e5e5;
220 | white-space: nowrap;
221 | background-image: none;
222 | }
223 |
224 | .dropdown-menu > li > a:focus, .dropdown-menu > li > a:hover {
225 | background-image: none;
226 | background-color: #828282;
227 | color: #f0f0f0;
228 | }
229 |
230 | .popover {
231 | border: 1px solid;
232 | }
233 |
234 | /*Buttons-------------------------------------------------------*/
235 | .btn-default {
236 | color: #828282;
237 | background-image: none;
238 | background-color: #444444;
239 | border-color: #555555;
240 | text-shadow: none;
241 | }
242 |
243 | .btn-default:focus, .btn-default:hover {
244 | background-color: #828282;
245 | background-position: 0 -15px;
246 | }
247 |
248 | .btn-primary, .btn-primary.disabled, .btn-info, .btn-info.disabled {
249 | background-color: #226699;
250 | background-image: none;
251 | color: white;
252 | }
253 |
254 | .btn-primary:focus, .btn-primary:hover {
255 | background-color: #828282;
256 | background-position: 0 -15px;
257 | }
258 |
259 | .btn-danger, .btn-danger.disabled, .btn-danger:disabled, .btn-danger[disabled] {
260 | background-color: #fe4d4d;
261 | background-image: none;
262 | color: white;
263 | }
264 |
265 | .btn-success, .btn-success.disabled, .btn-success:disabled, .btn-success[disabled] {
266 | background-color: #63b03c;
267 | background-image: none;
268 | color: white;
269 | }
270 |
271 | .btn-warning, .btn-warning.disabled, .btn-warning:disabled, .btn-warning[disabled] {
272 | background-color: #eb9316;
273 | background-image: none;
274 | color: white;
275 | }
276 |
277 | .btn-warning:focus, .btn-warning:hover {
278 | background-color: #828282;
279 | background-position: 0 -15px;
280 | }
281 |
282 | /* G-Code console rows-------------------------------------------------------*/
283 | .alert-success, .alert-info, .alert-warning, .alert-danger {
284 | background-image: none;
285 | }
286 |
287 | /* Modal dialog -------------------------------------------------------*/
288 | .modal-content {
289 | background-color: #828282;
290 | }
291 |
292 | /* Macros-------------------------------------------------------*/
293 | #panel_macro_buttons > div.panel-body.text-center > div > div > button {
294 | color: #0fa4f4;
295 | }
296 |
297 | /* Heater Temperatures-------------------------------------------------------*/
298 | .heater-0 {
299 | color: #3F99DC !important; /*Bed color*/
300 | }
301 |
302 | .heater-1 {
303 | color: #fe4d4d !important; /*Heater 1 color*/
304 | }
305 |
306 | .heater-2 {
307 | color: #63b03c !important; /*Heater 2 color*/
308 | }
309 |
310 | .heater-3 {
311 | color: #E040E0 !important; /*Heater 3 color*/
312 | }
313 |
314 | .heater-4 {
315 | color: #8A46F5 !important; /*Heater 4 color*/
316 | }
317 |
318 | .heater-5 {
319 | color: #30DCDC !important; /*Heater 5 color*/
320 | }
321 |
322 | #table_tools td,
323 | #table_heaters td,
324 | #table_extra td {
325 | color: #0fa4f4; /*Value text color*/
326 | }
327 |
328 | #table_tools a,
329 | #table_heaters a {
330 | font-size: 18px; /*Name and value font size*/
331 | }
332 |
333 | span.text-muted {
334 | font-size: 14px !important;
335 | }
336 |
337 | .text-muted {
338 | font-weight: lighter;
339 | color:#cccccc;
340 | }
341 |
342 | /* Machine status-------------------------------------------------------*/
343 |
344 | #td_x, #td_y, #td_z, #td_u, #td_v, #td_w, #td_a, #td_b, #td_c,
345 | #td_extr_1, #td_extr_2, #td_extr_3, #td_extr_4, #td_extr_5, #td_extr_6 {
346 | color: #0fa4f4;
347 | }
348 |
349 | .probe-trigger {
350 | color: #fe4d4d;
351 | }
352 |
353 | .probe-slow-down {
354 | color: #63b03c;
355 | }
356 |
357 | /* Sliders -------------------------------------------------------*/
358 | .slider-selection {
359 | background-image: none;
360 | background-color: #226699;
361 | }
362 |
363 | .slider {
364 | background-image: none;
365 | /*background-color: #2a2a2a;*/
366 | }
367 |
368 | .slider .tooltip-inner {
369 | background-color: #1c1c1c;
370 | color: #0fa4f4;
371 | }
372 |
373 | .slider.slider-disabled .slider-handle {
374 | background-image: none;
375 | background-color: #828282;
376 | opacity: 1;
377 | box-shadow: inset 1px 1px #004b7d, 1px 1px #004b7d;
378 | }
379 |
380 | /* File information -------------------------------------------------------*/
381 | .panel-body > dl > dd {
382 | color: #0fa4f4;
383 | font-size: 14px;
384 | font-family: Homenaje-Regular;
385 | }
386 |
387 | /* Settings ------------------------------------------------------*/
388 | textarea {
389 | background-color: #e5e5e5;
390 | color: #0fa4f4;
391 | }
392 |
--------------------------------------------------------------------------------
/core/js/3rd-party/bootstrap-notify.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Project: Bootstrap Notify = v3.1.3
3 | * Description: Turns standard Bootstrap alerts into "Growl-like" notifications.
4 | * Author: Mouse0270 aka Robert McIntosh
5 | * License: MIT License
6 | * Website: https://github.com/mouse0270/bootstrap-growl
7 | */
8 | (function (factory) {
9 | if (typeof define === 'function' && define.amd) {
10 | // AMD. Register as an anonymous module.
11 | define(['jquery'], factory);
12 | } else if (typeof exports === 'object') {
13 | // Node/CommonJS
14 | factory(require('jquery'));
15 | } else {
16 | // Browser globals
17 | factory(jQuery);
18 | }
19 | }(function ($) {
20 | // Create the defaults once
21 | var defaults = {
22 | element: 'body',
23 | position: null,
24 | type: "info",
25 | allow_dismiss: true,
26 | newest_on_top: false,
27 | showProgressbar: false,
28 | placement: {
29 | from: "top",
30 | align: "right"
31 | },
32 | offset: 20,
33 | spacing: 10,
34 | z_index: 1031,
35 | delay: 5000,
36 | timer: 1000,
37 | url_target: '_blank',
38 | mouse_over: null,
39 | animate: {
40 | enter: 'animated fadeInDown',
41 | exit: 'animated fadeOutUp'
42 | },
43 | onShow: null,
44 | onShown: null,
45 | onClose: null,
46 | onClosed: null,
47 | icon_type: 'class',
48 | template: ''
49 | };
50 |
51 | String.format = function() {
52 | var str = arguments[0];
53 | for (var i = 1; i < arguments.length; i++) {
54 | str = str.replace(RegExp("\\{" + (i - 1) + "\\}", "gm"), arguments[i]);
55 | }
56 | return str;
57 | };
58 |
59 | function Notify ( element, content, options ) {
60 | // Setup Content of Notify
61 | var content = {
62 | content: {
63 | message: typeof content == 'object' ? content.message : content,
64 | title: content.title ? content.title : '',
65 | icon: content.icon ? content.icon : '',
66 | url: content.url ? content.url : '#',
67 | target: content.target ? content.target : '-'
68 | }
69 | };
70 |
71 | options = $.extend(true, {}, content, options);
72 | this.settings = $.extend(true, {}, defaults, options);
73 | this._defaults = defaults;
74 | if (this.settings.content.target == "-") {
75 | this.settings.content.target = this.settings.url_target;
76 | }
77 | this.animations = {
78 | start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart',
79 | end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend'
80 | }
81 |
82 | if (typeof this.settings.offset == 'number') {
83 | this.settings.offset = {
84 | x: this.settings.offset,
85 | y: this.settings.offset
86 | };
87 | }
88 |
89 | this.init();
90 | };
91 |
92 | $.extend(Notify.prototype, {
93 | init: function () {
94 | var self = this;
95 |
96 | this.buildNotify();
97 | if (this.settings.content.icon) {
98 | this.setIcon();
99 | }
100 | if (this.settings.content.url != "#") {
101 | this.styleURL();
102 | }
103 | this.placement();
104 | this.bind();
105 |
106 | this.notify = {
107 | $ele: this.$ele,
108 | update: function(command, update) {
109 | var commands = {};
110 | if (typeof command == "string") {
111 | commands[command] = update;
112 | }else{
113 | commands = command;
114 | }
115 | for (var command in commands) {
116 | switch (command) {
117 | case "type":
118 | this.$ele.removeClass('alert-' + self.settings.type);
119 | this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type);
120 | self.settings.type = commands[command];
121 | this.$ele.addClass('alert-' + commands[command]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[command]);
122 | break;
123 | case "icon":
124 | var $icon = this.$ele.find('[data-notify="icon"]');
125 | if (self.settings.icon_type.toLowerCase() == 'class') {
126 | $icon.removeClass(self.settings.content.icon).addClass(commands[command]);
127 | }else{
128 | if (!$icon.is('img')) {
129 | $icon.find('img');
130 | }
131 | $icon.attr('src', commands[command]);
132 | }
133 | break;
134 | case "progress":
135 | var newDelay = self.settings.delay - (self.settings.delay * (commands[command] / 100));
136 | this.$ele.data('notify-delay', newDelay);
137 | this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[command]).css('width', commands[command] + '%');
138 | break;
139 | case "url":
140 | this.$ele.find('[data-notify="url"]').attr('href', commands[command]);
141 | break;
142 | case "target":
143 | this.$ele.find('[data-notify="url"]').attr('target', commands[command]);
144 | break;
145 | default:
146 | this.$ele.find('[data-notify="' + command +'"]').html(commands[command]);
147 | };
148 | }
149 | var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y);
150 | self.reposition(posX);
151 | },
152 | close: function() {
153 | self.close();
154 | }
155 | };
156 | },
157 | buildNotify: function () {
158 | var content = this.settings.content;
159 | this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target));
160 | this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align);
161 | if (!this.settings.allow_dismiss) {
162 | this.$ele.find('[data-notify="dismiss"]').css('display', 'none');
163 | }
164 | if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) {
165 | this.$ele.find('[data-notify="progressbar"]').remove();
166 | }
167 | },
168 | setIcon: function() {
169 | if (this.settings.icon_type.toLowerCase() == 'class') {
170 | this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon);
171 | }else{
172 | if (this.$ele.find('[data-notify="icon"]').is('img')) {
173 | this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon);
174 | }else{
175 | this.$ele.find('[data-notify="icon"]').append('
');
176 | }
177 | }
178 | },
179 | styleURL: function() {
180 | this.$ele.find('[data-notify="url"]').css({
181 | backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)',
182 | height: '100%',
183 | left: '0px',
184 | position: 'absolute',
185 | top: '0px',
186 | width: '100%',
187 | zIndex: this.settings.z_index + 1
188 | });
189 | this.$ele.find('[data-notify="dismiss"]').css({
190 | position: 'absolute',
191 | right: '10px',
192 | top: '5px',
193 | zIndex: this.settings.z_index + 2
194 | });
195 | },
196 | placement: function() {
197 | var self = this,
198 | offsetAmt = this.settings.offset.y,
199 | css = {
200 | display: 'inline-block',
201 | margin: '0px auto',
202 | position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'),
203 | transition: 'all .5s ease-in-out',
204 | zIndex: this.settings.z_index
205 | },
206 | hasAnimation = false,
207 | settings = this.settings;
208 |
209 | $('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function() {
210 | return offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing));
211 | });
212 | if (this.settings.newest_on_top == true) {
213 | offsetAmt = this.settings.offset.y;
214 | }
215 | css[this.settings.placement.from] = offsetAmt+'px';
216 |
217 | switch (this.settings.placement.align) {
218 | case "left":
219 | case "right":
220 | css[this.settings.placement.align] = this.settings.offset.x+'px';
221 | break;
222 | case "center":
223 | css.left = 0;
224 | css.right = 0;
225 | break;
226 | }
227 | this.$ele.css(css).addClass(this.settings.animate.enter);
228 | $.each(Array('webkit', 'moz', 'o', 'ms', ''), function(index, prefix) {
229 | self.$ele[0].style[prefix+'AnimationIterationCount'] = 1;
230 | });
231 |
232 | $(this.settings.element).append(this.$ele);
233 |
234 | if (this.settings.newest_on_top == true) {
235 | offsetAmt = (parseInt(offsetAmt)+parseInt(this.settings.spacing)) + this.$ele.outerHeight();
236 | this.reposition(offsetAmt);
237 | }
238 |
239 | if ($.isFunction(self.settings.onShow)) {
240 | self.settings.onShow.call(this.$ele);
241 | }
242 |
243 | this.$ele.one(this.animations.start, function(event) {
244 | hasAnimation = true;
245 | }).one(this.animations.end, function(event) {
246 | if ($.isFunction(self.settings.onShown)) {
247 | self.settings.onShown.call(this);
248 | }
249 | });
250 |
251 | setTimeout(function() {
252 | if (!hasAnimation) {
253 | if ($.isFunction(self.settings.onShown)) {
254 | self.settings.onShown.call(this);
255 | }
256 | }
257 | }, 600);
258 | },
259 | bind: function() {
260 | var self = this;
261 |
262 | this.$ele.find('[data-notify="dismiss"]').on('click', function() {
263 | self.close();
264 | })
265 |
266 | this.$ele.mouseover(function(e) {
267 | $(this).data('data-hover', "true");
268 | }).mouseout(function(e) {
269 | $(this).data('data-hover', "false");
270 | });
271 | this.$ele.data('data-hover', "false");
272 |
273 | if (this.settings.delay > 0) {
274 | self.$ele.data('notify-delay', self.settings.delay);
275 | var timer = setInterval(function() {
276 | var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer;
277 | if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over == "pause") || self.settings.mouse_over != "pause") {
278 | var percent = ((self.settings.delay - delay) / self.settings.delay) * 100;
279 | self.$ele.data('notify-delay', delay);
280 | self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%');
281 | }
282 | if (delay <= -(self.settings.timer)) {
283 | clearInterval(timer);
284 | self.close();
285 | }
286 | }, self.settings.timer);
287 | }
288 | },
289 | close: function() {
290 | var self = this,
291 | $successors = null,
292 | posX = parseInt(this.$ele.css(this.settings.placement.from)),
293 | hasAnimation = false;
294 |
295 | this.$ele.data('closing', 'true').addClass(this.settings.animate.exit);
296 | self.reposition(posX);
297 |
298 | if ($.isFunction(self.settings.onClose)) {
299 | self.settings.onClose.call(this.$ele);
300 | }
301 |
302 | this.$ele.one(this.animations.start, function(event) {
303 | hasAnimation = true;
304 | }).one(this.animations.end, function(event) {
305 | $(this).remove();
306 | if ($.isFunction(self.settings.onClosed)) {
307 | self.settings.onClosed.call(this);
308 | }
309 | });
310 |
311 | setTimeout(function() {
312 | if (!hasAnimation) {
313 | self.$ele.remove();
314 | if (self.settings.onClosed) {
315 | self.settings.onClosed(self.$ele);
316 | }
317 | }
318 | }, 600);
319 | },
320 | reposition: function(posX) {
321 | var self = this,
322 | notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])',
323 | $elements = this.$ele.nextAll(notifies);
324 | if (this.settings.newest_on_top == true) {
325 | $elements = this.$ele.prevAll(notifies);
326 | }
327 | $elements.each(function() {
328 | $(this).css(self.settings.placement.from, posX);
329 | posX = (parseInt(posX)+parseInt(self.settings.spacing)) + $(this).outerHeight();
330 | });
331 | }
332 | });
333 |
334 | $.notify = function ( content, options ) {
335 | var plugin = new Notify( this, content, options );
336 | return plugin.notify;
337 | };
338 | $.notifyDefaults = function( options ) {
339 | defaults = $.extend(true, {}, defaults, options);
340 | return defaults;
341 | };
342 | $.notifyClose = function( command ) {
343 | if (typeof command === "undefined" || command == "all") {
344 | $('[data-notify]').find('[data-notify="dismiss"]').trigger('click');
345 | }else{
346 | $('[data-notify-position="'+command+'"]').find('[data-notify="dismiss"]').trigger('click');
347 | }
348 | };
349 |
350 | }));
--------------------------------------------------------------------------------
/core/js/modals.js:
--------------------------------------------------------------------------------
1 | /* Modal dialog functions for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | /* Generic workarounds */
11 |
12 | $(".modal").on("hidden.bs.modal", function() {
13 | // Bootstrap bug: Padding is added to the right, but never cleaned
14 | $("body").css("padding-right", "");
15 | });
16 |
17 |
18 | /* Confirmation Dialog */
19 |
20 | function showConfirmationDialog(title, message, callback) {
21 | $("#modal_confirmation h4").html(' ' + title);
22 | $("#modal_confirmation p").html(message);
23 | $("#modal_confirmation button.btn-success").off().one("click", callback);
24 | $("#modal_confirmation").modal("show");
25 | $("#modal_confirmation .btn-success").focus();
26 | }
27 |
28 |
29 | /* Text Input Dialog */
30 |
31 | function showTextInput(title, message, callback, text, emptyCallback) {
32 | $("#modal_textinput h4").html(title);
33 | $("#modal_textinput p").html(message);
34 | $("#modal_textinput input").val((text == undefined) ? "" : text);
35 | $("#modal_textinput form").off().submit(function(e) {
36 | $("#modal_textinput").modal("hide");
37 | var value = $("#modal_textinput input").val();
38 | if (value.trim() != "") {
39 | callback(value);
40 | } else if (emptyCallback != undefined) {
41 | emptyCallback();
42 | }
43 | e.preventDefault();
44 | });
45 | $("#modal_textinput").modal("show");
46 | }
47 |
48 | $("#modal_textinput").on("shown.bs.modal", function() {
49 | $("#modal_textinput input").focus();
50 | });
51 |
52 |
53 | /* Host Prompt */
54 |
55 | function showHostPrompt() {
56 | if (settings.hasOwnProperty("lastHost")) {
57 | $('#input_host').val(settings.lastHost);
58 | }
59 | $("#modal_host_input").modal("show");
60 | }
61 |
62 | $("#modal_host_input").on("shown.bs.modal", function() {
63 | $("#input_host").focus();
64 | });
65 |
66 | $("#form_host").submit(function(e) {
67 | $("#modal_host_input").off("hide.bs.modal").modal("hide");
68 | ajaxPrefix = settings.lastHost = $("#input_host").val(); // let the user decide if this shall be saved
69 |
70 | ajaxPrefix += "/";
71 | if (ajaxPrefix.indexOf("://") == -1) {
72 | // Prepend http prefix if no URI scheme is given
73 | ajaxPrefix = "http://" + ajaxPrefix;
74 | }
75 |
76 | connect(sessionPassword, true);
77 | e.preventDefault();
78 | });
79 |
80 |
81 | /* Password prompt */
82 |
83 | function showPasswordPrompt() {
84 | $('#input_password').val("");
85 | $("#modal_pass_input").modal("show");
86 | $("#modal_pass_input").one("hide.bs.modal", function() {
87 | // The network request will take a few ms anyway, so no matter if the user has
88 | // cancelled the password input, we can reset the Connect button here...
89 | $(".btn-connect").removeClass("btn-warning disabled").addClass("btn-info").find("span:not(.glyphicon)").text(T("Connect"));
90 | });
91 | }
92 |
93 | $("#form_password").submit(function(e) {
94 | $("#modal_pass_input").off("hide.bs.modal").modal("hide");
95 | connect($("#input_password").val(), false);
96 | e.preventDefault();
97 | });
98 |
99 | $("#modal_pass_input").on("shown.bs.modal", function() {
100 | $("#input_password").focus();
101 | });
102 |
103 |
104 | /* Filament Change Dialog */
105 |
106 | var filamentChangeTool, changingFilament;
107 |
108 | function showFilamentDialog(tool, changeFilament) {
109 | // make list of all available filaments
110 | $("#div_filaments").children().remove();
111 | $("#table_filaments > tbody > tr").each(function() {
112 | var filament = $(this).data("filament");
113 | var isLoaded = false;
114 | for(var i = 0; i < toolMapping.length; i++) {
115 | if (toolMapping[i].hasOwnProperty("filament") && toolMapping[i].filament == filament) {
116 | isLoaded = true;
117 | break;
118 | }
119 | }
120 |
121 | if (!isLoaded) {
122 | $("#div_filaments").append(' ' + filament + '');
123 | }
124 | });
125 |
126 | // show notification or selection dialog
127 | if ($("#div_filaments").children().length == 0) {
128 | showMessage("warning", T("No Filaments"), T("There are no other filaments available to choose. Please go to the Filaments page and define more."));
129 | } else {
130 | filamentChangeTool = tool;
131 | changingFilament = changeFilament;
132 | $("#modal_change_filament").modal("show");
133 | }
134 | }
135 |
136 | $("body").on("click", ".a-load-filament", function(e) {
137 | $("#modal_change_filament").modal("hide");
138 |
139 | var gcode = "";
140 | if (lastStatusResponse != undefined && lastStatusResponse.currentTool != filamentChangeTool) {
141 | gcode = "T" + filamentChangeTool + "\n";
142 | }
143 | if (changingFilament) {
144 | gcode += "M702\n";
145 | }
146 | gcode += "M701 S\"" + $(this).data("filament") + "\"";
147 | sendGCode(gcode);
148 |
149 | e.preventDefault();
150 | });
151 |
152 |
153 | /* File Edit Dialog */
154 |
155 | function showEditDialog(title, content, callback) {
156 | $("#modal_edit .modal-title").text(T("Editing {0}", title));
157 | var edit = $("#modal_edit textarea").val(content).get(0);
158 | edit.selectionStart = edit.selectionEnd = 0;
159 | $("#modal_edit").modal("show");
160 | $("#btn_save_file").off("click").click(function() {
161 | $("#modal_edit").modal("hide");
162 | callback($("#modal_edit textarea").val());
163 | });
164 | }
165 |
166 | $("#modal_edit").on("shown.bs.modal", function() {
167 | $("#modal_edit textarea").focus();
168 | });
169 |
170 | $("#modal_edit div.modal-content").resize(function() {
171 | var contentHeight = $(this).height();
172 | var headerHeight = $("#modal_edit div.modal-header").height();
173 | var footerHeight = $("#modal_edit div.modal-footer").height();
174 | $("#modal_edit div.modal-body").css("height", (contentHeight - headerHeight - footerHeight - 60) + "px");
175 | });
176 |
177 | $(document).delegate("#modal_edit textarea", "keydown", function(e) {
178 | var keyCode = e.keyCode || e.which;
179 |
180 | if (keyCode == 9) {
181 | e.preventDefault();
182 | var start = $(this).get(0).selectionStart;
183 | var end = $(this).get(0).selectionEnd;
184 |
185 | // set textarea value to: text before caret + tab + text after caret
186 | $(this).val($(this).val().substring(0, start)
187 | + "\t"
188 | + $(this).val().substring(end));
189 |
190 | // put caret at right position again
191 | $(this).get(0).selectionStart = $(this).get(0).selectionEnd = start + 1;
192 | }
193 | });
194 |
195 |
196 | /* Start Scan Dialog (proprietary) */
197 |
198 | $("#modal_start_scan input").keyup(function() {
199 | $("#btn_start_scan_modal").toggleClass("disabled", $("#modal_start_scan input:invalid").length > 0);
200 | });
201 |
202 | $("#btn_toggle_laser").click(function(e) {
203 | if ($(this).hasClass("active")) {
204 | sendGCode("M755 P0");
205 | $(this).removeClass("active").children("span.content").text(T("Activate Laser"));
206 | } else {
207 | sendGCode("M755 P1")
208 | $(this).addClass("active").children("span.content").text(T("Deactivate Laser"));
209 | }
210 |
211 | $(this).blur();
212 | e.preventDefault();
213 | });
214 |
215 | $("#modal_start_scan form").submit(function(e) {
216 | if (!$("#btn_start_scan_modal").hasClass("disabled")) {
217 | // 1. Turn off the alignment laser if it is still active
218 | if ($("#btn_toggle_laser").hasClass("active")) {
219 | $("#btn_toggle_laser").removeClass("active").children("span.content").text(T("Activate Laser"));
220 | if (isConnected) {
221 | sendGCode("M755 P0");
222 | }
223 | }
224 |
225 | // 2. Start a new scan
226 | var filename = $("#input_scan_filename").val();
227 | var length = $("#input_scan_length").val();
228 |
229 | // 3. Check the filename and start a new scan
230 | if (filenameValid(filename)) {
231 | sendGCode("M752 S" + length + " P" + filename);
232 | } else {
233 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes."));
234 | }
235 |
236 | // 4. Hide the modal dialog
237 | $("#modal_start_scan").modal("hide");
238 | }
239 |
240 | e.preventDefault();
241 | });
242 |
243 | $("#modal_start_scan").on("hidden.bs.modal", function() {
244 | if ($("#btn_toggle_laser").hasClass("active")) {
245 | $("#btn_toggle_laser").removeClass("active").children("span.content").text(T("Activate Laser"));
246 | if (isConnected) {
247 | // Turn off laser again when the dialog is closed
248 | sendGCode("M755 P0");
249 | }
250 | }
251 | });
252 |
253 |
254 | /* Scanner Progress Dialogs */
255 |
256 | function updateScannerDialogs(scanResponse) {
257 | var scanProgress = 100, postProcessingProgress = 100, uploadProgress = 100;
258 |
259 | if (scanResponse.status == "S" || scanResponse.status == "P" || scanResponse.status == "U") {
260 | // Scanner is active
261 | if (!$("#modal_scanner").hasClass("in")) {
262 | $("#btn_cancel_scan").removeClass("hidden").removeClass("disabled");
263 | $("#btn_close_scan").addClass("hidden");
264 |
265 | $("#modal_scanner .modal-title").text(T("Scanning..."));
266 | $("#p_scan_info").text(T("Please wait while a scan is being made. This may take a while..."));
267 |
268 | $("#modal_scanner").modal("show");
269 | }
270 |
271 | // Update progress
272 | if (scanResponse.status == "S") {
273 | scanProgress = scanResponse.progress;
274 | postProcessingProgress = 0;
275 | uploadProgress = 0;
276 | } else if (scanResponse.status == "P") {
277 | scanProgress = 100;
278 | postProcessingProgress = scanResponse.progress;
279 | uploadProgress = 0;
280 | } else if (scanResponse.status == "U") {
281 | scanProgress = 100;
282 | postProcessingProgress = 100;
283 | uploadProgress = scanResponse.progress;
284 | $("#btn_cancel_scan").addClass("disabled");
285 | }
286 | } else if (scanResponse.status == "C") {
287 | // Scanner calibration is running
288 | if (!$("#modal_scanner_calibration").hasClass("in")) {
289 | $("#btn_cancel_calibration").removeClass("disabled");
290 | $("#modal_scanner_calibration").modal("show");
291 | }
292 |
293 | // Update progress
294 | $("#progress_calibration").css("width", scanResponse.progress + "%");
295 | $("#span_calibration_progress").text(T("{0} %", scanResponse.progress));
296 | } else if (scanResponse.status == "I") {
297 | // Scanner is inactive
298 | if ($("#modal_scanner").hasClass("in")) {
299 | $("#modal_scanner .modal-title").text(T("Scan complete"));
300 | $("#p_scan_info").text(T("Your 3D scan is now complete! You may download it from the file list next."));
301 |
302 | $("#btn_cancel_scan").addClass("hidden");
303 | $("#btn_close_scan").removeClass("hidden");
304 | }
305 |
306 | if ($("#modal_scanner_calibration").hasClass("in")) {
307 | $("#modal_scanner_calibration").modal("hide");
308 | }
309 | }
310 |
311 | // Update progress bars
312 | if ($("#modal_scanner").hasClass("in")) {
313 | $("#progress_scan").css("width", scanProgress + "%");
314 | $("#span_scan_progress").text(T("{0} %", scanProgress));
315 |
316 | $("#progress_scan_postprocessing").css("width", postProcessingProgress + "%");
317 | $("#span_scan_postprocessing_progress").text(T("{0} %", postProcessingProgress));
318 |
319 | $("#progress_scan_upload").css("width", uploadProgress + "%");
320 | $("#span_scan_upload_progress").text(T("{0} %", uploadProgress));
321 | }
322 | }
323 |
324 | $("#btn_cancel_scan").click(function() {
325 | if (!$(this).hasClass("disabled")) {
326 | sendGCode("M753");
327 | $("#modal_scanner").modal("hide");
328 | }
329 | });
330 |
331 | $("#btn_cancel_calibration").click(function() {
332 | if (!$(this).hasClass("disabled")) {
333 | sendGCode("M753");
334 | $(this).addClass("disabled");
335 | }
336 | });
337 |
338 |
339 | /* Message Box Dialog */
340 |
341 | var messageBoxResponse = undefined;
342 |
343 | function updateMessageBox(response) {
344 | var timeout = response.timeout;
345 | response.timeout = 0;
346 |
347 | var stringifiedResponse = JSON.stringify(response);
348 | if (stringifiedResponse != messageBoxResponse)
349 | {
350 | messageBoxResponse = stringifiedResponse;
351 | showMessageBox(response.msg, response.title, response.mode, timeout, response.controls);
352 | }
353 | }
354 |
355 | function closeMessageBox() {
356 | if (messageBoxResponse != undefined) {
357 | if ($("#modal_messagebox").hasClass("in")) {
358 | $("#modal_messagebox").modal("hide");
359 | }
360 | messageBoxResponse = undefined;
361 | }
362 | }
363 |
364 |
365 | var messageBoxTimer = undefined;
366 |
367 | function showMessageBox(message, title, mode, timeout, controls) {
368 | // Display message, title and optionally show Z controls
369 | $("#h3_messagebox").html(message);
370 | $("#h4_messagebox_title").html(title);
371 | $("#modal_messagebox div.modal-header").toggleClass("hidden", title == "");
372 |
373 | // Toggle axis control visibility
374 | $("#div_x_controls").toggleClass("hidden", (controls & (1 << 0)) == 0);
375 | $("#div_y_controls").toggleClass("hidden", (controls & (1 << 1)) == 0);
376 | $("#div_z_controls").toggleClass("hidden", (controls & (1 << 2)) == 0);
377 |
378 | // Toggle button visibility
379 | $("#modal_messagebox div.modal-footer").toggleClass("hidden", mode == 0);
380 | $("#modal_messagebox [data-dismiss]").toggleClass("hidden", mode != 1);
381 | $("#btn_ack_messagebox").toggleClass("hidden", mode == 0 || mode == 1);
382 | $("#btn_cancel_messagebox").toggleClass("hidden", mode != 3);
383 |
384 | // Show message box
385 | var backdropValue = (mode != 1) ? "static" : "true";
386 | $("#modal_messagebox").modal({ backdrop: backdropValue });
387 |
388 | var data = $("#modal_messagebox").data("bs.modal");
389 | data.options.backdrop = backdropValue;
390 | $("#modal_messagebox").data("bs.modal", data);
391 |
392 | // Take care of the timeouts
393 | if (messageBoxTimer != undefined) {
394 | clearTimeout(messageBoxTimer);
395 | }
396 | if (timeout > 0) {
397 | messageBoxTimer = setTimeout(function() {
398 | messageBoxTimer = undefined;
399 | $("#modal_messagebox").modal("hide");
400 | }, timeout * 1000);
401 | }
402 | }
403 |
404 | $("#btn_ack_messagebox").click(function() {
405 | $("#modal_messagebox").modal("hide");
406 | sendGCode("M292");
407 | });
408 |
409 | $("#btn_cancel_messagebox").click(function() {
410 | $("#modal_messagebox").modal("hide");
411 | sendGCode("M292 P1");
412 | });
413 |
414 | $('#modal_messagebox').on("hide.bs.modal", function() {
415 | if (messageBoxTimer != undefined) {
416 | clearTimeout(messageBoxTimer);
417 | messageBoxTimer = undefined;
418 | }
419 |
420 | // FIXME: This is needed to ensure the backdrop always works as intended
421 | $('#modal_messagebox').removeData();
422 | });
423 |
424 |
--------------------------------------------------------------------------------
/core/js/3rd-party/jquery.flot.navigate.js:
--------------------------------------------------------------------------------
1 | /* Flot plugin for adding the ability to pan and zoom the plot.
2 |
3 | Copyright (c) 2007-2014 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | The default behaviour is double click and scrollwheel up/down to zoom in, drag
7 | to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and
8 | plot.pan( offset ) so you easily can add custom controls. It also fires
9 | "plotpan" and "plotzoom" events, useful for synchronizing plots.
10 |
11 | The plugin supports these options:
12 |
13 | zoom: {
14 | interactive: false
15 | trigger: "dblclick" // or "click" for single click
16 | amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
17 | }
18 |
19 | pan: {
20 | interactive: false
21 | cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer"
22 | frameRate: 20
23 | }
24 |
25 | xaxis, yaxis, x2axis, y2axis: {
26 | zoomRange: null // or [ number, number ] (min range, max range) or false
27 | panRange: null // or [ number, number ] (min, max) or false
28 | }
29 |
30 | "interactive" enables the built-in drag/click behaviour. If you enable
31 | interactive for pan, then you'll have a basic plot that supports moving
32 | around; the same for zoom.
33 |
34 | "amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to
35 | the current viewport.
36 |
37 | "cursor" is a standard CSS mouse cursor string used for visual feedback to the
38 | user when dragging.
39 |
40 | "frameRate" specifies the maximum number of times per second the plot will
41 | update itself while the user is panning around on it (set to null to disable
42 | intermediate pans, the plot will then not update until the mouse button is
43 | released).
44 |
45 | "zoomRange" is the interval in which zooming can happen, e.g. with zoomRange:
46 | [1, 100] the zoom will never scale the axis so that the difference between min
47 | and max is smaller than 1 or larger than 100. You can set either end to null
48 | to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis
49 | will be disabled.
50 |
51 | "panRange" confines the panning to stay within a range, e.g. with panRange:
52 | [-10, 20] panning stops at -10 in one end and at 20 in the other. Either can
53 | be null, e.g. [-10, null]. If you set panRange to false, panning on that axis
54 | will be disabled.
55 |
56 | Example API usage:
57 |
58 | plot = $.plot(...);
59 |
60 | // zoom default amount in on the pixel ( 10, 20 )
61 | plot.zoom({ center: { left: 10, top: 20 } });
62 |
63 | // zoom out again
64 | plot.zoomOut({ center: { left: 10, top: 20 } });
65 |
66 | // zoom 200% in on the pixel (10, 20)
67 | plot.zoom({ amount: 2, center: { left: 10, top: 20 } });
68 |
69 | // pan 100 pixels to the left and 20 down
70 | plot.pan({ left: -100, top: 20 })
71 |
72 | Here, "center" specifies where the center of the zooming should happen. Note
73 | that this is defined in pixel space, not the space of the data points (you can
74 | use the p2c helpers on the axes in Flot to help you convert between these).
75 |
76 | "amount" is the amount to zoom the viewport relative to the current range, so
77 | 1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You
78 | can set the default in the options.
79 |
80 | */
81 |
82 | // First two dependencies, jquery.event.drag.js and
83 | // jquery.mousewheel.js, we put them inline here to save people the
84 | // effort of downloading them.
85 |
86 | /*
87 | jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
88 | Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
89 | */
90 | (function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) {
245 | // make sure min < max
246 | var tmp = min;
247 | min = max;
248 | max = tmp;
249 | }
250 |
251 | //Check that we are in panRange
252 | if (pr) {
253 | if (pr[0] != null && min < pr[0]) {
254 | min = pr[0];
255 | }
256 | if (pr[1] != null && max > pr[1]) {
257 | max = pr[1];
258 | }
259 | }
260 |
261 | var range = max - min;
262 | if (zr &&
263 | ((zr[0] != null && range < zr[0] && amount >1) ||
264 | (zr[1] != null && range > zr[1] && amount <1)))
265 | return;
266 |
267 | opts.min = min;
268 | opts.max = max;
269 | });
270 |
271 | plot.setupGrid();
272 | plot.draw();
273 |
274 | if (!args.preventEvent)
275 | plot.getPlaceholder().trigger("plotzoom", [ plot, args ]);
276 | };
277 |
278 | plot.pan = function (args) {
279 | var delta = {
280 | x: +args.left,
281 | y: +args.top
282 | };
283 |
284 | if (isNaN(delta.x))
285 | delta.x = 0;
286 | if (isNaN(delta.y))
287 | delta.y = 0;
288 |
289 | $.each(plot.getAxes(), function (_, axis) {
290 | var opts = axis.options,
291 | min, max, d = delta[axis.direction];
292 |
293 | min = axis.c2p(axis.p2c(axis.min) + d),
294 | max = axis.c2p(axis.p2c(axis.max) + d);
295 |
296 | var pr = opts.panRange;
297 | if (pr === false) // no panning on this axis
298 | return;
299 |
300 | if (pr) {
301 | // check whether we hit the wall
302 | if (pr[0] != null && pr[0] > min) {
303 | d = pr[0] - min;
304 | min += d;
305 | max += d;
306 | }
307 |
308 | if (pr[1] != null && pr[1] < max) {
309 | d = pr[1] - max;
310 | min += d;
311 | max += d;
312 | }
313 | }
314 |
315 | opts.min = min;
316 | opts.max = max;
317 | });
318 |
319 | plot.setupGrid();
320 | plot.draw();
321 |
322 | if (!args.preventEvent)
323 | plot.getPlaceholder().trigger("plotpan", [ plot, args ]);
324 | };
325 |
326 | function shutdown(plot, eventHolder) {
327 | eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
328 | eventHolder.unbind("mousewheel", onMouseWheel);
329 | eventHolder.unbind("dragstart", onDragStart);
330 | eventHolder.unbind("drag", onDrag);
331 | eventHolder.unbind("dragend", onDragEnd);
332 | if (panTimeout)
333 | clearTimeout(panTimeout);
334 | }
335 |
336 | plot.hooks.bindEvents.push(bindEvents);
337 | plot.hooks.shutdown.push(shutdown);
338 | }
339 |
340 | $.plot.plugins.push({
341 | init: init,
342 | options: options,
343 | name: 'navigate',
344 | version: '1.3'
345 | });
346 | })(jQuery);
347 |
--------------------------------------------------------------------------------
/core/js/utils.js:
--------------------------------------------------------------------------------
1 | /* Utility functions for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 | var heatersInUse;
10 |
11 |
12 | /* Text formatting */
13 |
14 | function formatUploadSpeed(bytesPerSec) {
15 | if (settings.useKiB) {
16 | if (bytesPerSec > 1073741824) { // GiB
17 | return (bytesPerSec / 1073741824).toFixed(2) + " GiB/s";
18 | }
19 | if (bytesPerSec > 1048576) { // MiB
20 | return (bytesPerSec / 1048576).toFixed(2) + " MiB/s";
21 | }
22 | if (bytesPerSec > 1024) { // KiB
23 | return (bytesPerSec / 1024).toFixed(1) + " KiB/s";
24 | }
25 | } else {
26 | if (bytesPerSec > 1000000000) { // GB
27 | return (bytesPerSec / 1000000000).toFixed(2) + " GB/s";
28 | }
29 | if (bytesPerSec > 1000000) { // MB
30 | return (bytesPerSec / 1000000).toFixed(2) + " MB/s";
31 | }
32 | if (bytesPerSec > 1000) { // KB
33 | return (bytesPerSec / 1000).toFixed(1) + " KB/s";
34 | }
35 | }
36 | return bytesPerSec + " B/s";
37 | }
38 |
39 | function formatSize(bytes) {
40 | if (settings.useKiB) {
41 | if (bytes > 1073741824) { // GiB
42 | return (bytes / 1073741824).toFixed(1) + " GiB";
43 | }
44 | if (bytes > 1048576) { // MiB
45 | return (bytes / 1048576).toFixed(1) + " MiB";
46 | }
47 | if (bytes > 1024) { // KiB
48 | return (bytes / 1024).toFixed(1) + " KiB";
49 | }
50 | } else {
51 | if (bytes > 1000000000) { // GB
52 | return (bytes / 1000000000).toFixed(1) + " GB";
53 | }
54 | if (bytes > 1000000) { // MB
55 | return (bytes / 1000000).toFixed(1) + " MB";
56 | }
57 | if (bytes > 1000) { // KB
58 | return (bytes / 1000).toFixed(1) + " KB";
59 | }
60 | }
61 | return bytes + " B";
62 | }
63 |
64 | function formatTime(value) {
65 | value = Math.round(value);
66 | if (value < 0) {
67 | value = 0;
68 | }
69 |
70 | var timeLeft = [], temp;
71 | if (value >= 3600) {
72 | temp = Math.floor(value / 3600);
73 | if (temp > 0) {
74 | timeLeft.push(temp + "h");
75 | value = value % 3600;
76 | }
77 | }
78 | if (value >= 60) {
79 | temp = Math.floor(value / 60);
80 | if (temp > 0) {
81 | timeLeft.push((temp > 9 ? temp : "0" + temp) + "m");
82 | value = value % 60;
83 | }
84 | }
85 | value = value.toFixed(0);
86 | timeLeft.push((value > 9 ? value : "0" + value) + "s");
87 |
88 | return timeLeft.reduce(function(a, b) { return a + " " + b; });
89 | }
90 |
91 | function timeToStr(time) {
92 | // Should return an ISO-like datetime string like "2016-10-24T15:39:09"
93 | // Cannot use toISOString() here because it doesn't output the localtime
94 | var result = "";
95 | result += time.getFullYear() + "-";
96 | result += (time.getMonth() + 1) + "-";
97 | result += time.getDate() + "T";
98 | result += time.getHours() + ":";
99 | result += time.getMinutes() + ":";
100 | result += time.getSeconds();
101 | return result;
102 | }
103 |
104 | function strToTime(str) {
105 | // Date.parse() doesn't always return correct dates.
106 | // Hence we must parse it using a regex here
107 | var re = /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)/;
108 | var results;
109 | if ((results = re.exec(str)) != null) {
110 | var date = new Date();
111 | date.setFullYear(results[1]);
112 | date.setMonth(results[2] - 1);
113 | date.setDate(results[3]);
114 | date.setHours(results[4]);
115 | date.setMinutes(results[5]);
116 | date.setSeconds(results[6]);
117 | return date;
118 | }
119 | return undefined;
120 | }
121 |
122 | function getHeaterStateText(state) {
123 | switch (state) {
124 | case 0:
125 | return T("off");
126 | case 1:
127 | return T("standby");
128 | case 2:
129 | return T("active");
130 | case 3:
131 | return T("fault");
132 | case 4:
133 | return T("tuning");
134 | }
135 | return T("n/a");
136 | }
137 |
138 |
139 | /* CSV Parsing */
140 |
141 | function parseCSV(str) {
142 | var arr = [];
143 | var quote = false; // true means we're inside a quoted field
144 |
145 | // iterate over each character, keep track of current row and column (of the returned array)
146 | for (var row = col = c = 0; c < str.length; c++) {
147 | var cc = str[c], nc = str[c+1]; // current character, next character
148 | if (arr.length <= row) { arr.push([]); }
149 | if (arr[row].length <= col) { arr[row].push([""]); }
150 |
151 | // If the current character is a quotation mark, and we're inside a
152 | // quoted field, and the next character is also a quotation mark,
153 | // add a quotation mark to the current column and skip the next character
154 | if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
155 |
156 | // If it's just one quotation mark, begin/end quoted field
157 | if (cc == '"') { quote = !quote; continue; }
158 |
159 | // If it's a comma and we're not in a quoted field, move on to the next column
160 | if (cc == ',' && !quote) { ++col; continue; }
161 |
162 | // If it's a newline and we're not in a quoted field, move on to the next
163 | // row and move to column 0 of that new row
164 | if (cc == '\n' && !quote) { ++row; col = 0; continue; }
165 |
166 | // Otherwise, append the current character to the current column
167 | arr[row][col] += cc;
168 | }
169 | return arr;
170 | }
171 |
172 | function getCSVValue(csvArray, key) {
173 | if (csvArray.length > 1) {
174 | var index = csvArray[0].indexOf(key);
175 | if (index == -1) {
176 | return undefined;
177 | }
178 | return csvArray[1][index].trim();
179 | }
180 | return undefined;
181 | }
182 |
183 |
184 | /* Heaters */
185 |
186 | function setHeatersInUse() {
187 | heatersInUse = [];
188 | for(var heater = 0; heater < maxHeaters; heater++) {
189 | var heaterAssigned = (heater == bedHeater);
190 | heaterAssigned |= (heater == chamberHeater);
191 | heaterAssigned |= (heater == cabinetHeater);
192 | heaterAssigned |= (getToolsByHeater(heater).length > 0);
193 | heatersInUse.push(heaterAssigned);
194 | }
195 | }
196 |
197 |
198 | /* Tool Mapping */
199 |
200 | var initialTools = "";
201 | $("#table_tools > tbody > tr[data-tool]").each(function() {
202 | initialTools += this.outerHTML;
203 | });
204 |
205 | function setToolMapping(mapping) {
206 | var mappingHasChanged = (toolMapping == undefined);
207 | if (!mappingHasChanged) {
208 | // Add missing fields in case we're talking with an older firmware version
209 | for(var i = 0; i < mapping.length; i++) {
210 | if (!mapping[i].hasOwnProperty("number")) {
211 | // This is rather ugly and should be never used
212 | mapping[i].number = i + 1;
213 | }
214 |
215 | if (!mapping[i].hasOwnProperty("name")) {
216 | mapping[i].name = "";
217 | }
218 | }
219 |
220 | // Compare the old tool mapping with the new one
221 | if (mapping.length != toolMapping.length) {
222 | mappingHasChanged = true;
223 | } else {
224 | for(var i = 0; i < mapping.length; i++) {
225 | // Stop immediately if anything significant has changed
226 | if (mapping[i].number != toolMapping[i].number ||
227 | mapping[i].name != toolMapping[i].name ||
228 | mapping[i].hasOwnProperty("filament") != toolMapping[i].hasOwnProperty("filament") ||
229 | mapping[i].heaters.toString() != toolMapping[i].heaters.toString() ||
230 | mapping[i].drives.toString() != toolMapping[i].drives.toString()
231 | ) {
232 | mappingHasChanged = true;
233 | break;
234 | }
235 |
236 | // If only the filament has changed, update it
237 | if (mapping[i].hasOwnProperty("filament") && mapping[i].filament != toolMapping[i].filament) {
238 | setToolFilament(i, mapping[i].filament);
239 | }
240 | }
241 | }
242 | }
243 |
244 | // See if anything significant has changed
245 | if (mappingHasChanged) {
246 | toolMapping = mapping;
247 | setHeatersInUse();
248 |
249 | // TODO: The web interface has no idea which drive is assigned to which axis/extruder,
250 | // so the following cannot be fully implemented yet.
251 |
252 | /**
253 | // Find out which drives may be assigned to XYZ
254 | xyzAxisMapping = [[0], [1], [2]];
255 | if (toolMapping != undefined) {
256 | for(var i = 0; i < toolMapping.length; i++) {
257 | var tool = toolMapping[i];
258 | if (tool.hasOwnProperty("axisMap")) {
259 | for(var k = 0; k < tool.axisMap.length; i++) {
260 | for(var l = 0; l < tool.axisMap[k].length; l++) {
261 | var mappedDrive = tool.axisMap[k][l];
262 | if (xyzAxisMapping[k].indexOf(mappedDrive) == -1) {
263 | xyzAxisMapping[k].push(mappedDrive);
264 | }
265 | }
266 | }
267 | }
268 | }
269 | }
270 |
271 | // Adjust the column headers of the XYZ position to match this mapping
272 | if (axisMapping[0].length > 1) {
273 | var content = "X (";
274 |
275 |
276 | content += ")";
277 | } else {
278 | $("#th_x").text("X");
279 | }*/
280 |
281 | return true;
282 | }
283 | return false;
284 | }
285 |
286 | function setToolFilament(tool, filament) {
287 | toolMapping[tool].filament = filament;
288 |
289 | var label = "T" + toolMapping[tool].number + ((filament == "") ? "" : (" - " + filament));
290 | $("#table_tools tr[data-tool='" + toolMapping[tool].number + "'] > th:first-child > span.text-muted").text(label);
291 |
292 | var filamentLabel = (filament == "") ? T("none") : filament;
293 | $("#page_tools div[data-tool='" + toolMapping[tool].number + "'] dd.filament").text(filamentLabel);
294 | }
295 |
296 | function updateFixedToolTemps(rows) {
297 | settings.defaultActiveTemps.forEach(function(temp) {
298 | rows.find(".ul-active-temp").append('' + T("{0} °C", temp) + '');
299 | rows.find(".btn-active-temp").removeClass("disabled");
300 | });
301 |
302 | settings.defaultStandbyTemps.forEach(function(temp) {
303 | rows.find(".ul-standby-temp").append('' + T("{0} °C", temp) + '');
304 | rows.find(".btn-standby-temp").removeClass("disabled");
305 | });
306 |
307 | rows.find("input").prop("disabled", !isConnected);
308 | }
309 |
310 | function getTool(number) {
311 | if (toolMapping == undefined) {
312 | return undefined;
313 | }
314 |
315 | for(var i = 0; i < toolMapping.length; i++) {
316 | if (toolMapping[i].hasOwnProperty("number")) {
317 | if (toolMapping[i].number == number) {
318 | return toolMapping[i];
319 | }
320 | } else if (i + 1 == number) {
321 | return toolMapping[i];
322 | }
323 | }
324 | return undefined;
325 | }
326 |
327 | function getToolsByHeater(heater) {
328 | if (toolMapping == undefined) {
329 | return [];
330 | }
331 |
332 | var result = [];
333 | for(var i = 0; i < toolMapping.length; i++) {
334 | for(var k = 0; k < toolMapping[i].heaters.length; k++) {
335 | if (toolMapping[i].heaters[k] == heater) {
336 | if (toolMapping[i].hasOwnProperty("number")) {
337 | result.push(toolMapping[i].number);
338 | } else {
339 | result.push(i + 1);
340 | }
341 | }
342 | }
343 | }
344 | return result;
345 | }
346 |
347 | function setCurrentTool(toolNumber) {
348 | if (toolMapping == undefined) {
349 | return;
350 | }
351 |
352 | // Hide extruder drives that cannot be used
353 | var hideExtruderInputs = true;
354 | if (toolNumber >= 0) {
355 | var drives = getTool(toolNumber).drives;
356 | for(var i = 0; i < numExtruderDrives; i++) {
357 | var toolHasDrive = (drives.indexOf(i) != -1);
358 | $("#div_extruders > div > .extr-" + i).toggleClass("hidden", !toolHasDrive);
359 | }
360 |
361 | hideExtruderInputs = (drives.length < 2);
362 | }
363 |
364 | // Hide whole selection if there is no point showing it
365 | $("#div_extruders").toggleClass("hidden", hideExtruderInputs);
366 | if (hideExtruderInputs) {
367 | $("#div_feedrate, #div_feed").removeClass("col-lg-4").addClass("col-lg-5");
368 | $("#div_extrude").removeClass("col-lg-1").addClass("col-lg-2");
369 | } else {
370 | $("#div_feedrate, #div_feed").addClass("col-lg-4").removeClass("col-lg-5");
371 | $("#div_extrude").addClass("col-lg-1").removeClass("col-lg-2");
372 |
373 | // Select first available extruder
374 | if ($('input[name="extruder"]:checked').parent().hasClass("hidden")) {
375 | $("#div_extruders > div > label:not(.hidden):first-child").click();
376 | }
377 | }
378 |
379 | // Underline current tool
380 | $("#table_tools > tbody > tr").each(function() {
381 | $(this).find("th:first-child > a > span:first-child").css("text-decoration", ($(this).data("tool") == toolNumber) ? "underline" : "");
382 | });
383 |
384 | // Update tools on the Settings page
385 | $("#page_tools button.btn-select-tool").prop("title", T("Select this tool")).html(' ' + T("Select") + '');
386 | $("#page_tools div[data-tool='" + toolNumber + "'] button.btn-select-tool").prop("title", T("Deselect this tool")).html(' ' + T("Deselect") + '');
387 | }
388 |
389 |
390 | /* Control state management */
391 |
392 | function enableControls() {
393 | $("nav input, #div_tools_heaters input, #div_content input").prop("disabled", false); // Generic inputs
394 | $("#page_tools label").removeClass("disabled"); // and on Settings page
395 | $(".machine-button").removeClass("disabled");
396 |
397 | $(".btn-emergency-stop, .gcode-input button[type=submit], .gcode").removeClass("disabled"); // Navbar
398 | $(".bed-temp, .gcode, .heater-temp, .btn-upload").removeClass("disabled"); // List items and Upload buttons
399 |
400 | $("#mobile_home_buttons button, #btn_homeall, .table-move a").removeClass("disabled"); // Move buttons
401 | $("#btn_bed_dropdown").removeClass("disabled"); // Automatic Bed Compensation
402 | $("#panel_extrude label.btn, #panel_extrude button").removeClass("disabled"); // Extruder Control
403 | $("#panel_control_misc label.btn").removeClass("disabled"); // ATX Power
404 | $("#slider_fan_control").slider("enable"); // Fan Control
405 |
406 | $("#page_scanner button").removeClass("disabled"); // Scanner
407 | $("#page_print .checkbox, #btn_baby_down, #btn_baby_up").removeClass("disabled"); // Print Control
408 | $("#slider_fan_print").slider("enable"); // Fan ...
409 | $(".table-fan-control button").removeClass("disabled"); // ... Control
410 | $("#slider_speed").slider("enable"); // Speed Factor
411 | for(var extr = 0; extr < maxExtruders; extr++) {
412 | $("#slider_extr_" + extr).slider("enable"); // Extrusion Factors
413 | }
414 |
415 | $(".online-control").removeClass("hidden"); // G-Code/Macro Files
416 | }
417 |
418 | function disableControls() {
419 | $("nav input, #div_tools_heaters input, #div_content input").prop("disabled", true); // Generic inputs
420 | $("#page_general input, #page_ui input, #page_listitems input").prop("disabled", false); // ... except ...
421 | $("#page_tools label").addClass("disabled"); // ... for Settings
422 | $(".machine-button").addClass("disabled");
423 |
424 | $(".btn-emergency-stop, .gcode-input button[type=submit], .gcode").addClass("disabled"); // Navbar
425 | $(".bed-temp, .gcode, .heater-temp, .btn-upload").addClass("disabled"); // List items and Upload buttons
426 |
427 | $("#mobile_home_buttons button, #btn_homeall, #table_move_head a").addClass("disabled"); // Move buttons
428 | $("#btn_bed_dropdown").addClass("disabled"); // Automatic Bed Compensation
429 | $("#panel_extrude label.btn, #panel_extrude button").addClass("disabled"); // Extruder Control
430 | $("#panel_control_misc label.btn").addClass("disabled"); // ATX Power
431 | $("#slider_fan_control").slider("disable"); // Fan Control
432 |
433 | $("#page_scanner button").addClass("disabled"); // Scanner
434 | $("#btn_pause, #page_print .checkbox, #btn_baby_down, #btn_baby_up").addClass("disabled"); // Print Control
435 | $("#slider_fan_print").slider("disable"); // Fan ...
436 | $(".table-fan-control button").addClass("disabled"); // ... Control
437 | $("#slider_speed").slider("disable"); // Speed Factor
438 | for(var extr = 0; extr < maxExtruders; extr++) {
439 | $("#slider_extr_" + extr).slider("disable"); // Extrusion Factors
440 | }
441 |
442 | $(".online-control").addClass("hidden"); // G-Code/Macro Files
443 | }
444 |
445 |
446 | /* Window size queries */
447 |
448 | function windowIsXsSm() {
449 | return window.matchMedia('(max-width: 991px)').matches;
450 | }
451 |
452 | function windowIsMdLg() {
453 | return window.matchMedia('(min-width: 992px)').matches;
454 | }
455 |
456 |
457 | /* Misc */
458 |
459 | function arraysEqual(a, b) {
460 | if (a != undefined && b != undefined && a.length == b.length) {
461 | for(var i = 0; i < a.length; i++) {
462 | if (a[i].constructor === Array && b[i].constructor === Array) {
463 | if (!arraysEqual(a[i], b[i])) {
464 | return false;
465 | }
466 | } else if (a[i] != b[i]) {
467 | return false;
468 | }
469 | }
470 | return true;
471 | }
472 | return false;
473 | }
474 |
475 | function log(style, message) {
476 | var entry = '';
477 | entry += '
' + (new Date()).toLocaleTimeString() + '
';
478 | entry += '
' + message + '
';
479 | $("#console_log").prepend(entry);
480 | }
481 |
482 | var audioContext = new (window.AudioContext || window.webkitAudioContext);
483 | function beep(frequency, duration) {
484 | var oscillator = audioContext.createOscillator();
485 |
486 | oscillator.type = "sine";
487 | oscillator.frequency.value = frequency;
488 | oscillator.connect(audioContext.destination);
489 | oscillator.start();
490 |
491 | setTimeout(function() {
492 | oscillator.disconnect();
493 | }, duration);
494 | }
495 |
--------------------------------------------------------------------------------
/core/js/settings.js:
--------------------------------------------------------------------------------
1 | /* Settings management for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | var settings = {
11 | autoConnect: true, // automatically connect once the page has loaded
12 | lastHost: "", // only used on localhost
13 | updateInterval: 250, // in ms
14 | extendedStatusInterval: 10, // nth status request will include extended values
15 | maxRetries: 1, // number of AJAX retries before the connection is terminated
16 |
17 | haltedReconnectDelay: 10000, // in ms (increased from 5000 for Duet WiFi)
18 | updateReconnectDelay: 20000, // in ms
19 | dwsReconnectDelay: 45000, // in ms
20 | dwcReconnectDelay: 225000, // in ms
21 |
22 | confirmStop: false, // ask for confirmation when pressing Emergency STOP
23 | useKiB: true, // display file sizes in KiB instead of KB
24 | theme: "default", // name of the theme to use
25 | scrollContent: true, // make the main content scrollable on md+ resolutions
26 | language: "en",
27 |
28 | moveFeedrate: 6000, // in mm/min
29 | halfZMovements: false, // use half Z movements
30 | babysteppingZ: 0.05, // in mm
31 | showATXControl: false, // show ATX control
32 |
33 | showFan1: true, // show fan controls for fan 1
34 | showFan2: false, // show fan controls for fan 2
35 | showFan3: false, // show fan controls for fan 3
36 | showFanRPM: false, // show fan RPM in sensors
37 |
38 | logSuccess: false, // log all sucessful G-Codes in the console
39 | uppercaseGCode: true, // convert G-Codes to upper-case before sending them
40 |
41 | doTfree: true, // tool
42 | doTpre: true, // change
43 | doTpost: true, // options
44 |
45 | useHtmlNotifications: false, // whether HTML5-based notifications can be used
46 | notificationTimeout: 5000, // in ms
47 | autoCloseUserMessages: false, // whether M117 messages are automatically closed
48 |
49 | webcamURL: "",
50 | webcamInterval: 5000, // in ms
51 | webcamFix: false, // do not append extra HTTP qualifier when reloading images
52 | webcamEmbedded: false, // use iframe to embed webcam stream
53 |
54 | defaultActiveTemps: [0, 180, 190, 200, 210, 220, 235],
55 | defaultStandbyTemps: [0, 95, 120, 140, 155, 170],
56 | defaultBedTemps: [0, 55, 60, 65, 90, 110, 120],
57 | defaultGCodes: [
58 | ["M0", "Stop"],
59 | ["M1", "Sleep"],
60 | ["M84", "Motors Off"],
61 | ["M561", "Disable bed compensation"]
62 | ]
63 | };
64 |
65 | var defaultSettings = jQuery.extend(true, {}, settings); // need to do this to get a valid copy
66 |
67 | var themeInclude;
68 |
69 |
70 | /* Setting methods */
71 |
72 | function loadSettings() {
73 | // Delete cookie and use localStorage instead
74 | document.cookie = "settings=; expires=Thu, 01 Jan 1970 00:00:00 UTC";
75 |
76 | // Try to parse the stored settings (if any)
77 | if (localStorage.getItem("settings") != null) {
78 | var loadedSettings = localStorage.getItem("settings");
79 | if (loadedSettings != undefined && loadedSettings.length > 0) {
80 | loadedSettings = JSON.parse(loadedSettings);
81 |
82 | for(var key in settings) {
83 | // Try to copy each setting if their types are equal
84 | if (loadedSettings.hasOwnProperty(key) && settings[key].constructor === loadedSettings[key].constructor) {
85 | settings[key] = loadedSettings[key];
86 | }
87 | }
88 |
89 | // Backward-compatibility
90 | if (loadedSettings.hasOwnProperty("useDarkTheme")) {
91 | settings.theme = loadedSettings.useDarkTheme ? "dark" : "default";
92 | }
93 | }
94 | }
95 |
96 | // Apply them
97 | applySettings();
98 |
99 | // Try to load the translation data
100 | $.ajax("language.xml", {
101 | type: "GET",
102 | dataType: "xml",
103 | global: false,
104 | error: function() {
105 | pageLoadComplete();
106 | },
107 | success: function(response) {
108 | translationData = response;
109 |
110 | if (translationData.children == undefined)
111 | {
112 | // Internet Explorer and Edge cannot deal with XML files in the way we want.
113 | // Disable translations for those browsers.
114 | translationData = undefined;
115 | $("#dropdown_language, #label_language").addClass("hidden");
116 | } else {
117 | $("#dropdown_language ul > li:not(:first-child)").remove();
118 | for(var i = 0; i < translationData.children[0].children.length; i++) {
119 | var id = translationData.children[0].children[i].tagName;
120 | var name = translationData.children[0].children[i].attributes["name"].value;
121 | $("#dropdown_language ul").append('' + name + '');
122 | if (settings.language == id) {
123 | $("#btn_language > span:first-child").text(name);
124 | }
125 | }
126 |
127 | translatePage();
128 | }
129 |
130 | pageLoadComplete();
131 | }
132 | });
133 | }
134 |
135 | function applySettings() {
136 | /* Apply settings */
137 |
138 | // Set AJAX timeout
139 | $.ajaxSetup({ timeout: sessionTimeout / (settings.maxRetries + 1) });
140 |
141 | // Webcam
142 | if (settings.webcamURL != "") {
143 | $("#panel_webcam").removeClass("hidden");
144 | $("#img_webcam").toggleClass("hidden", settings.webcamEmbedded);
145 | $("#div_ifm_webcam").toggleClass("hidden", !settings.webcamEmbedded);
146 |
147 | updateWebcam(true);
148 | } else {
149 | $("#panel_webcam").addClass("hidden");
150 | }
151 |
152 | // Half Z Movements on the message box dialog
153 | decreaseVal = (settings.halfZMovements) ? 5 : 10;
154 | increaseVal = (settings.halfZMovements) ? 0.05 : 0.1;
155 | $("#div_z_controls > div > button[data-axis-letter='Z']").each(function() {
156 | if ($(this).data("amount") < 0) {
157 | $(this).data("amount", decreaseVal * (-1)).contents().last().replaceWith(" -" + decreaseVal);
158 | decreaseVal /= 10;
159 | } else {
160 | $(this).data("amount", increaseVal).contents().first().replaceWith("+" + increaseVal + " ");
161 | increaseVal *= 10;
162 | }
163 | });
164 |
165 | // Babystepping
166 | $("#btn_baby_down > span.content").text(T("{0} mm", (-settings.babysteppingZ)));
167 | $("#btn_baby_up > span.content").text(T("{0} mm ", "+" + settings.babysteppingZ));
168 | $(".babystepping").toggleClass("hidden", settings.babysteppingZ <= 0);
169 |
170 | // Show/Hide Fan Controls
171 | setFanVisibility(0, settings.showFan1);
172 | setFanVisibility(1, settings.showFan2);
173 | setFanVisibility(2, settings.showFan3);
174 | numFans = undefined; // let the next status response callback hide fans that are not available
175 |
176 | // Show/Hide Fan RPM
177 | $(".fan-rpm").toggleClass("hidden", !settings.showFanRPM);
178 | $("#th_probe, #td_probe").css("border-right", settings.showFanRPM ? "" : "0px");
179 |
180 | // Show/Hide ATX Power
181 | $(".atx-control").toggleClass("hidden", !settings.showATXControl);
182 |
183 | // Apply or revoke theme
184 | if (themeInclude != undefined) {
185 | themeInclude.remove();
186 | themeInclude = undefined;
187 | }
188 |
189 | switch (settings.theme) {
190 | case "default": // Default Bootstrap theme
191 | themeInclude = $('');
192 | themeInclude.appendTo("head");
193 | $("#theme_notice").addClass("hidden");
194 | break;
195 |
196 | case "dark": // Bootstrap theme + fotomas's customizations
197 | themeInclude = $('' +
198 | '');
199 | themeInclude.appendTo("head");
200 | $("#theme_notice").removeClass("hidden");
201 | break;
202 |
203 | case "none": // No theme at all
204 | applyThemeColors();
205 | $("#theme_notice").addClass("hidden");
206 | break;
207 | }
208 |
209 | // Make main content scrollable on md+ screens or restore default behavior
210 | $("#div_content").css("overflow-y", (settings.scrollContent) ? "auto" : "").resize();
211 |
212 | /* Set values on the Settings page */
213 |
214 | // Set input values
215 | for(var key in settings) {
216 | var element = $('[data-setting="' + key + '"]');
217 | if (element.length != 0) {
218 | var type = element.attr("type");
219 | if (type == "checkbox") {
220 | element.prop("checked", settings[key]);
221 | } else if (type == "number") {
222 | var factor = element.data("factor");
223 | if (factor == undefined) {
224 | factor = 1;
225 | }
226 | element.val(settings[key] / factor);
227 | } else if (type != "button") {
228 | element.val(settings[key]);
229 | }
230 | }
231 | }
232 |
233 | // Set theme selection
234 | $("#btn_theme").data("theme", settings.theme);
235 | var themeName = $('#dropdown_theme ul [data-theme="' + settings.theme + '"]').text();
236 | $("#btn_theme > span:first-child").text(themeName);
237 |
238 | // Language is set in XML AJAX handler
239 |
240 | // Default head temperatures
241 | clearHeadTemperatures();
242 | settings.defaultActiveTemps.forEach(function(temp) {
243 | addHeadTemperature(temp, "active");
244 | });
245 | settings.defaultStandbyTemps.forEach(function(temp) {
246 | addHeadTemperature(temp, "standby");
247 | });
248 |
249 | // Default bed temperatures
250 | clearBedTemperatures();
251 | settings.defaultBedTemps.forEach(function(temp) {
252 | addBedTemperature(temp);
253 | });
254 |
255 | // Default G-Codes
256 | clearDefaultGCodes();
257 | settings.defaultGCodes.forEach(function(entry) {
258 | addDefaultGCode(entry[1], entry[0]);
259 | });
260 |
261 | // Force GUI update to apply half Z movements in the axes
262 | updateGui();
263 | }
264 |
265 | function saveSettings() {
266 | // Get input values
267 | for(var key in settings) {
268 | var element = $('[data-setting="' + key + '"]');
269 | if (element.length != 0) {
270 | var type = element.attr("type");
271 | if (type == "checkbox") {
272 | settings[key] = element.is(":checked");
273 | } else if (type == "number") {
274 | var min = element.data("min");
275 | var max = element.data("max");
276 | var factor = element.data("factor");
277 | if (factor == undefined) {
278 | factor = 1;
279 | }
280 | settings[key] = constrainSetting(element.val() * factor, defaultSettings[key], min, max);
281 | } else {
282 | settings[key] = element.val();
283 | }
284 | }
285 | }
286 |
287 | // Save theme
288 | settings.theme = $("#btn_theme").data("theme");
289 |
290 | // Save language
291 | if (settings.language != $("#btn_language").data("language")) {
292 | showMessage("success", T("Language has changed"), T("You have changed the current language. Please reload the web interface to apply this change."), 0);
293 | }
294 | settings.language = $("#btn_language").data("language");
295 |
296 | // Default G-Codes
297 | settings.defaultGCodes = [];
298 | $("#table_gcodes > tbody > tr").each(function() {
299 | settings.defaultGCodes.push([$(this).find("label").text(), $(this).find("td:eq(1)").text()]);
300 | });
301 | settings.defaultGCodes = settings.defaultGCodes.sort(function(a, b) {
302 | if (a[0][0] != b[0][0]) {
303 | return a[0].charCodeAt(0) - b[0].charCodeAt(0);
304 | }
305 | var x = a[0].match(/(\d+)/g)[0];
306 | var y = b[0].match(/(\d+)/g)[0];
307 | if (x == undefined || y == undefined) {
308 | return parseInt(a[0]) - parseInt(b[0]);
309 | }
310 | return x - y;
311 | });
312 |
313 | // Default Heater Temperatures
314 | settings.defaultActiveTemps = [];
315 | $("#ul_active_temps > li").each(function() {
316 | settings.defaultActiveTemps.push($(this).data("temperature"));
317 | });
318 | settings.defaultActiveTemps = settings.defaultActiveTemps.sort(function(a, b) { return a - b; });
319 | settings.defaultStandbyTemps = [];
320 | $("#ul_standby_temps > li").each(function() {
321 | settings.defaultStandbyTemps.push($(this).data("temperature"));
322 | });
323 | settings.defaultStandbyTemps = settings.defaultStandbyTemps.sort(function(a, b) { return a - b; });
324 |
325 | // Default Bed Temperatures
326 | settings.defaultBedTemps = [];
327 | $("#ul_bed_temps > li").each(function() {
328 | settings.defaultBedTemps.push($(this).data("temperature"));
329 | });
330 | settings.defaultBedTemps = settings.defaultBedTemps.sort(function(a, b) { return a - b; });
331 |
332 | // Save Settings
333 | localStorage.setItem("settings", JSON.stringify(settings));
334 | }
335 |
336 | function constrainSetting(value, defaultValue, minValue, maxValue) {
337 | if (isNaN(value)) {
338 | return defaultValue;
339 | }
340 | if (value < minValue) {
341 | return minValue;
342 | }
343 | if (value > maxValue) {
344 | return maxValue;
345 | }
346 |
347 | return value;
348 | }
349 |
350 |
351 | /* Setting events */
352 |
353 | // Apply & Reset settings
354 |
355 | $(".btn-reset-settings").click(function(e) {
356 | showConfirmationDialog(T("Reset Settings"), T("Are you sure you want to revert to Factory Settings?"), function() {
357 | if (defaultSettings.language != settings.language) {
358 | showMessage("info", T("Language has changed"), T("You have changed the current language. Please reload the web interface to apply this change."), 0);
359 | }
360 |
361 | settings = jQuery.extend(true, {}, defaultSettings);
362 | $("#btn_language").data("language", "en").children("span:first-child").text("English");
363 | $("#btn_theme").data("theme", "default").children("span:first-child").text(T("Bootstrap"));
364 |
365 | applySettings();
366 | saveSettings();
367 |
368 | localStorage.removeItem("extraSensorVisibility");
369 | resetChartData();
370 |
371 | localStorage.removeItem("cachedFileInfo");
372 | });
373 | e.preventDefault();
374 | });
375 |
376 | $("#frm_settings").submit(function(e) {
377 | saveSettings();
378 | applySettings();
379 | showMessage("success", "", "" + T("Settings applied!") + "");
380 | e.preventDefault();
381 | });
382 |
383 | $("#frm_settings > ul > li a").on("shown.bs.tab", function(e) {
384 | $("#frm_settings > ul li").removeClass("active");
385 | var links = $('#frm_settings > ul > li a[href="' + $(this).attr("href") + '"]');
386 | $.each(links, function() {
387 | $(this).parent().addClass("active");
388 | });
389 | });
390 |
391 | // User Interface
392 |
393 | $("#dropdown_theme > ul a").click(function(e) {
394 | $("#btn_theme > span:first-child").text($(this).text());
395 | $("#btn_theme").data("theme", $(this).data("theme"));
396 | e.preventDefault();
397 | });
398 |
399 | $("body").on("click", "#dropdown_language > ul a", function(e) {
400 | $("#btn_language > span:first-child").text($(this).text());
401 | $("#btn_language").data("language", $(this).data("language"));
402 | e.preventDefault();
403 | });
404 |
405 | $('a[href="#page_ui"]').on('shown.bs.tab', function () {
406 | $("#btn_clear_cache").toggleClass("disabled", $.isEmptyObject(cachedFileInfo));
407 | });
408 |
409 | $("#btn_clear_cache").click(function(e) {
410 | gcodeUpdateIndex = -1;
411 | clearFileCache();
412 | $("#btn_clear_cache").addClass("disabled");
413 | e.preventDefault();
414 | });
415 |
416 | $("[data-setting='useHtmlNotifications']").change(function() {
417 | if ($(this).prop("checked")) {
418 | if (!("Notification" in window)) {
419 | // Don't allow this option to be set if the browser doesn't support it
420 | $(this).prop("checked", false);
421 | alert(T("This browser does not support desktop notification"));
422 | }
423 | else if (Notification.permission !== 'denied') {
424 | $(this).prop("checked", false);
425 | Notification.requestPermission(function(permission) {
426 | if (!('permission' in Notification)) {
427 | Notification.permission = permission;
428 | }
429 |
430 | if (permission === "granted") {
431 | // Don't allow this option to be set unless permission has been granted
432 | $("[data-setting='useHtmlNotifications']").prop("checked", true);
433 | }
434 | });
435 | }
436 | }
437 | });
438 |
439 | // List Items
440 |
441 | $("#btn_add_gcode").click(function(e) {
442 | var item = ' | ' + $("#input_gcode_description").val().trim() + ' | ';
443 | item += ' |
';
445 | $("#table_gcodes > tbody").append(item);
446 |
447 | e.preventDefault();
448 | });
449 |
450 | $("input[name='temp_selection']:radio").change(function() {
451 | if ($(this).val() == "active") {
452 | $("#ul_active_temps").removeClass("hidden");
453 | $("#ul_standby_temps").addClass("hidden");
454 | } else {
455 | $("#ul_standby_temps").removeClass("hidden");
456 | $("#ul_active_temps").addClass("hidden");
457 | }
458 | });
459 |
460 | $("#btn_add_head_temp").click(function(e) {
461 | var temperature = constrainSetting($("#input_add_head_temp").val(), 0, -273.15, 300);
462 | var type = $('input[name="temp_selection"]:checked').val();
463 |
464 | var item = '' + temperature + ' °C';
465 | item += '';
467 | $("#ul_" + type + "_temps").append(item);
468 |
469 | e.preventDefault();
470 | });
471 |
472 | $("#btn_add_bed_temp").click(function(e) {
473 | var temperature = constrainSetting($("#input_add_bed_temp").val(), 0, -273.15, 180);
474 |
475 | var item = '' + temperature + ' °C';
476 | item += '';
478 | $("#ul_bed_temps").append(item);
479 |
480 | e.preventDefault();
481 | });
482 |
483 | $("body").on("click", ".btn-delete-parent", function(e) {
484 | $(this).parents("tr, li").remove();
485 | e.preventDefault();
486 | });
487 |
488 | $("#page_listitems input").on("input", function() {
489 | // Validate form controls
490 | $("#btn_add_gcode").toggleClass("disabled", $("#input_gcode").val().trim() == "" || $("#input_gcode_description").val().trim() == "");
491 | $("#btn_add_head_temp").toggleClass("disabled", isNaN(parseFloat($("#input_add_head_temp").val())));
492 | $("#btn_add_bed_temp").toggleClass("disabled", isNaN(parseFloat($("#input_add_bed_temp").val())));
493 | });
494 |
495 | $("#page_listitems input").keydown(function(e) {
496 | if (e.which == 13) {
497 | var button = $(this).closest("div:not(.input-group):eq(0)").find("button");
498 | if (!button.hasClass("disabled")) {
499 | button.click();
500 | }
501 | e.preventDefault();
502 | }
503 | });
504 |
505 | // Machine Properties
506 |
507 | $("#btn_fw_diagnostics").click(function() {
508 | if (isConnected) {
509 | sendGCode("M122");
510 | showPage("console");
511 | }
512 | });
513 |
514 | // Tools
515 |
516 | $("#btn_add_tool").click(function(e) {
517 | var gcode = "M563 P" + $("#input_tool_number").val() + " S\"" + $("#input_tool_name").val() + "\"";
518 |
519 | var drives = $("input[name='tool_drives']:checked");
520 | if (drives.length > 0) {
521 | var driveList = [];
522 | drives.each(function() { driveList.push($(this).val()); });
523 | gcode += " D" + driveList.reduce(function(a, b) { return a + ":" + b; });
524 | }
525 |
526 | var heaters = $("input[name='tool_heaters']:checked");
527 | if (heaters.length > 0) {
528 | var heaterList = [];
529 | heaters.each(function() { heaterList.push($(this).val()); });
530 | gcode += " H" + heaterList.reduce(function(a, b) { return a + ":" + b; });
531 | }
532 |
533 | sendGCode(gcode);
534 | extendedStatusCounter = settings.extendedStatusInterval;
535 |
536 | e.preventDefault();
537 | });
538 |
539 | // Display toggle for settings sub-pages
540 |
541 | $('a[href="#page_general"], a[href="#page_ui"], a[href="#page_listitems"]').on('shown.bs.tab', function () {
542 | $("#row_save_settings").removeClass("hidden");
543 | });
544 |
545 | $('a[href="#page_machine"], a[href="#page_tools"], a[href="#page_sysedit"]').on('shown.bs.tab', function () {
546 | $("#row_save_settings").addClass("hidden");
547 | });
548 |
549 | // Piecon settings
550 |
551 | Piecon.setOptions({
552 | color: "#0000ff", // Pie chart color
553 | background: "#bbb", // Empty pie chart color
554 | shadow: "#fff", // Outer ring color
555 | fallback: "force" // Toggles displaying percentage in the title bar (possible values - true, false, 'force')
556 | });
557 |
558 |
--------------------------------------------------------------------------------
/core/js/upload.js:
--------------------------------------------------------------------------------
1 | /* Upload handling for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | var isUploading = false; // Is a file upload in progress?
11 |
12 | var uploadType, uploadFiles, uploadRows, uploadedFileCount;
13 | var uploadTotalBytes, uploadedTotalBytes;
14 | var uploadStartTime, uploadRequest, uploadFileSize, uploadFileName, uploadPosition;
15 | var uploadedDWC, uploadIncludedConfig, uploadFirmwareFile, uploadDWCFile, uploadDWSFile, uploadDWSSFile;
16 | var uploadHadError, uploadFilesSkipped;
17 |
18 | var firmwareFileName = "RepRapFirmware"; // Name of the firmware file without .bin extension
19 | var targetFirmwareFileName = firmwareFileName;
20 |
21 |
22 | function uploadTextFile(filename, content, callback, showNotification, configFileHandled) {
23 | if (showNotification == undefined) { showNotification = true; }
24 | if (configFileHandled == undefined) { configFileHandled = false; }
25 |
26 | // Move config.g to config.g.bak if it is overwritten
27 | if (filename == "0:/sys/config.g" && !configFileHandled && $("#table_sys_files tr[data-file='config.g']").length != 0) {
28 | $.ajax(ajaxPrefix + "rr_move?old=0:/sys/config.g&new=0:/sys/config.g.bak&deleteexisting=yes", {
29 | dataType: "json",
30 | success: function() {
31 | uploadTextFile(filename, content, callback, showNotification, true);
32 | }
33 | });
34 | return;
35 | }
36 |
37 | // Stop status updates as long as files are being transferred (should not be needed but leave it here because some users reported issues)
38 | stopUpdates();
39 |
40 | // Ideally we should use FileAPI here, but IE+Edge don't support it
41 | //var file = new File([content], filename, { type: "application/octet-stream" });
42 | var file = new Blob([content], { type: "application/octet-stream" });
43 | file.name = filename;
44 | file.lastModified = (new Date()).getTime();
45 |
46 | var uploadRequest = $.ajax(ajaxPrefix + "rr_upload?name=" + encodeURIComponent(filename) + "&time=" + encodeURIComponent(timeToStr(new Date())), {
47 | data: file,
48 | dataType: "json",
49 | processData: false,
50 | contentType: false,
51 | timeout: 0,
52 | type: "POST",
53 | global: false,
54 | error: function(jqXHR, textStatus, errorThrown) {
55 | startUpdates();
56 | showMessage("danger", T("Error"), T("Could not update file {0}!", filename));
57 | console.log("Text file upload failed!\nStatus: " + textStatus + "\nError: " + errorThrown);
58 | },
59 | success: function(response) {
60 | if (response.err == 0) {
61 | if (showNotification) {
62 | showMessage("success", T("File Updated"), T("The file {0} has been successfully uploaded.", filename));
63 | if (lastStatusResponse != undefined && lastStatusResponse.status == 'I') {
64 | if (filename.toLowerCase() == "0:/sys/config.g") {
65 | showConfirmationDialog(T("Reboot Duet?"), T("You have just uploaded a config file. Would you like to reboot your Duet now?"), function() {
66 | // Perform software reset
67 | sendGCode("M999");
68 | });
69 | }
70 | }
71 | }
72 | } else {
73 | showMessage("danger", T("Error"), T("Could not update file {0}!", filename));
74 | }
75 |
76 | startUpdates();
77 | if (callback != undefined) {
78 | callback();
79 | }
80 | }
81 | });
82 | }
83 |
84 | function startUpload(type, files, fromCallback) {
85 | // Unzip files if necessary
86 | if (type == "filament" || type == "macro" || type == "generic") {
87 | var containsZip = false;
88 | $.each(files, function() {
89 | if (this.name.toLowerCase().match("\\.zip$") != null) {
90 | uploadedDWC |= this.name.toLowerCase().match("^duetwebcontrol.*\\.zip") != null;
91 |
92 | var fileReader = new FileReader();
93 | fileReader.onload = (function(theFile) {
94 | return function(e) {
95 | try {
96 | var zipFile = new JSZip(), filesToUnpack = 0, filesUnpacked = 0;
97 | zipFile.loadAsync(e.target.result).then(function(zip) {
98 | var zipFiles = [];
99 | $.each(zip.files, function(index, zipEntry) {
100 | if (!zipEntry.dir && zipEntry.name.indexOf(".") != 0 && zipEntry.name.match("README") == null) {
101 | var zipName = zipEntry.name;
102 | if (type != "filament") {
103 | zipName = zipEntry.name.split("/");
104 | zipName = zipName[zipName.length - 1];
105 | }
106 |
107 | filesToUnpack++;
108 | zipEntry.async("arraybuffer").then(function(buffer) {
109 | // See above. FileAPI isn't supported by IE+Edge
110 | //var unpackedFile = new File([zipEntry.asArrayBuffer()], zipName, { type: "application/octet-stream", lastModified: zipEntry.date });
111 | var unpackedFile = new Blob([buffer], { type: "application/octet-stream" });
112 | unpackedFile.name = zipName;
113 | unpackedFile.lastModified = zipEntry.date;
114 | zipFiles.push(unpackedFile);
115 |
116 | filesUnpacked++;
117 | if (filesToUnpack == filesUnpacked) {
118 | // This had to be moved because the new JSZip API is completely async
119 | startUpload(type, zipFiles, true);
120 | }
121 | });
122 | }
123 | });
124 |
125 | if (zip.files.length == 0) {
126 | showMessage("warning", T("Error"), T("The archive {0} does not contain any files!", theFile.name));
127 | }
128 | });
129 | } catch(e) {
130 | console.log("ZIP error: " + e);
131 | showMessage("danger", T("Error"), T("Could not read contents of file {0}!", theFile.name));
132 | }
133 | }
134 | })(this);
135 | fileReader.readAsArrayBuffer(this);
136 |
137 | containsZip = true;
138 | return false;
139 | }
140 | });
141 |
142 | if (containsZip) {
143 | // We're relying on an async task which will trigger this method again when required
144 | return false;
145 | }
146 | }
147 |
148 | // Safety check for Upload and Print
149 | if (type == "print" && files.length > 1) {
150 | showMessage("warning", T("Error"), T("You can only upload and print one file at once!"));
151 | return false;
152 | }
153 |
154 | // Safety check for Filament uploads
155 | if (type == "filament") {
156 | var hadError = false;
157 | $.each(files, function() {
158 | if (this.name.indexOf("/") == -1) {
159 | showMessage("danger", T("Error"), T("You cannot upload single files. Please upload only ZIP files that contain at least one directory per filament configuration."));
160 | hadError = true;
161 | return false;
162 | }
163 | });
164 | if (hadError) {
165 | return false;
166 | }
167 | }
168 |
169 | // Initialize some values
170 | stopUpdates();
171 | isUploading = true;
172 | uploadType = type;
173 | uploadTotalBytes = uploadedTotalBytes = uploadedFileCount = 0;
174 | uploadFiles = files;
175 | $.each(files, function() {
176 | uploadTotalBytes += this.size;
177 | });
178 | uploadRows = [];
179 | if (!fromCallback) {
180 | uploadedDWC = false;
181 | }
182 | uploadIncludedConfig = false;
183 | uploadFirmwareFile = uploadDWCFile = uploadDWSFile = uploadDWSSFile = undefined;
184 | uploadFilesSkipped = uploadHadError = false;
185 |
186 | // Reset modal dialog
187 | $("#modal_upload").data("backdrop", "static");
188 | $("#modal_upload .close, #modal_upload button[data-dismiss='modal']").addClass("hidden");
189 | $("#btn_cancel_upload, #modal_upload p").removeClass("hidden");
190 | $("#modal_upload h4").text(T("Uploading File(s), {0}% Complete", 0));
191 |
192 | // Add files to the table
193 | $("#table_upload_files > tbody").children().remove();
194 | $.each(files, function() {
195 | if (type == "generic") {
196 | // config and firmware files can be always uploaded
197 | uploadIncludedConfig |= (this.name == "config.g");
198 | if (this.name.toUpperCase().match("^" + firmwareFileName.toUpperCase() + ".*\.BIN") != null) {
199 | uploadFirmwareFile = this.name;
200 | targetFirmwareFileName = firmwareFileName;
201 | }
202 | else if (allowCombinedFirmware && this.name.toUpperCase().match("^DUET2COMBINEDFIRMWARE.*\.BIN") != null) {
203 | uploadFirmwareFile = this.name;
204 | targetFirmwareFileName = "Duet2CombinedFirmware";
205 | }
206 |
207 | // See if a new DWC version is being installed
208 | uploadedDWC |= this.name.indexOf("reprap.htm") != -1;
209 |
210 | // DuetWiFi-specific files can be used only on a Duet WiFi
211 | if (boardType.indexOf("duetwifi") == 0) {
212 | if (this.name.toUpperCase().match("^DUETWIFISOCKETSERVER.*\.BIN") != null) {
213 | uploadDWSSFile = this.name;
214 | } else if (this.name.toUpperCase().match("^DUETWIFISERVER.*\.BIN") != null) {
215 | uploadDWSFile = this.name;
216 | } else if (this.name.toUpperCase().match("^DUETWEBCONTROL.*\.BIN") != null) {
217 | uploadDWCFile = this.name;
218 | }
219 | }
220 | }
221 |
222 | var row = '| ' + this.name + ' | ';
223 | row += '' + formatSize(this.size) + ' | ';
224 | row += ' |
';
225 |
226 | var rowElem = $(row);
227 | rowElem.appendTo("#table_upload_files > tbody");
228 | uploadRows.push(rowElem);
229 | });
230 | $("#modal_upload").modal("show");
231 |
232 | // Start file upload
233 | uploadNextFile();
234 | }
235 |
236 | function uploadNextFile() {
237 | // Prepare some upload values
238 | var file = uploadFiles[uploadedFileCount];
239 | uploadFileName = file.name;
240 | uploadFileSize = file.size;
241 | uploadStartTime = new Date();
242 | uploadPosition = 0;
243 |
244 | // Check if this file should be skipped
245 | if (uploadType == "generic") {
246 | var skipFile = false;
247 |
248 | if (!filenameValid(uploadFileName)) {
249 | // Skip files that contain invalid characters
250 | skipFile = true;
251 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes."));
252 | } else if (boardType.indexOf("duetwifi") != 0) {
253 | var lcName = uploadFileName.toLowerCase();
254 |
255 | // Skip DuetWebControl*.bin on wired Duets
256 | if (lcName.match("^duetwebcontrol.*\\.bin") != null) {
257 | skipFile = true;
258 | }
259 |
260 | // Skip DuetWiFiServer*.bin and DuetWiFiSocketServer*.bin on wired Duets
261 | if (lcName.match("^duetwifiserver.*\\.bin") != null || lcName.match("^duetwifisocketserver.*\\.bin") != null) {
262 | skipFile = true;
263 | }
264 | }
265 |
266 | if (skipFile) {
267 | fileUploadSkipped();
268 | return;
269 | }
270 | }
271 |
272 | // Determine the right path
273 | var targetPath = "";
274 | switch (uploadType) {
275 | case "gcode": // Upload G-Code
276 | case "print": // Upload & Print G-Code
277 | targetPath = currentGCodeDirectory + "/" + uploadFileName;
278 | clearFileCache(targetPath);
279 | break;
280 |
281 | case "macro": // Upload Macro
282 | targetPath = currentMacroDirectory + "/" + uploadFileName;
283 | break;
284 |
285 | case "filament": // Filament (only to sub-directories)
286 | targetPath = "0:/filaments/" + uploadFileName;
287 | break;
288 |
289 | default: // Generic Upload (on the Settings page)
290 | var fileExts = uploadFileName.split('.');
291 | var fileExt = fileExts.pop().toLowerCase();
292 | if (fileExt == "gz") {
293 | // If this file was compressed, try to get the actual extension
294 | fileExt = fileExts.pop();
295 | }
296 |
297 | switch (fileExt) {
298 | case "ico":
299 | case "html":
300 | case "htm":
301 | case "xml":
302 | targetPath = "0:/www/" + uploadFileName;
303 | break;
304 |
305 | case "css":
306 | case "map":
307 | targetPath = "0:/www/css/" + uploadFileName;
308 | break;
309 |
310 | case "eot":
311 | case "svg":
312 | case "ttf":
313 | case "woff":
314 | case "woff2":
315 | targetPath = "0:/www/fonts/" + uploadFileName;
316 | break;
317 |
318 | case "jpeg":
319 | case "jpg":
320 | case "png":
321 | targetPath = "0:/www/img/" + uploadFileName;
322 | break;
323 |
324 | case "js":
325 | targetPath = "0:/www/js/" + uploadFileName;
326 | break;
327 |
328 | default:
329 | targetPath = "0:/sys/" + uploadFileName;
330 | }
331 | }
332 |
333 | // Update the GUI
334 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(T("Starting"));
335 | uploadRows[uploadedFileCount].find(".glyphicon").removeClass("glyphicon-asterisk").addClass("glyphicon-cloud-upload");
336 |
337 | // Begin another POST file upload
338 | uploadRequest = $.ajax(ajaxPrefix + "rr_upload?name=" + encodeURIComponent(targetPath) + "&time=" + encodeURIComponent(timeToStr(new Date(file.lastModified))), {
339 | data: file,
340 | dataType: "json",
341 | processData: false,
342 | contentType: false,
343 | timeout: 0,
344 | type: "POST",
345 | global: false,
346 | error: function(jqXHR, textStatus, errorThrown) {
347 | finishCurrentUpload(false);
348 | },
349 | success: function(data) {
350 | if (isUploading) {
351 | finishCurrentUpload(data.err == 0);
352 | }
353 | },
354 | xhr: function() {
355 | var xhr = new window.XMLHttpRequest();
356 | xhr.upload.addEventListener("progress", function(event) {
357 | if (isUploading && event.lengthComputable) {
358 | // Calculate current upload speed (Date is based on milliseconds)
359 | uploadSpeed = event.loaded / (((new Date()) - uploadStartTime) / 1000);
360 |
361 | // Update global progress
362 | uploadedTotalBytes += (event.loaded - uploadPosition);
363 | uploadPosition = event.loaded;
364 |
365 | var uploadTitle = T("Uploading File(s), {0}% Complete", ((uploadedTotalBytes / uploadTotalBytes) * 100).toFixed(0));
366 | if (uploadSpeed > 0) {
367 | uploadTitle += " (" + formatUploadSpeed(uploadSpeed) + ")";
368 | }
369 | $("#modal_upload h4").text(uploadTitle);
370 |
371 | // Update progress bar
372 | var progress = ((event.loaded / event.total) * 100).toFixed(0);
373 | uploadRows[uploadedFileCount].find(".progress-bar").css("width", progress + "%");
374 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(progress + " %");
375 | }
376 | }, false);
377 | return xhr;
378 | }
379 | });
380 | }
381 |
382 | function finishCurrentUpload(success) {
383 | // Keep the progress updated
384 | if (!success) {
385 | uploadHadError = true;
386 | uploadedTotalBytes += (uploadFileSize - uploadPosition);
387 | }
388 |
389 | // Update glyphicon and progress bar
390 | uploadRows[uploadedFileCount].find(".glyphicon").removeClass("glyphicon-cloud-upload").addClass(success ? "glyphicon-ok" : "glyphicon-alert");
391 | uploadRows[uploadedFileCount].find(".progress-bar").removeClass("progress-bar-info").addClass(success ? "progress-bar-success" : "progress-bar-danger").css("width", "100%");
392 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(success ? "100 %" : T("ERROR"));
393 |
394 | // Go on with upload logic if we're still busy
395 | if (isUploading) {
396 | uploadedFileCount++;
397 | if (uploadFiles.length > uploadedFileCount) {
398 | // Upload the next file
399 | uploadNextFile();
400 | } else {
401 | // No more files to upload - we're done
402 | finishUpload(!uploadHadError);
403 | }
404 | }
405 | }
406 |
407 | function fileUploadSkipped() {
408 | // Keep the progress updated
409 | uploadedTotalBytes += (uploadFileSize - uploadPosition);
410 |
411 | // Update glyphicon and progress bar
412 | uploadRows[uploadedFileCount].find(".glyphicon").removeClass("glyphicon-cloud-asterisk").addClass("glyphicon-warning-sign");
413 | uploadRows[uploadedFileCount].find(".progress-bar").removeClass("progress-bar-info").addClass("progress-bar-warning").css("width", "100%");
414 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(T("SKIPPED"));
415 |
416 | // Go on with upload logic if we're still busy
417 | uploadFilesSkipped = true;
418 | if (isUploading) {
419 | uploadedFileCount++;
420 | if (uploadFiles.length > uploadedFileCount) {
421 | // Upload the next file
422 | uploadNextFile();
423 | } else {
424 | // No more files to upload - we're done
425 | finishUpload(true);
426 | }
427 | }
428 | }
429 |
430 | function cancelUpload() {
431 | isUploading = uploadFilesSkipped = false;
432 | finishCurrentUpload(false);
433 | uploadRequest.abort();
434 | finishUpload(false);
435 | $("#modal_upload h4").text(T("Upload Cancelled!"));
436 | startUpdates();
437 | }
438 |
439 | function finishUpload(success) {
440 | // Reset upload variables
441 | isUploading = false;
442 | $("#input_file_upload").val("");
443 |
444 | // Set some values in the modal dialog
445 | $("#modal_upload h4").text(T("Upload Complete!"));
446 | $("#btn_cancel_upload, #modal_upload p").addClass("hidden");
447 | $("#modal_upload .close, #modal_upload button[data-dismiss='modal']").removeClass("hidden");
448 |
449 | if (success) {
450 | // If everything went well, update the GUI immediately
451 | uploadHasFinished(true);
452 | } else {
453 | // In case an upload has been aborted, give the firmware some time to recover
454 | setTimeout(function() { uploadHasFinished(false); }, 1000);
455 | }
456 | }
457 |
458 | function uploadHasFinished(success) {
459 | // Make sure the G-Codes and Macro pages are updated
460 | if (uploadType == "gcode" || uploadType == "print") {
461 | gcodeUpdateIndex = -1;
462 | if (currentPage == "files") {
463 | updateGCodeFiles();
464 | }
465 | } else if (uploadType == "macro") {
466 | macroUpdateIndex = -1;
467 | if (currentPage == "control" || currentPage == "macros") {
468 | updateMacroFiles();
469 | }
470 | } else if (uploadType == "filament") {
471 | if (currentPage == "filaments") {
472 | updateFilaments();
473 | }
474 | }
475 |
476 | // Start polling again
477 | startUpdates();
478 |
479 | // Deal with different upload types
480 | if (success) {
481 | // Check if a print is supposed to be started
482 | if (uploadType == "print") {
483 | waitingForPrintStart = true;
484 | sendGCode('M32 "' + currentGCodeDirectory + "/" + uploadFileName + '"');
485 | }
486 |
487 | // Ask for firmware/DWC update if it's safe to do
488 | else if (lastStatusResponse != undefined && lastStatusResponse.status == 'I') {
489 | // Test for firmware update before we test for a new config file, because a firmware update includes a reset
490 | if (uploadFirmwareFile != undefined && uploadDWSFile == undefined && uploadDWSSFile == undefined && uploadDWCFile == undefined) {
491 | $("#modal_upload").modal("hide");
492 | showConfirmationDialog(T("Perform Firmware Update?"), T("You have just uploaded a firmware file. Would you like to update your Duet now?"), startFirmwareUpdate);
493 | } else if ((uploadDWSFile != undefined || uploadDWSSFile != undefined) && uploadFirmwareFile == undefined && uploadDWCFile == undefined) {
494 | $("#modal_upload").modal("hide");
495 | showConfirmationDialog(T("Perform WiFi Server Update?"), T("You have just uploaded a Duet WiFi server firmware file. Would you like to install it now?"), startDWSUpdate);
496 | } else if (uploadDWCFile != undefined && uploadFirmwareFile == undefined && uploadDWSFile == undefined && uploadDWSSFile == undefined) {
497 | $("#modal_upload").modal("hide");
498 | showConfirmationDialog(T("Perform Duet Web Control Update?"), T("You have just uploaded a Duet Web Control package. Would you like to install it now?"), startDWCUpdate);
499 | } else if (uploadFirmwareFile != undefined || uploadDWSFile != undefined || uploadDWSSFile != undefined || uploadDWCFile != undefined) {
500 | $("#modal_upload").modal("hide");
501 | showConfirmationDialog(T("Perform Firmware Update?"), T("You have just uploaded multiple firmware files. Would you like to install them now?"), startFirmwareUpdates);
502 | } else if (uploadIncludedConfig) {
503 | $("#modal_upload").modal("hide");
504 | showConfirmationDialog(T("Reboot Duet?"), T("You have just uploaded a config file. Would you like to reboot your Duet now?"), function() {
505 | // Perform software reset
506 | sendGCode("M999");
507 | });
508 | } else if (uploadedDWC) {
509 | $("#modal_upload").modal("hide");
510 |
511 | if (sessionPassword != defaultPassword) {
512 | connect(sessionPassword, false);
513 |
514 | showConfirmationDialog(T("Reload Page?"), T("You have just updated Duet Web Control. Would you like to reload the page now?"), function() {
515 | location.reload();
516 | });
517 | } else {
518 | location.reload();
519 | }
520 | }
521 | }
522 | }
523 | }
524 |
525 | function startFirmwareUpdates() {
526 | var sParams = [], fwFile, dwsFile, dwcFile;
527 | if (uploadFirmwareFile != undefined) {
528 | fwFile = "0:/sys/" + uploadFirmwareFile;
529 | sParams.push("0");
530 | }
531 | if (uploadDWSSFile != undefined) {
532 | dwsFile = "0:/sys/" + uploadDWSSFile;
533 | sParams.push("1");
534 | } else if (uploadDWSFile != undefined) {
535 | dwsFile = "0:/sys/" + uploadDWSFile;
536 | sParams.push("1");
537 | }
538 | if (uploadDWCFile != undefined) {
539 | dwcFile = "0:/sys/" + uploadDWCFile;
540 | sParams.push("2");
541 | }
542 |
543 | // Stop status updates so the user doesn't see any potential error messages
544 | stopUpdates();
545 |
546 | // Move the file(s) into place
547 | moveFile(fwFile, "0:/sys/" + targetFirmwareFileName + ".bin", function() {
548 | moveFile(dwsFile, "0:/sys/DuetWiFiServer.bin", function() {
549 | moveFile(dwcFile, "0:/sys/DuetWebControl.bin", function() {
550 | // Initiate firmware updates
551 | var sParam = sParams.join(":");
552 | sendGCode("M997 S" + sParam);
553 |
554 | // Resume the status update loop
555 | startUpdates();
556 | }, startUpdates);
557 | }, startUpdates);
558 | }, startUpdates);
559 | }
560 |
561 | function startFirmwareUpdate() {
562 | // Stop status updates so the user doesn't see any potential error messages
563 | stopUpdates();
564 |
565 | // The filename is hardcoded in the firmware binary, so try to rename the uploaded file first
566 | moveFile("0:/sys/" + uploadFirmwareFile, "0:/sys/" + targetFirmwareFileName + ".bin", function() {
567 | // Rename succeeded and flashing can be performed now
568 | sendGCode("M997 S0");
569 | startUpdates();
570 | }, startUpdates);
571 | }
572 |
573 | function startDWSUpdate() {
574 | // Stop status updates so the user doesn't see any potential error messages
575 | stopUpdates();
576 |
577 | // We prefer DWSS over DWS. Move it into place and start the update
578 | if (uploadDWSSFile != undefined) {
579 | moveFile("0:/sys/" + uploadDWSSFile, "0:/sys/DuetWiFiServer.bin", function() {
580 | // Rename succeeded and flashing can be performed now
581 | sendGCode("M997 S1");
582 | startUpdates();
583 | }, startUpdates);
584 | } else {
585 | moveFile("0:/sys/" + uploadDWSFile, "0:/sys/DuetWiFiServer.bin", function() {
586 | // Rename succeeded and flashing can be performed now
587 | sendGCode("M997 S1");
588 | startUpdates();
589 | }, startUpdates);
590 | }
591 | }
592 |
593 | function startDWCUpdate() {
594 | // Stop status updates so the user doesn't see any potential error messages
595 | stopUpdates();
596 |
597 | // The filename is hardcoded in the firmware binary, so try to rename the uploaded file first
598 | moveFile("0:/sys/" + uploadDWCFile, "0:/sys/DuetWebControl.bin", function() {
599 | // Rename succeeded and flashing can be performed now
600 | sendGCode("M997 S2");
601 | startUpdates();
602 | }, startUpdates);
603 | }
604 |
605 |
606 | /* Upload events */
607 |
608 | $("#btn_cancel_upload").click(function() {
609 | cancelUpload();
610 | });
611 |
612 | $(".btn-upload").click(function(e) {
613 | if (!$(this).is(".disabled")) {
614 | var type = $(this).data("type"), filter = "";
615 | if (type == "print" || type == "gcode") {
616 | filter = ".g,.gcode,.gc,.gco,.nc,.ngc,.tap";
617 | } else if (type == "filament") {
618 | filter = ".zip";
619 | } else if (type == "generic") {
620 | filter=".zip,.bin,.csv,.g,.json,.htm,.html,.ico,.xml,.css,.map,.js,.ttf,.eot,.svg,.woff,.woff2,.jpeg,.jpg,.png,.gz";
621 | }
622 | $("#input_file_upload").prop("accept", filter).data("type", type).click();
623 | }
624 | e.preventDefault();
625 | });
626 |
627 | ["print", "gcode", "macro", "filament", "generic"].forEach(function(type) {
628 | var child = $(".btn-upload[data-type='" + type + "']");
629 |
630 | // Drag Enter
631 | child.on("dragover", function(e) {
632 | $(this).removeClass($(this).data("style")).addClass("btn-success");
633 | e.preventDefault();
634 | e.stopPropagation();
635 | });
636 |
637 | // Drag Leave
638 | child.on("dragleave", function(e) {
639 | $(this).removeClass("btn-success").addClass($(this).data("style"));
640 | e.preventDefault();
641 | e.stopPropagation();
642 | });
643 |
644 | // Drop
645 | child.on("drop", function(e) {
646 | $(this).removeClass("btn-success").addClass($(this).data("style"));
647 | e.preventDefault();
648 | e.stopPropagation();
649 |
650 | var files = e.originalEvent.dataTransfer.files;
651 | if (!$(this).hasClass("disabled") && files != null && files.length > 0) {
652 | // Start new file upload
653 | startUpload($(this).data("type"), files, false);
654 | }
655 | });
656 | });
657 |
658 | $("#input_file_upload").change(function(e) {
659 | if (this.files.length > 0) {
660 | // For POST uploads we need file blobs
661 | startUpload($(this).data("type"), this.files, false);
662 | }
663 | });
664 |
665 | $('#modal_upload').on('hidden.bs.modal', function (e) {
666 | if (uploadFilesSkipped) {
667 | showMessage("warning", T("Warning"), T("Some files were not uploaded because they were not suitable for your board."));
668 | }
669 | })
670 |
--------------------------------------------------------------------------------
/core/css/defaults.css:
--------------------------------------------------------------------------------
1 | /* Generic classes */
2 |
3 | body {
4 | min-width: 465px;
5 | width: 100%;
6 | height: 100%;
7 | }
8 |
9 | dl {
10 | margin-bottom: 0px;
11 | }
12 |
13 | dd {
14 | padding-bottom: 6px;
15 | }
16 | dd:last-child {
17 | padding-bottom: 0px;
18 | }
19 |
20 | hr {
21 | margin-top: 5px;
22 | margin-bottom: 5px;
23 | }
24 |
25 | @-moz-document url-prefix() {
26 | input[type="checkbox"],
27 | input[type="radio"] {
28 | margin-top: 0px;
29 | }
30 | }
31 |
32 | input::-webkit-outer-spin-button,
33 | input::-webkit-inner-spin-button {
34 | -webkit-appearance: none;
35 | -moz-appearance: none;
36 | margin: 0;
37 | }
38 | input[type="number"] {
39 | -moz-appearance: textfield;
40 | }
41 |
42 | label {
43 | font-weight: inherit;
44 | }
45 |
46 | #main_content {
47 | background-color: inherit;
48 | }
49 |
50 | .no-horizontal-padding {
51 | padding-left: 0px !important;
52 | padding-right: 0px !important;
53 | }
54 |
55 | .panel-chart {
56 | padding-top: 0px;
57 | padding-bottom: 15px;
58 | padding-left: 15px;
59 | padding-right: 5px;
60 | }
61 |
62 | .panel-heading {
63 | padding-top: 3px;
64 | padding-bottom: 3px;
65 | }
66 | .panel-heading span {
67 | font-size: 14px;
68 | }
69 |
70 | .padding-small {
71 | padding-left: 8px !important;
72 | padding-right: 8px !important;
73 | }
74 |
75 | .slider .slider-handle {
76 | background-image: linear-gradient(to bottom, #337ab7 0, #2e6da4 100%);
77 | }
78 |
79 | .slider .tooltip-arrow {
80 | border-top-color: #d9edf7 !important;
81 | }
82 |
83 | .slider .tooltip-inner {
84 | background-color: #d9edf7;
85 | color: #31708f;
86 | }
87 |
88 | .slider.slider-horizontal {
89 | margin-left:12px;
90 | margin-right: 12px;
91 | margin-top: 32px;
92 | margin-bottom: 6px;
93 | width: calc(100% - 20px) !important;
94 | }
95 |
96 | .table-description {
97 | width: 100%;
98 | }
99 |
100 | .table-description > tbody > tr > td {
101 | width: 100%;
102 | }
103 |
104 | .table-description > tbody > tr > td:first-child {
105 | padding-right: 12px;
106 | text-align: right;
107 | width: auto;
108 | white-space: nowrap;
109 | }
110 |
111 | .table-description > tbody > tr:first-child td {
112 | padding-top: 0px;
113 | }
114 |
115 | .tooltip {
116 | z-index: inherit !important;
117 | }
118 |
119 |
120 | /* Navigation bar */
121 |
122 | #btn_toggle_sidebar {
123 | margin-left: 15px;
124 | }
125 |
126 | .brand-sm {
127 | display: inline-block !important;
128 | }
129 | .brand-lg {
130 | display: none !important;
131 | }
132 |
133 | .gcode {
134 | display: inline;
135 | padding-left: 12px !important;
136 | padding-right: 8px !important;
137 | vertical-align: middle;
138 | white-space: nowrap;
139 | }
140 |
141 | .gcode span {
142 | display: inline-block;
143 | vertical-align: middle;
144 | }
145 |
146 | .gcode span.label {
147 | display: inline-block;
148 | margin-left: 15px;
149 | }
150 |
151 | /* There appears to be no easy way to right-align list item labels, so I use JS for now
152 | * If anyone has a better solution for this, please implement it.
153 | */
154 | .gcode-float span.label {
155 | position: absolute;
156 | right: 9px;
157 | margin-top: 3px;
158 | }
159 |
160 | .navbar-brand {
161 | float: none;
162 | font-size: 20px;
163 | margin-left: 0px !important;
164 | padding-left: 3px !important;
165 | padding-right: 3px !important;
166 | padding-top: 15px !important;
167 | }
168 |
169 | .navbar-brand > abbr {
170 | border: 0px;
171 | margin-left: 9px;
172 | }
173 | .navbar-brand > abbr > span {
174 | color: #F0AD4E;
175 | }
176 |
177 | .navbar-default {
178 | margin-bottom: 0px;
179 | }
180 |
181 | .navbar-collapse {
182 | padding-left: 0px;
183 | }
184 |
185 | .navbar-label {
186 | margin-left: 0px !important;
187 | margin-right: 15px !important;
188 | margin-top: 17px;
189 | margin-bottom: 13px;
190 | }
191 |
192 | .navbar-table td {
193 | padding-left: 0px !important;
194 | padding-right: 0px !important;
195 | }
196 |
197 | .navbar-table {
198 | margin-bottom: 0px;
199 | }
200 |
201 | .ul-bed-temp a,
202 | .ul-active-temp a,
203 | .ul-standby-temp a {
204 | text-align: right;
205 | }
206 |
207 | .ul-bed-temp,
208 | .ul-active-temp,
209 | .ul-standby-temp {
210 | min-width: initial;
211 | }
212 |
213 |
214 | /* Heater and Status elements */
215 |
216 | #chart_temp {
217 | min-height: 50px;
218 | min-width: 50px;
219 | height: 200px;
220 | }
221 |
222 | #div_tools_heaters input[type="number"] {
223 | min-width: 50px;
224 | }
225 |
226 | #div_temp_chart {
227 | padding-left: 0px;
228 | padding-right: 0px;
229 | }
230 |
231 | #table_extruder_positions td {
232 | white-space: nowrap;
233 | }
234 |
235 | .input-td {
236 | padding-top: 6px !important;
237 | padding-bottom: 6px !important;
238 | }
239 |
240 | .panel-status {
241 | margin-bottom: 12px;
242 | }
243 |
244 | .probe-slow-down {
245 | color: #FFFFE0;
246 | }
247 |
248 | .probe-trigger {
249 | color: #FFF0F0;
250 | }
251 |
252 | #div_info_panels {
253 | background-color: #FFFFFF;
254 | padding-top: 12px;
255 | }
256 |
257 | .span-collapse {
258 | cursor: pointer;
259 | }
260 |
261 | .table-centered-cells th, .table-centered-cells td {
262 | text-align: center;
263 | vertical-align: middle !important;
264 | }
265 |
266 | #table_heaters input[type="number"] {
267 | min: 0;
268 | max: 300;
269 | }
270 | #input_temp_bed {
271 | max: 160;
272 | }
273 |
274 | .table-fan-control {
275 | width: 100%;
276 | }
277 |
278 | .table-fan-control tr > td:nth-child(2) {
279 | padding-right: 15px;
280 | }
281 |
282 | .table-fan-control tr:first-child > td:last-child {
283 | width: 100%;
284 | }
285 |
286 | #table_tools tr:first-child > th {
287 | width: 20%;
288 | }
289 | #table_heaters tr:first-child > th {
290 | width: 25%;
291 | }
292 |
293 | #table_tools tr > th:first-child,
294 | #table_heaters tr > th:first-child {
295 | padding-top: 2px;
296 | padding-bottom: 3px;
297 | }
298 | #table_tools tr > th > *,
299 | #table_heaters tr > th > * {
300 | display: block;
301 | }
302 | #table_tools tr > th > span:last-child,
303 | #table_heaters tr > th > span:last-child {
304 | font-size: 11px;
305 | }
306 |
307 | .table-status {
308 | margin: 0px !important;
309 | table-layout: fixed;
310 | }
311 | div.panel table.table-status:not(:last-child)
312 | {
313 | border-bottom: 1px solid #ddd;
314 | }
315 |
316 | .th-status {
317 | width: 100px;
318 | }
319 |
320 | .heater-0 {
321 | color: #0000FF !important;
322 | }
323 | .heater-1 {
324 | color: #FF0000 !important;
325 | }
326 | .heater-2 {
327 | color: #00DD00 !important;
328 | }
329 | .heater-3 {
330 | color: #FFA000 !important;
331 | }
332 | .heater-4 {
333 | color: #FF00FF !important;
334 | }
335 | .heater-5 {
336 | color: #337AB7 !important;
337 | }
338 | .heater-6 {
339 | color: #000000 !important;
340 | }
341 | .heater-7 {
342 | color: #E0E000 !important;
343 | }
344 | .chamber {
345 | color: #00DCDC !important;
346 | }
347 | .cabinet {
348 | color: #8b1892 !important;
349 | }
350 | .temp-sensor-0 {
351 | color: #AEAEAE !important;
352 | }
353 | .temp-sensor-1 {
354 | color: #BC0000 !important;
355 | }
356 | .temp-sensor-2 {
357 | color: #00CB00 !important;
358 | }
359 | .temp-sensor-3 {
360 | color: #0000DC !important;
361 | }
362 | .temp-sensor-4 {
363 | color: #FEABEF !important;
364 | }
365 | .temp-sensor-5 {
366 | color: #A0A000 !important;
367 | }
368 | .temp-sensor-6 {
369 | color: #DDDD00 !important;
370 | }
371 | .temp-sensor-7 {
372 | color: #00BDBD !important;
373 | }
374 | .temp-sensor-8 {
375 | color: #CCBBAA !important;
376 | }
377 | .temp-sensor-9 {
378 | color: #AA00AA !important;
379 | }
380 |
381 |
382 | /* Static sidebar (for desktops) */
383 |
384 | .sidebar {
385 | padding-left: 20px;
386 | padding-right: 20px;
387 | padding-top: 0px;
388 | background-color: #f5f5f5;
389 | border-right: 1px solid #bfbfbf;
390 | border-top: 1px solid #bfbfbf;
391 | }
392 |
393 | .sidebar-continuation {
394 | border-top: 0px;
395 | min-height: 100vh;
396 | height: 100%;
397 | position: fixed;
398 | top: 0px;
399 | bottom: 0px;
400 | z-index: -9999;
401 | }
402 |
403 | .nav-sidebar {
404 | margin-right: -20px;
405 | margin-bottom: 10px;
406 | margin-left: -20px;
407 | }
408 |
409 | .nav-sidebar > li > a {
410 | font-size: 15px;
411 | padding-right: 20px;
412 | padding-left: 20px;
413 | white-space: nowrap;
414 | }
415 |
416 | .nav-sidebar:last-child {
417 | margin-bottom: 0px;
418 | }
419 |
420 | .nav-sidebar .glyphicon {
421 | padding-right: 3px;
422 | }
423 |
424 | .nav-sidebar > .active > a,
425 | .nav-sidebar > .active > a:hover,
426 | .nav-sidebar > .active > a:focus {
427 | color: #fff;
428 | background-color: #428bca;
429 | }
430 |
431 | .nav-sidebar .span-refresh-scans,
432 | .nav-sidebar .span-refresh-files,
433 | .nav-sidebar .span-refresh-macros,
434 | .nav-sidebar .span-refresh-filaments {
435 | position: absolute;
436 | right: 15px;
437 | top: 14px;
438 | }
439 |
440 |
441 | /* Dynamic sidebar (for mobile devices) */
442 |
443 | .slideout-menu {
444 | position: fixed;
445 | left: 0;
446 | top: 0;
447 | bottom: 0;
448 | right: 0;
449 | z-index: 0;
450 | width: 220px;
451 | overflow-y: scroll;
452 | -webkit-overflow-scrolling: touch;
453 | display: none;
454 |
455 | background-color: #f5f5f5;
456 | padding-left: 15px;
457 | }
458 |
459 | .slideout-menu .nav-sidebar {
460 | margin-right: 0px;
461 | }
462 |
463 | .slideout-panel {
464 | position: relative;
465 | z-index: 1;
466 | will-change: transform;
467 | }
468 |
469 | .slideout-open,
470 | .slideout-open body,
471 | .slideout-open .slideout-panel {
472 | overflow: hidden;
473 | }
474 |
475 | .slideout-open .slideout-menu {
476 | display: block;
477 | }
478 |
479 | #section_actions {
480 | margin-top: 15px;
481 | }
482 |
483 | #section_actions,
484 | #section_app {
485 | margin-right: 15px;
486 | }
487 |
488 | #section_actions > button,
489 | #section_app > button {
490 | width: 100%;
491 | margin-bottom: 15px;
492 | }
493 |
494 | #section_actions > :last-child {
495 | margin-bottom: 30px;
496 | }
497 |
498 | #section_navigation > :last-child {
499 | margin-bottom: 15px;
500 | }
501 |
502 | #section_app {
503 | position: absolute;
504 | bottom: 0px;
505 | width: 190px;
506 | }
507 |
508 |
509 | /* Main content */
510 |
511 | #div_content .panel-body {
512 | padding: 9px !important;
513 | }
514 |
515 | #div_content .panel-heading {
516 | padding-left: 9px !important;
517 | padding-right: 9px !important;
518 | text-align: center;
519 | }
520 |
521 | .page {
522 | display: none;
523 | }
524 |
525 | .page.active {
526 | display: block !important;
527 | }
528 |
529 | #panel_macro_buttons h4 {
530 | margin-top: 15px;
531 | margin-bottom: 0px;
532 | }
533 |
534 |
535 | /* Control page */
536 |
537 | .atx-control div.btn-group {
538 | margin-bottom: 9px;
539 | }
540 |
541 | #btn_homeall {
542 | min-width: 82px;
543 | }
544 |
545 | .btn-group-vertical-justified {
546 | display: inline-block;
547 | width: 100%;
548 | }
549 |
550 | #page_control > div > div.col-right:last-child {
551 | float: right;
552 | }
553 |
554 | .home-warning {
555 | margin-top: -11px;
556 | padding-top: 6px;
557 | padding-bottom: 6px;
558 | }
559 |
560 | #panel_control_misc {
561 | padding-bottom: 4px;
562 | }
563 | #panel_control_misc .btn-group label {
564 | padding-left: 0px;
565 | padding-right: 0px;
566 | }
567 |
568 | #panel_extrude button {
569 | padding-left: 6px !important;
570 | padding-right: 6px !important;
571 | }
572 |
573 | #panel_extrude .btn-group label.btn {
574 | padding-left: 0px !important;
575 | padding-right: 0px !important;
576 | }
577 |
578 | #panel_extrude div.panel-body > div:first-child {
579 | padding-bottom: 9px;
580 | }
581 |
582 | #mobile_home_buttons,
583 | #mobile_extra_home_buttons {
584 | padding-bottom: 9px;
585 | }
586 |
587 | #panel_macro_buttons button {
588 | font-size: 14px;
589 | }
590 |
591 | #panel_macro_buttons li {
592 | text-align: center;
593 | }
594 |
595 | .table-move .btn-group {
596 | padding-bottom: 9px;
597 | }
598 |
599 | .table-move a {
600 | padding-left: 6px;
601 | padding-right: 6px;
602 | }
603 | .table-move a.btn-home {
604 | padding-left: 12px;
605 | padding-right: 12px;
606 | }
607 |
608 | .table-move td {
609 | padding-right: 12px;
610 | width: 50%;
611 | }
612 |
613 | .table-move tr > td:first-child {
614 | width: auto;
615 | }
616 |
617 | .table-move tr > td:last-child {
618 | padding-right: 0px;
619 | }
620 |
621 | #table_move_head tr:last-child div {
622 | padding-bottom: 0px;
623 | }
624 |
625 | /* Calibration page (OEM) */
626 |
627 | #table_calibration_tools > thead > tr > th:not(:first-child) {
628 | text-align: center;
629 | }
630 |
631 | #table_calibration_tools > thead > tr > th:last-child {
632 | width: 1%;
633 | }
634 |
635 | #table_calibration_tools > tbody > tr > td:first-child {
636 | vertical-align: middle;
637 | }
638 |
639 | #table_calibration_tools > tbody > tr > td:not(:first-child) {
640 | text-align: center;
641 | }
642 |
643 | #table_calibration_tools > tbody > tr > td:not(:first-child) > span {
644 | margin-left: 15px;
645 | margin-right: 15px;
646 | }
647 |
648 | /* Print status page */
649 |
650 | #img_webcam {
651 | width: 100%;
652 | }
653 |
654 | #ifm_webcam {
655 | width: 100%;
656 | border: 0px;
657 | }
658 |
659 | #page_print .checkbox {
660 | margin-top: 0px;
661 | margin-bottom: 0px;
662 | }
663 |
664 | #page_print .col-left {
665 | padding-right: 0px;
666 | }
667 |
668 | #page_print .slider-container {
669 | padding-right: 12px;
670 | }
671 |
672 | #page_print .progress {
673 | margin-bottom: 9px;
674 | }
675 |
676 | #progress {
677 | width: 100%;
678 | font-weight: bold;
679 | }
680 |
681 | #label_babystepping {
682 | width: 100%;
683 | text-align: center;
684 | margin-bottom: 0px;
685 | }
686 |
687 | .layer-done-animation {
688 | background-color: #D9EDF7 !important;
689 | -webkit-transition: background-color 500ms linear;
690 | -moz-transition: background-color 500ms linear;
691 | -o-transition: background-color 500ms linear;
692 | -ms-transition: background-color 500ms linear;
693 | transition: background-color 500ms linear;
694 | }
695 |
696 | #layer_tooltip {
697 | position: absolute;
698 | display: none;
699 | border: 1px solid #fdd;
700 | padding: 2px;
701 | background-color: #fee;
702 | opacity: 0.80;
703 | z-index: 1;
704 | }
705 |
706 | #panel_print_control > div > div.btn-group-justified {
707 | padding-bottom: 6px;
708 | }
709 |
710 | #panel_print_control > div > div.btn-group-justified:last-child {
711 | padding-bottom: 0px;
712 | }
713 |
714 | #panel_print_control .btn {
715 | padding-left: 0px;
716 | padding-right: 0px;
717 | }
718 |
719 | #btn_baby_up,
720 | #btn_baby_down {
721 | word-wrap: break-word;
722 | white-space: normal;
723 | }
724 |
725 | #div_cancel,
726 | #panel_print_info .row > div:not(:first-child) {
727 | padding-left: 6px;
728 | }
729 |
730 | #panel_print_info table {
731 | table-layout: fixed;
732 | }
733 | #panel_print_info table th,
734 | #panel_print_info table td {
735 | text-align: center;
736 | }
737 |
738 | #chart_print {
739 | min-height: 50px;
740 | min-width: 50px;
741 | height: 225px;
742 | }
743 |
744 | .chart-print-line {
745 | color: #EDC240;
746 | }
747 | #span_babystepping {
748 | white-space: nowrap;
749 | }
750 |
751 | @media (min-width: 970px) {
752 | #span_progress_right {
753 | float: right;
754 | }
755 | }
756 |
757 | #table_estimations {
758 | table-layout: fixed;
759 | }
760 |
761 | /* Scanner */
762 |
763 | #table_scan_files_directory tr > td {
764 | width: auto;
765 | padding-right: 6px;
766 | }
767 |
768 | #table_scan_files_directory tr > td:nth-child(2) {
769 | padding-left: 0px;
770 | }
771 |
772 | #table_scan_files_directory tr > td:nth-child(3) {
773 | width: 100%;
774 | padding-left: 0px;
775 | padding-right: 0px;
776 | }
777 |
778 | #table_scan_files_directory tr > td:last-child {
779 | padding-left: 6px;
780 | padding-right: 0px;
781 | }
782 |
783 | #table_scan_files button > span {
784 | padding-top: 6px;
785 | padding-bottom: 6px;
786 | }
787 |
788 | #table_scan_files tr > td {
789 | padding-top: 4px !important;
790 | padding-bottom: 4px !important;
791 | vertical-align: middle !important;
792 | }
793 |
794 |
795 | #table_scan_files tr > td:nth-child(2) {
796 | border-right: 0px;
797 | border-left: 0px;
798 | width: 1%;
799 | }
800 |
801 | #table_scan_files th:first-child {
802 | width: 97px;
803 | }
804 |
805 |
806 | /* G-Code console page */
807 |
808 | #console_log {
809 | padding-top: 0px;
810 | background-color: #eee;
811 | border: 1px solid #bbb;
812 | margin-bottom: 15px;
813 | }
814 |
815 | #console_log div.row {
816 | padding-top: 3px !important;
817 | padding-bottom: 3px;
818 | }
819 |
820 | #page_console form > div {
821 | padding-left: 0px;
822 | padding-right: 9px;
823 | }
824 |
825 | #page_console form > div:first-child {
826 | padding-left: 15px;
827 | }
828 |
829 | #page_console form > div:last-child {
830 | padding-left: 0px;
831 | padding-right: 15px;
832 | }
833 |
834 | #page_console button[type='submit'] {
835 | padding-left: 6px;
836 | padding-right: 6px;
837 | }
838 |
839 | #page_console div.row:last-child {
840 | padding-top: 9px;
841 | }
842 |
843 | /* G-Code Files */
844 |
845 | .btn-delete-directory {
846 | float: right;
847 | margin-right: 8px;
848 | }
849 |
850 | #btn_new_gcode_directory span {
851 | padding-right: 6px;
852 | }
853 |
854 | .breadcrumb-directory {
855 | margin-bottom: 0px !important;
856 | }
857 |
858 | .breadcrumb-directory > li:first-child span.glyphicon {
859 | padding-right: 3px;
860 | }
861 |
862 | #page_files h1 {
863 | margin-bottom: 20px;
864 | }
865 |
866 | #ol_gcode_directory {
867 | margin-bottom: 9px;
868 | }
869 |
870 | #ol_gcode_directory > li.pull-right:before {
871 | content: "";
872 | }
873 |
874 | .table-file-navigation {
875 | width: 100%;
876 | margin-bottom: 9px;
877 | }
878 |
879 | .table-file-navigation td:first-child {
880 | width: 100%;
881 | }
882 |
883 | .table-file-navigation td:not(:first-child) {
884 | padding-left: 6px;
885 | }
886 |
887 | .table-files {
888 | margin-bottom: 15px;
889 | }
890 |
891 | .table-files > tbody > tr > td:first-child > input[type="checkbox"] {
892 | margin-right: 8px;
893 | }
894 |
895 | .table-files tr > td:first-child {
896 | border-right: 0px;
897 | padding-right: 0px;
898 | width: 1%;
899 | }
900 |
901 | .table-files a > span.glyphicon {
902 | padding-right: 6px;
903 | }
904 |
905 | #table_gcode_files_directory tr > td:first-child {
906 | width: auto;
907 | padding-right: 6px;
908 | }
909 |
910 | #table_gcode_files_directory tr > td:nth-child(2) {
911 | padding-left: 0px;
912 | width: 100%;
913 | }
914 |
915 | /* Macros Page */
916 |
917 | #btn_new_macro_directory span {
918 | padding-right: 6px;
919 | }
920 |
921 | #page_macros ol .pull-right:before {
922 | content: "";
923 | }
924 |
925 | /* Settings Page */
926 |
927 | #btn_clear_cache {
928 | width: 100%;
929 | }
930 |
931 | #page_tools button,
932 | #btn_add_gcode,
933 | .machine-button {
934 | margin-top: 9px;
935 | }
936 |
937 | #btn_add_tool {
938 | margin-top: 25px !important;
939 | width: 100%;
940 | }
941 |
942 | .machine-button {
943 | padding-left: 4px;
944 | padding-right: 4px;
945 | width: 100%;
946 | }
947 |
948 | #check_heaters > label:first-child {
949 | padding: 6px;
950 | }
951 |
952 | #div_fan_settings div.checkbox {
953 | margin-top: 0px;
954 | margin-bottom: 0px;
955 | }
956 |
957 | #div_fan_settings > div {
958 | padding-right: 0px;
959 | }
960 |
961 | label[for="dropdown_language"] {
962 | margin-right: 6px;
963 | }
964 |
965 | #frm_settings li.open li > a {
966 | height: 40px;
967 | padding-top: 10px;
968 | }
969 |
970 | #panel_head_temps > div > label:first-child {
971 | display: block;
972 | }
973 | #panel_head_temps > div > div.radio {
974 | display: inline-block;
975 | margin: 0px;
976 | margin-bottom: 9px;
977 | width: 49%;
978 | }
979 |
980 | #panel_tool_changes div.checkbox {
981 | margin-top: 0px;
982 | margin-bottom: 0px;
983 | }
984 |
985 | #software_info {
986 | margin-bottom: 7px;
987 | }
988 | #software_info tr * {
989 | padding-bottom: 8px;
990 | }
991 |
992 | #software_info tr > td {
993 | padding-left: 15px;
994 | }
995 |
996 | #p_uploadinfo {
997 | margin-top: 15px;
998 | margin-bottom: 0px;
999 | }
1000 |
1001 | #page_settings {
1002 | padding-bottom: 15px;
1003 | }
1004 |
1005 | #page_settings div.panel-body > div.checkbox:first-child {
1006 | margin-top: 0px;
1007 | }
1008 |
1009 | #page_settings div.panel-body > div.checkbox:last-child {
1010 | margin-bottom: 0px;
1011 | }
1012 |
1013 | #page_settings div.panel-body > label {
1014 | margin-bottom: 3px;
1015 | }
1016 | #page_settings div.panel-body > label:not(:first-child) {
1017 | margin-top: 6px;
1018 | }
1019 |
1020 | .tab-content {
1021 | border: 1px solid #ddd;
1022 | border-bottom-width: 1px;
1023 | border-left-width: 1px;
1024 | border-right-width: 1px;
1025 | border-top-width: 0px;
1026 | padding: 15px;
1027 | padding-bottom: 0px;
1028 | margin-bottom: 15px;
1029 | }
1030 |
1031 | .table-add-temp td:first-child {
1032 | padding-right: 9px;
1033 | width: 100%;
1034 | }
1035 |
1036 | .table-add-temp .input-group {
1037 | width: 100%;
1038 | }
1039 |
1040 | .table-add-temp td:last-child {
1041 | width: auto;
1042 | vertical-align: bottom;
1043 | }
1044 |
1045 | #table_add_gcode {
1046 | width: 100%;
1047 | }
1048 | #table_gcodes {
1049 | margin-bottom: 0px;
1050 | }
1051 |
1052 | #table_gcodes .label {
1053 | font-size: 95%;
1054 | }
1055 |
1056 | #table_add_gcode td:first-child,
1057 | #table_gcodes > thead > tr > th:first-child {
1058 | width: 35%;
1059 | }
1060 | #table_add_gcode td:first-child {
1061 | padding-right: 9px;
1062 | }
1063 | #table_add_gcode td:nth-child(2),
1064 | #table_gcodes > thead > tr > th:nth-child(2) {
1065 | width: 65%;
1066 | }
1067 | #table_gcodes > thead > tr > th:nth-child(3) {
1068 | width: auto;
1069 | }
1070 | #table_gcodes > tbody > tr > td {
1071 | vertical-align: middle;
1072 | }
1073 | #table_gcodes > tbody > tr > td:last-child {
1074 | padding: 6px;
1075 | }
1076 |
1077 | .temp-list > li {
1078 | line-height: 30px;
1079 | vertical-align: middle;
1080 | padding: 6px;
1081 | }
1082 |
1083 | #page_sysedit {
1084 | margin: -16px -16px;
1085 | }
1086 |
1087 | #page_sysedit h1 {
1088 | margin-bottom: 31px;
1089 | }
1090 |
1091 | #ol_sys_directory {
1092 | margin-left: 1px;
1093 | margin-right: 1px;
1094 | margin-top: 1px;
1095 | margin-bottom: 0px;
1096 | }
1097 |
1098 | #ol_sys_directory .pull-right:before {
1099 | content: "";
1100 | }
1101 |
1102 | #ul_control_dropdown_tools,
1103 | #ul_control_dropdown_heaters {
1104 | padding: 5px;
1105 | }
1106 |
1107 | #ul_control_dropdown_tools > li > a,
1108 | #ul_control_dropdown_heaters > li > a {
1109 | padding-left: 6px;
1110 | padding-right: 6px;
1111 | }
1112 |
1113 | #ul_control_dropdown_tools > li:not(:first-child),
1114 | #ul_control_dropdown_heaters > li:not(:first-child) {
1115 | padding-top: 5px;
1116 | }
1117 |
1118 | /* Modals */
1119 |
1120 | #modal_bed div.modal-footer > .pull-left {
1121 | margin-right: 9px;
1122 | }
1123 |
1124 | #modal_textinput div.input-group {
1125 | width: 100%;
1126 | }
1127 |
1128 | #table_upload_files {
1129 | width: 100%;
1130 | }
1131 | #table_upload_files .progress {
1132 | margin-top: 8px;
1133 | margin-bottom: 8px;
1134 | }
1135 | #table_upload_files td {
1136 | vertical-align: middle;
1137 | }
1138 | #modal_edit > div {
1139 | width: calc(100vw - 60px);
1140 | }
1141 |
1142 | #modal_edit > div > div,
1143 | #modal_bed > div > div {
1144 | height: calc(100vh - 60px);
1145 | }
1146 |
1147 | #text_edit {
1148 | font-family: monospace;
1149 | height: 100%;
1150 | width: 100%;
1151 | resize: none;
1152 | }
1153 |
1154 | #modal_bed > div > div.modal-content {
1155 | min-height: 400px;
1156 | }
1157 |
1158 | #div_visualization {
1159 | margin-bottom: 15px;
1160 | padding-right: 0px;
1161 | }
1162 |
1163 | #div_visualization_placeholder,
1164 | #div_legend {
1165 | margin-bottom: 15px;
1166 | }
1167 |
1168 | #div_visualization_placeholder > div {
1169 | background-color: black;
1170 | color: white;
1171 | }
1172 |
1173 | #div_visualization_placeholder h3 {
1174 | display: inline-block;
1175 | vertical-align: middle;
1176 | }
1177 |
1178 | #div_visualization > div.tooltip {
1179 | pointer-events: none;
1180 | }
1181 |
1182 | #div_legend {
1183 | padding-left: 0px;
1184 | }
1185 |
1186 | #canvas_legend {
1187 | width: 100%;
1188 | }
1189 |
1190 | #row_stats div > p:last-child {
1191 | margin-bottom: 0px;
1192 | }
1193 |
1194 | #btn_top_view {
1195 | margin-top: 9px;
1196 | }
1197 |
1198 | #progress_scan,
1199 | #progress_scan_postprocessing,
1200 | #progress_scan_upload,
1201 | #progress_calibration {
1202 | min-width: 2em;
1203 | }
1204 |
1205 | #modal_start_scan h4:first-child {
1206 | margin-top: 0px;
1207 | }
1208 |
1209 | #modal_start_scan .description {
1210 | margin-left: 20px;
1211 | width: calc(100% - 20px);
1212 | }
1213 |
1214 | #modal_start_scan .description:not(:last-child) {
1215 | margin-bottom: 20px;
1216 | }
1217 |
1218 | #modal_start_scan .description > input,
1219 | #btn_toggle_laser {
1220 | width: 100%;
1221 | }
1222 |
1223 | #modal_messagebox div {
1224 | text-align: center;
1225 | }
1226 |
1227 | #h3_messagebox {
1228 | margin-bottom: 20px;
1229 | }
1230 |
1231 | #div_x_controls,
1232 | #div_y_controls,
1233 | #div_z_controls {
1234 | margin-top: 20px;
1235 | }
1236 |
1237 |
1238 | /* Popovers */
1239 |
1240 | .popover-content {
1241 | padding: 6px;
1242 | }
1243 |
1244 |
1245 | /* Context menus */
1246 |
1247 | #ul_file_contextmenu {
1248 | position: absolute;
1249 | display: none;
1250 | }
1251 |
--------------------------------------------------------------------------------