├── 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' | 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: '' 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('Notify Icon'); 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 | --------------------------------------------------------------------------------