├── core ├── favicon.ico ├── js │ ├── .tern-project │ ├── 3rd-party │ │ ├── jquery.flot.resize.js │ │ ├── piecon.js │ │ ├── FileSaver.js │ │ ├── jquery.flot.dashes.js │ │ ├── jquery.flot.navigate.js │ │ └── bootstrap-notify.js │ ├── i18n.js │ ├── sliders.js │ ├── notify.js │ ├── charts.js │ ├── modals.js │ ├── utils.js │ ├── settings.js │ └── upload.js ├── favicon_green.ico ├── favicon_red.ico ├── fonts │ ├── 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 │ └── bootstrap.theme.css ├── DuetWebControl-1.22.5.zip ├── geti18n.sh ├── .gitattributes └── README.md /core/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/core/favicon.ico -------------------------------------------------------------------------------- /core/js/.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "loadEagerly": [ 3 | "./3rd-party/*.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /core/favicon_green.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/core/favicon_green.ico -------------------------------------------------------------------------------- /core/favicon_red.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/core/favicon_red.ico -------------------------------------------------------------------------------- /DuetWebControl-1.22.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/DuetWebControl-1.22.5.zip -------------------------------------------------------------------------------- /core/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/core/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /core/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/core/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /core/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/core/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /core/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockryan/DuetWebControl/HEAD/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 | .visible-xl { 3 | display: none !important; 4 | } 5 | 6 | /* Info panels */ 7 | #div_tools_heaters { 8 | padding-left: 0px; 9 | padding-right: 0px; 10 | } 11 | 12 | /* Main content */ 13 | #div_content row > div[class^='col-'] { 14 | padding-left: 8px !important; 15 | padding-right: 8px !important; 16 | } 17 | 18 | /* Control page */ 19 | #div_content .panel-heading { 20 | padding-left: 9px !important; 21 | } 22 | 23 | #page_control .col-right, 24 | #panel_extrude div.panel-body > div:first-child { 25 | padding-left: 0px; 26 | } 27 | 28 | /* Print status page */ 29 | #page_print .col-right { 30 | padding-left: 0px; 31 | } 32 | 33 | /* Settings page */ 34 | #div_add_tool div.row > div > label { 35 | margin-top: 9px; 36 | } 37 | 38 | #page_settings .col-left { 39 | padding-right: 0px; 40 | } 41 | 42 | #page_settings .col-right { 43 | padding-left: 0px; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/css/layout-sm.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 768px) and (max-width: 991px) { 2 | .visible-xl { 3 | display: none !important; 4 | } 5 | 6 | /* Navigation bar */ 7 | .navbar-checkbox { 8 | margin-left: 15px; 9 | margin-top: 8px; 10 | margin-bottom: 8px; 11 | } 12 | 13 | /* Main content */ 14 | .content-collapsed-padding { 15 | padding-top: 12px; 16 | } 17 | 18 | #div_content div.row > div[class^='col-'] { 19 | padding-left: 8px !important; 20 | padding-right: 8px !important; 21 | } 22 | 23 | /* Static sidebar */ 24 | .nav-sidebar > li > a { 25 | padding-left: 15px; 26 | } 27 | 28 | /* Control page */ 29 | #panel_head_movement > div.panel-heading { 30 | text-align: left; 31 | } 32 | 33 | /* Scanner page */ 34 | #table_scan_files th:first-child { 35 | width: 100px; 36 | } 37 | 38 | /* G-Code files page */ 39 | #page_files { 40 | padding-bottom: 15px; 41 | } 42 | 43 | /* Macros page */ 44 | #page_macros { 45 | padding-bottom: 15px; 46 | } 47 | 48 | /* Settings page */ 49 | #div_add_tool div.row > div > label { 50 | margin-top: 9px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/css/layout-xs.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 767px) { 2 | .visible-xl { 3 | display: none !important; 4 | } 5 | 6 | /* Navigation bar */ 7 | .navbar-checkbox { 8 | float: right; 9 | margin-right: 15px; 10 | margin-top: 8px; 11 | margin-bottom: 8px; 12 | } 13 | 14 | .navbar span.label-status { 15 | float: right; 16 | } 17 | 18 | /* Main content */ 19 | #div_content { 20 | padding-top: 12px; 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 | /* Print Status */ 43 | #btn_baby_up, 44 | #btn_baby_down { 45 | word-wrap: break-word; 46 | white-space: normal; 47 | } 48 | 49 | /* Scanner page */ 50 | #table_scan_files th:first-child { 51 | width: 100px; 52 | } 53 | 54 | /* G-Code console page */ 55 | #page_console div.well { 56 | padding-left: 15px; 57 | padding-right: 15px; 58 | } 59 | 60 | /* G-Code files page */ 61 | #page_files { 62 | padding-bottom: 15px; 63 | } 64 | 65 | /* Macros page */ 66 | #page_macros { 67 | padding-bottom: 15px; 68 | } 69 | 70 | /* Settings page */ 71 | #btn_language, 72 | #btn_theme { 73 | width: 100%; 74 | } 75 | 76 | #div_add_tool div.row > div > label { 77 | margin-top: 9px; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /core/css/layout-lg.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 1200px) and (max-width: 1600px) { 2 | .visible-xl { 3 | display: none !important; 4 | } 5 | } 6 | 7 | @media (min-width: 1200px) { 8 | /* Workarounds (may be dropped in future releases) */ 9 | .btn-group-justified .visible-lg { 10 | display: table-cell !important; 11 | } 12 | 13 | /* Navigation bar */ 14 | .brand-sm { 15 | display: none !important; 16 | } 17 | .brand-lg { 18 | display: inline-block !important; 19 | } 20 | 21 | /* Info panels */ 22 | #div_tools_heaters { 23 | padding-left: 0px; 24 | padding-right: 0px; 25 | } 26 | 27 | /* Main content */ 28 | #div_content row > div[class^='col-'] { 29 | padding-left: 8px !important; 30 | padding-right: 8px !important; 31 | } 32 | 33 | /* Control page */ 34 | #div_content .panel-heading { 35 | padding-left: 9px !important; 36 | } 37 | 38 | #page_control .col-right, 39 | #panel_extrude div.panel-body > div:first-child { 40 | padding-left: 0px; 41 | } 42 | 43 | #panel_head_movement .panel-heading, 44 | #panel_macro_buttons .panel-heading { 45 | padding-top: 6px; 46 | padding-bottom: 6px; 47 | } 48 | 49 | /* Print status page */ 50 | #btn_baby_up, 51 | #btn_baby_down { 52 | word-wrap: break-word; 53 | white-space: normal; 54 | } 55 | 56 | #page_print .col-right { 57 | padding-left: 0px; 58 | } 59 | 60 | /* Settings page */ 61 | #btn_language, 62 | #btn_theme { 63 | margin-left: 9px; 64 | } 65 | 66 | #dropdown_language, 67 | #dropdown_theme { 68 | display: inline-block; 69 | } 70 | 71 | #page_settings .col-left { 72 | padding-right: 0px; 73 | } 74 | 75 | #page_settings .col-right { 76 | padding-left: 0px; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 = undefined; 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, select > option"), "textContent"); 57 | translateEntries(root, $("th, td, dt"), "textContent"); 58 | translateEntries(root, $("h1, h4, label, a, #div_content ol > li:first-child"), "textContent"); 59 | translateEntries(root, $("#ol_gcode_directory > li:last-child, #ol_macro_directory > li:last-child"), "textContent"); 60 | translateEntries(root, $("input[type='text']"), "placeholder"); 61 | translateEntries(root, $("a, abbr, button, label, li, #chart_temp, input, td"), "title"); 62 | translateEntries(root, $("img"), "alt"); 63 | 64 | // This doesn't work with data attributes though 65 | $("button[data-content]").each(function() { 66 | $(this).attr("data-content", T($(this).attr("data-content"))); 67 | }); 68 | 69 | // Update SD Card button caption 70 | $("#btn_volume > span.content").text(T("SD Card {0}", currentGCodeVolume)); 71 | 72 | // Set new language on Settings page 73 | $("#btn_language").data("language", settings.language).children("span:first-child").text(root.attributes["name"].value); 74 | $("html").attr("lang", settings.language); 75 | } 76 | } 77 | } 78 | 79 | function translateEntries(root, entries, key) { 80 | var doNodeCheck = (key == "textContent"); 81 | $.each(entries, function() { 82 | // If this node has no children, we can safely use it 83 | if (!doNodeCheck || this.childNodes.length < 2) { 84 | translateEntry(root, this, key); 85 | // Otherwise we need to check for non-empty text nodes 86 | } else { 87 | for(var i = 0; i < this.childNodes.length; i++) { 88 | var val = this.childNodes[i][key]; 89 | if (this.childNodes[i].nodeType == 3 && val != undefined && this.childNodes[i].childNodes.length == 0 && val.trim().length > 0) { 90 | translateEntry(root, this.childNodes[i], key); 91 | } 92 | } 93 | } 94 | }); 95 | } 96 | 97 | function translateEntry(root, item, key) { 98 | if (item != undefined) { 99 | var originalText = item[key]; 100 | if (originalText != undefined && originalText.trim() != "") { 101 | var text = originalText.trim(); 102 | 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/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-2018 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 = undefined, speedSliderActive = false, extrSliderActive = false; 11 | var overriddenFanValues = [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]; // this must hold maxFans items 12 | 13 | 14 | /* Fan Control */ 15 | 16 | for(var fan = -1; fan < maxFans; fan++) { 17 | var fanID = (fan == -1) ? "tool" : fan; 18 | $('#slider_fan_control_' + fanID).slider({ 19 | enabled: false, 20 | id: "fan_control_" + fanID, 21 | min: 0, 22 | max: 100, 23 | step: 1, 24 | value: 35, 25 | tooltip: "always", 26 | formatter: function(value) { 27 | return value + " %"; 28 | } 29 | }).on("slideStart", function() { 30 | fanSliderActive = $(this).parents("tr").data("fan"); 31 | }).on("slideStop", function(slideEvt) { 32 | if (isConnected && !isNaN(slideEvt.value)) { 33 | var fanID = $(this).parents("tr").data("fan"); 34 | var fanValue = slideEvt.value / 100.0; 35 | if (fanID == "tool") { 36 | // Generic tool fan is selected 37 | sendGCode("M106 S" + fanValue); 38 | } else { 39 | // Specific fan is selected 40 | if (overriddenFanValues[fanID] != undefined) { 41 | overriddenFanValues[fanID] = fanValue; 42 | } 43 | sendGCode("M106 P" + fanID + " S" + fanValue); 44 | } 45 | $("#slider_fan_job_" + fanID).slider("setValue", slideEvt.value); 46 | } 47 | fanSliderActive = undefined; 48 | }); 49 | 50 | $('#slider_fan_job_' + fanID).slider({ 51 | enabled: false, 52 | id: "fan_job_" + fanID, 53 | min: 0, 54 | max: 100, 55 | step: 1, 56 | value: 35, 57 | tooltip: "always", 58 | formatter: function(value) { 59 | return value + " %"; 60 | } 61 | }).on("slideStart", function() { 62 | fanSliderActive = $(this).parents("tr").data("fan"); 63 | }).on("slideStop", function(slideEvt) { 64 | if (isConnected && !isNaN(slideEvt.value)) { 65 | var fanID = $(this).parents("tr").data("fan"); 66 | var fanValue = slideEvt.value / 100.0; 67 | if (fanID == "tool") { 68 | // Generic tool fan is selected 69 | sendGCode("M106 S" + fanValue); 70 | } else { 71 | // Specific fan is selected 72 | if (overriddenFanValues[fanID] != undefined) { 73 | overriddenFanValues[fanID] = fanValue; 74 | } 75 | sendGCode("M106 P" + fanID + " S" + fanValue); 76 | } 77 | $("#slider_fan_control_" + fanID).slider("setValue", slideEvt.value); 78 | } 79 | fanSliderActive = undefined; 80 | }); 81 | } 82 | 83 | function setFanVisibility(fan, visible) { 84 | // set visibility of the corresponding control button 85 | $('.table-fan-control tr[data-fan="' + fan + '"]').toggleClass("hidden", !visible); 86 | 87 | // if this fan's value was being enforced, undo it 88 | setFanOverride(fan, undefined); 89 | } 90 | 91 | function setFanOverride(fan, overriddenValue) { 92 | // set model value 93 | overriddenFanValues[fan] = overriddenValue; 94 | 95 | // update UI 96 | var overridden = (overriddenValue != undefined); 97 | var toggleButton = $('.table-fan-control tr[data-fan="' + fan + '"] button.fan-override'); 98 | toggleButton.toggleClass("btn-primary", overridden).toggleClass("btn-default", !overridden); 99 | toggleButton.toggleClass("active", overridden); 100 | } 101 | 102 | $("button.fan-visibility").click(function() { 103 | var fan = $(this).parents("tr").data("fan"); 104 | setFanDisplayed(fan, !$(this).hasClass("active")); 105 | }); 106 | 107 | $("button.fan-override").click(function() { 108 | if ($(this).hasClass("disabled")) { 109 | return; 110 | } 111 | 112 | var fan = $(this).parents("tr").data("fan"); 113 | if (overriddenFanValues[fan] == undefined) { 114 | var sliderValue = $(this).parents("tr").find(".fan-slider > input").slider("getValue"); 115 | setFanOverride(fan, sliderValue / 100.0); 116 | } else { 117 | setFanOverride(fan, undefined); 118 | } 119 | }); 120 | 121 | function loadFanVisibility() { 122 | var fanVisibility = getLocalSetting("fanVisibility", null); 123 | if (fanVisibility != null) { 124 | for(var fan = 0; fan <= maxFans; fan++) { 125 | if (fan == 0) { 126 | setFanDisplayed("tool", (fanVisibility & 1) != 0); 127 | } else { 128 | setFanDisplayed(fan - 1, (fanVisibility & (1 << fan)) != 0); 129 | } 130 | } 131 | } 132 | } 133 | 134 | function setFanDisplayed(fan, show) { 135 | if (show) { 136 | $(".table-fan-control tr[data-fan='" + fan + "'] button.fan-visibility").removeClass("btn-default").addClass("btn-primary active"); 137 | $(".table-fan-control tr[data-fan='" + fan + "'] button.fan-override").toggleClass("disabled", !isConnected); 138 | $(".table-fan-control tr[data-fan='" + fan + "'] > td:last-child").children().removeClass("hidden"); 139 | 140 | // Restore correct value 141 | if (fan != "tool") { 142 | var fanValue = overriddenFanValues[fan]; 143 | if (fanValue == undefined && lastStatusResponse != undefined) { 144 | // this is only called if the firmware reports fan values as an array 145 | fanValue = lastStatusResponse.params.fanPercent[fan] / 100.0; 146 | } 147 | 148 | if (fanValue != undefined) { 149 | $("tr[data-fan='" + fan + "'] .fan-slider > input").slider("setValue", fanValue * 100.0); 150 | } 151 | } else { 152 | $("tr[data-fan='tool'] .fan-slider > input").slider("relayout"); 153 | } 154 | } else { 155 | $(".table-fan-control tr[data-fan='" + fan + "'] button.fan-visibility").removeClass("btn-primary active").addClass("btn-default"); 156 | $(".table-fan-control tr[data-fan='" + fan + "'] button.fan-override").addClass("disabled"); 157 | $(".table-fan-control tr[data-fan='" + fan + "'] > td:last-child").children().addClass("hidden"); 158 | 159 | if (fan != "tool") { 160 | setFanOverride(fan, undefined); 161 | } 162 | } 163 | 164 | // Save the fan visibility states 165 | var visibilityBitmap = 0; 166 | for(var fan = 0; fan <= maxFans; fan++) { 167 | if (fan == 0) { 168 | if ($(".table-fan-control tr[data-fan='tool'] button.fan-visibility").hasClass("active")) { 169 | visibilityBitmap |= 1; 170 | } 171 | } else if ($(".table-fan-control tr[data-fan='" + (fan - 1) + "'] button.fan-visibility").hasClass("active")) { 172 | visibilityBitmap |= (1 << fan); 173 | } 174 | } 175 | setLocalSetting("fanVisibility", visibilityBitmap); 176 | } 177 | 178 | 179 | /* Extrusion Multiplier */ 180 | 181 | for(var extr = 0; extr < maxExtruders; extr++) { 182 | $('#slider_extr_' + extr).slider({ 183 | enabled: false, 184 | id: "extr-" + extr, 185 | min: 50, 186 | max: 150, 187 | step: 1, 188 | value: 100, 189 | tooltip: "always", 190 | formatter: function(value) { 191 | return value + " %"; 192 | } 193 | }).on("slideStart", function() { 194 | extrSliderActive = true; 195 | }).on("slideStop", function(slideEvt) { 196 | if (isConnected && !isNaN(slideEvt.value)) { 197 | sendGCode("M221 D" + $(this).data("drive") + " S" + slideEvt.value); 198 | } 199 | extrSliderActive = false; 200 | }); 201 | } 202 | 203 | 204 | /* Speed slider */ 205 | 206 | $('#slider_speed').slider({ 207 | enabled: false, 208 | id: "speed", 209 | min: 20, 210 | max: 300, 211 | step: 1, 212 | value: 100, 213 | tooltip: "always", 214 | formatter: function(value) { 215 | return value + " %"; 216 | } 217 | }).on("slideStart", function() { 218 | speedSliderActive = true; 219 | }).on("slideStop", function(slideEvt) { 220 | if (isConnected && !isNaN(slideEvt.value)) { 221 | sendGCode("M220 S" + slideEvt.value); 222 | } 223 | speedSliderActive = false; 224 | }); 225 | -------------------------------------------------------------------------------- /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/notify.js: -------------------------------------------------------------------------------- 1 | /* Notification subsystem for Duet Web Control 2 | * 3 | * written by Christian Hammacher (c) 2016-2018 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 | $.notifyDefaults({ 11 | animate: { 12 | enter: "animated fadeInDown", 13 | exit: "animated fadeOutDown" 14 | }, 15 | placement: { 16 | from: "bottom", 17 | align: "center" 18 | }, 19 | newest_on_top: true, 20 | template: '
' + 21 | '' + 22 | ' ' + 23 | '{1} ' + 24 | '{2}' + 25 | '
' + 26 | '
' + 27 | '
' + 28 | '' + 29 | '
' 30 | }); 31 | 32 | var openNotifications = []; 33 | var openMessageNotifications = []; 34 | 35 | 36 | function showDownloadMessage() { 37 | var notifySettings = { icon: "glyphicon glyphicon-compressed", 38 | title: "" + T("Downloading Files") + "

", 39 | message: T("Please wait while the selected files are being downloaded...") 40 | }; 41 | var options = { type: "info", 42 | allow_dismiss: false, 43 | delay: -1, 44 | showProgressbar: true 45 | }; 46 | return $.notify(notifySettings, options); 47 | } 48 | 49 | function showHaltMessage() { 50 | // Display backdrop and hide it when ready 51 | $("#modal_backdrop").modal("show"); 52 | setTimeout(function() { 53 | $("#modal_backdrop").modal("hide"); 54 | }, settings.haltedReconnectDelay); 55 | 56 | // Display notification 57 | var notifySettings = { icon: "glyphicon glyphicon-flash", 58 | title: "" + T("Emergency STOP") + "

", 59 | message: T("Please wait while the firmware is restarting..."), 60 | progress: settings.haltedReconnectDelay / 100, 61 | }; 62 | var options = { type: "warning", 63 | allow_dismiss: false, 64 | delay: settings.haltedReconnectDelay, 65 | timeout: 1000, 66 | showProgressbar: true 67 | }; 68 | var notification = $.notify(notifySettings, options); 69 | $(notification.$ele).css("z-index", 9999); 70 | } 71 | 72 | function showMessage(type, title, message, timeout, allowDismiss, regularNotification) { 73 | // Set default arguments 74 | if (allowDismiss == undefined) { allowDismiss = true; } 75 | if (type == "danger" && allowDismiss && !settings.autoCloseErrorMessages) { 76 | if (timeout == undefined) { timeout = 0; } 77 | if (regularNotification == undefined) { regularNotification = false; } 78 | } else { 79 | if (timeout == undefined) { timeout = settings.notificationTimeout; } 80 | if (regularNotification == undefined) { regularNotification = true; } 81 | } 82 | 83 | // Check if an HTML5 notification shall be displayed 84 | if (document.hidden && settings.useHtmlNotifications) { 85 | // HTML5 notifications expect plain strings. Remove potential styles first 86 | if (title.indexOf("<") != -1) { title = $("" + title + "").text(); } 87 | if (message.indexOf("<") != -1) { message = $("" + message + "").text(); } 88 | 89 | // Create new notification and display it 90 | var options = { 91 | body: message, 92 | icon: "favicon.ico", 93 | dir : "ltr", 94 | requireInteraction: allowDismiss 95 | }; 96 | var notification = new Notification(title, options); 97 | if (allowDismiss) { notification.onclick = function() { this.close(); }; } 98 | if (timeout > 0) { setTimeout(function() { notification.close(); }, timeout); } 99 | return notification; 100 | } 101 | 102 | // Otherwise display a JQuery notification. Find a suitable icon first 103 | var icon = "glyphicon glyphicon-info-sign"; 104 | if (type == "warning") { 105 | icon = "glyphicon glyphicon-warning-sign"; 106 | } else if (type == "danger") { 107 | icon = "glyphicon glyphicon-exclamation-sign"; 108 | } 109 | 110 | // Check if the title can be displayed as bold text 111 | if (title != "") 112 | { 113 | if (title.indexOf("") == -1) 114 | { 115 | title = "" + title + ""; 116 | } 117 | title += "

"; 118 | } 119 | 120 | // Create and show the notification 121 | var notifySettings = { icon: icon, 122 | title: title, 123 | message: message }; 124 | var options = { type: type, 125 | allow_dismiss: allowDismiss, 126 | delay: timeout }; 127 | var notification = $.notify(notifySettings, options); 128 | if (allowDismiss) { 129 | $(notification.$ele).click(function() { 130 | openNotifications = openNotifications.filter(function(notif) { return notif != notification; }); 131 | openMessageNotifications = openMessageNotifications.filter(function(notif) { return notif != notification; }); 132 | notification.close(); 133 | }); 134 | } 135 | 136 | // Make sure we don't display more notifications than allowed 137 | if (regularNotification && settings.maxNotifications >= 1) { 138 | if (timeout > 0) { 139 | setTimeout(function() { 140 | openNotifications = openNotifications.filter(function(notif) { return notif != notification; }); 141 | openMessageNotifications = openMessageNotifications.filter(function(item) { return item != notification; }); 142 | }, timeout); 143 | } 144 | while (openMessageNotifications.length >= settings.maxNotifications) { 145 | openMessageNotifications.shift().close(); 146 | } 147 | openMessageNotifications.push(notification); 148 | } 149 | openNotifications.push(notification); 150 | 151 | return notification; 152 | } 153 | 154 | function showUpdateMessage(type, customTimeout) { 155 | // Determine the message and timespan for the notification 156 | var title, message, timeout; 157 | switch (type) { 158 | case 0: // Firmware 159 | title = T("Updating Firmware..."); 160 | message = T("Please wait while the firmware is being updated..."); 161 | timeout = settings.updateReconnectDelay; 162 | break; 163 | 164 | case 1: // Duet WiFi Server 165 | title = T("Updating WiFi Server..."); 166 | message = T("Please wait while the Duet WiFi Server firmware is being updated..."); 167 | timeout = settings.dwsReconnectDelay; 168 | break; 169 | 170 | case 2: // Duet Web Control 171 | title = T("Updating Web Interface..."); 172 | message = T("Please wait while Duet Web Control is being updated..."); 173 | timeout = settings.dwcReconnectDelay; 174 | break; 175 | 176 | case 3: // Multiple Updates 177 | title = T("Updating Firmware..."); 178 | message = T("Please wait while multiple firmware updates are being installed..."); 179 | timeout = customTimeout; 180 | break; 181 | 182 | default: // Unknown 183 | alert(T("Error! Unknown update parameter!")); 184 | return; 185 | } 186 | 187 | // Display backdrop and hide it when ready 188 | $("#modal_backdrop").modal("show"); 189 | setTimeout(function() { 190 | $("#modal_backdrop").modal("hide"); 191 | }, timeout); 192 | 193 | // Display notification 194 | var notifySettings = { icon: "glyphicon glyphicon-time", 195 | title: "" + title + "

", 196 | message: message, 197 | progress: timeout / 100, 198 | }; 199 | var options = { type: "success", 200 | allow_dismiss: false, 201 | delay: timeout, 202 | timeout: 1000, 203 | showProgressbar: true 204 | }; 205 | var notification = $.notify(notifySettings, options); 206 | $(notification.$ele).css("z-index", 9999); 207 | return notification; 208 | } 209 | 210 | /*function closeNotifications() { 211 | openMessageNotifications.forEach(function(notification) { 212 | openNotifications = openNotifications.filter(function(notif) { return notif != notification; }); 213 | notification.close(); 214 | }); 215 | openMessageNotifications = []; 216 | }*/ 217 | 218 | function closeAllNotifications() { 219 | openNotifications.forEach(function(notification) { 220 | notification.close(); 221 | }); 222 | openNotifications = []; 223 | openMessageNotifications = []; 224 | } 225 | -------------------------------------------------------------------------------- /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 | // Temperature chart options 10 | var maxTemperatureSamples = 1000; 11 | 12 | var tempChart; 13 | var tempChartOptions = { 14 | // This array should hold maxHeaters + maxTempSensors items 15 | colors: ["#0000FF", "#FF0000", "#00DD00", "#FFA000", "#FF00FF", "#337AB7", "#000000", "#E0E000", // Heater colors 16 | "#AEAEAE", "#BC0000", "#00CB00", "#0000DC", "#FEABEF", "#A0A000", "#DDDD00", "#00BDBD", "#CCBBAA", "#AA00AA"], // Virtual heater colors 17 | grid: { 18 | borderWidth: 0 19 | }, 20 | xaxis: { 21 | show: false 22 | /*labelWidth: 0, 23 | labelHeight: 0, 24 | tickSize: 30000, 25 | tickFormatter: function() { return ""; }, 26 | reserveSpace: false*/ 27 | }, 28 | yaxis: { 29 | min: 0, 30 | max: 280 31 | } 32 | }; 33 | 34 | var recordedTemperatures, extraSensorVisibility; 35 | var maxLayerTime = 0; 36 | var refreshTempChart = false; 37 | 38 | // Print chart options 39 | var printChart; 40 | var printChartOptions = { 41 | colors: ["#EDC240"], 42 | grid: { 43 | borderWidth: 0, 44 | hoverable: true, 45 | clickable: true 46 | }, 47 | pan: { 48 | interactive: true 49 | }, 50 | series: { 51 | lines: { 52 | show: true 53 | }, 54 | points: { 55 | show: true 56 | } 57 | }, 58 | xaxis: { 59 | min: 1, 60 | tickDecimals: 0, 61 | }, 62 | yaxis: { 63 | min: 0, 64 | max: 30, 65 | ticks: 5, 66 | tickDecimals: 0, 67 | tickFormatter: function(val) { 68 | if (!val) { 69 | return ""; 70 | } else { 71 | return formatTime(val); 72 | } 73 | } 74 | }, 75 | zoom: { 76 | interactive: true 77 | } 78 | }; 79 | 80 | var refreshPrintChart = false; 81 | var layerData; 82 | 83 | 84 | /* Temperature chart */ 85 | 86 | function recordCurrentTemperatures(temps) { 87 | var timeNow = (new Date()).getTime(); 88 | 89 | // Add temperatures for each heater 90 | // Also cut off the last one if there are too many temperature samples 91 | for(var heater = 0; heater < maxHeaters; heater++) { 92 | if (heater < temps.length && heatersInUse[heater]) { 93 | recordedTemperatures[heater].push([timeNow, temps[heater]]); 94 | } else { 95 | recordedTemperatures[heater].push([]); 96 | } 97 | 98 | if (recordedTemperatures[heater].length > maxTemperatureSamples) { 99 | recordedTemperatures[heater].shift(); 100 | } 101 | } 102 | } 103 | 104 | function recordExtraTemperatures(temps) { 105 | var timeNow = (new Date()).getTime(); 106 | 107 | // Add dashed series for each temperature sensor 108 | for(var i = 0; i < maxTempSensors; i++) 109 | { 110 | if (i < temps.length) { 111 | recordedTemperatures[maxHeaters + i].data.push([timeNow, temps[i].temp]); 112 | } else { 113 | recordedTemperatures[maxHeaters + i].data.push([]); 114 | } 115 | 116 | if (recordedTemperatures[maxHeaters + i].data.length > maxTemperatureSamples) { 117 | recordedTemperatures[maxHeaters + i].data.shift(); 118 | } 119 | } 120 | } 121 | 122 | function drawTemperatureChart() { 123 | // Only draw the chart if it's possible 124 | if ($("#chart_temp").width() === 0) { 125 | refreshTempChart = true; 126 | return; 127 | } 128 | 129 | // Check if we need to recreate the chart 130 | var recreateChart = false; 131 | if (tempLimit != tempChartOptions.yaxis.max) { 132 | tempChartOptions.yaxis.max = tempLimit; 133 | recreateChart = true; 134 | } 135 | 136 | // Draw it 137 | if (tempChart == undefined || recreateChart) { 138 | tempChart = $.plot("#chart_temp", recordedTemperatures, tempChartOptions); 139 | } else { 140 | tempChart.setData(recordedTemperatures); 141 | tempChart.setupGrid(); 142 | tempChart.draw(); 143 | } 144 | 145 | refreshTempChart = false; 146 | } 147 | 148 | function setExtraTemperatureVisibility(sensor, visible) { 149 | // Update visibility 150 | recordedTemperatures[maxHeaters + sensor].dashes.show = visible; 151 | 152 | // Save state 153 | extraSensorVisibility[sensor] = visible; 154 | setLocalSetting("extraSensorVisibility", extraSensorVisibility); 155 | } 156 | 157 | 158 | /* Print statistics chart */ 159 | 160 | function addLayerData(lastLayerTime, filamentUsed, updateGui) { 161 | layerData.push([layerData.length + 1, lastLayerTime, filamentUsed]); 162 | if (lastLayerTime > maxLayerTime) { 163 | maxLayerTime = lastLayerTime; 164 | } 165 | 166 | if (updateGui) { 167 | $("#td_last_layertime").html(formatTime(lastLayerTime)).addClass("layer-done-animation"); 168 | setTimeout(function() { 169 | $("#td_last_layertime").removeClass("layer-done-animation"); 170 | }, 2000); 171 | 172 | drawPrintChart(); 173 | } 174 | } 175 | 176 | function drawPrintChart() { 177 | // Only draw the chart if it's possible 178 | if ($("#chart_print").width() === 0) { 179 | refreshPrintChart = true; 180 | return; 181 | } 182 | 183 | // Find absolute maximum values for the X axis 184 | var maxX = 25, maxPanX = 25; 185 | if (layerData.length < 21) { 186 | maxX = maxPanX = 20; 187 | } else if (layerData.length < 25) { 188 | maxX = maxPanX = layerData.length; 189 | } else { 190 | maxPanX = layerData.length; 191 | } 192 | printChartOptions.xaxis.max = maxX; 193 | printChartOptions.xaxis.panRange = [1, maxPanX]; 194 | printChartOptions.xaxis.zoomRange = [25, maxPanX]; 195 | 196 | // Find max visible value for Y axis 197 | var maxY = 30; 198 | if (layerData.length > 1) { 199 | var firstLayerToCheck = (layerData.length > 26) ? layerData.length - 25 : 1; 200 | for(var i = firstLayerToCheck; i < layerData.length; i++) { 201 | var layerVal = layerData[i][1] * 1.1; 202 | if (maxY < layerVal) { 203 | maxY = layerVal; 204 | } 205 | } 206 | } 207 | printChartOptions.yaxis.max = maxY; 208 | printChartOptions.yaxis.panRange = [0, (maxLayerTime < maxY) ? maxY : maxLayerTime]; 209 | printChartOptions.yaxis.zoomRange = [30, (maxLayerTime < maxY) ? maxY : maxLayerTime]; 210 | 211 | // Update chart and pan to the right 212 | printChart = $.plot("#chart_print", [layerData], printChartOptions); 213 | printChart.pan({ left: 99999 }); 214 | refreshPrintChart = false; 215 | 216 | // Add hover events to chart 217 | $("#chart_print").unbind("plothover").bind("plothover", function(e, pos, item) { 218 | if (item) { 219 | // Get layer number and time used 220 | var layer = item.datapoint[0]; 221 | var timeUsed = item.datapoint[1]; 222 | 223 | // Get filament usage 224 | var filamentUsed; 225 | if (layer > 1) { 226 | filamentUsed = layerData[layer - 1][2] - layerData[layer - 2][2]; 227 | } else { 228 | filamentUsed = layerData[layer - 1][2]; 229 | } 230 | 231 | // Build tool tip 232 | var toolTip = "
" + T("Layer {0}", layer) + "

"; 233 | toolTip += T("Time: {0}", formatTime(timeUsed)) + "
"; 234 | toolTip += T("Filament usage: {0} mm", filamentUsed.toFixed(1)); 235 | 236 | // Show it 237 | $("#layer_tooltip").html(toolTip).css({top: item.pageY + 5, left: item.pageX + 5}).fadeIn(200); 238 | } else { 239 | $("#layer_tooltip").hide(); 240 | } 241 | }); 242 | } 243 | 244 | 245 | /* Common functions */ 246 | 247 | function resizeCharts() { 248 | var contentHeight = $("#table_tools").height(); 249 | if (!$("#div_heaters").hasClass("hidden")) { 250 | contentHeight = $("#table_heaters").height(); 251 | } else if (!$("#div_extra").hasClass("hidden")) { 252 | contentHeight = $("#table_extra").height(); 253 | } 254 | 255 | var statusHeight = 0; 256 | $("#div_status table").each(function() { 257 | statusHeight += $(this).outerHeight(); 258 | }); 259 | 260 | var max = (contentHeight > statusHeight) ? contentHeight : statusHeight; 261 | var padding = $("#chart_temp").parent().outerHeight() - $("#chart_temp").height(); 262 | max -= padding; 263 | 264 | if (max > 0) { 265 | $("#chart_temp").css("height", max); 266 | } 267 | 268 | if (refreshTempChart) { 269 | drawTemperatureChart(); 270 | } 271 | if (refreshPrintChart) { 272 | drawPrintChart(); 273 | } 274 | } 275 | 276 | $(".panel-chart").resize(function() { 277 | resizeCharts(); 278 | }); 279 | 280 | function resetChartData() { 281 | // Initialize visibility states of the extra temperature sensors 282 | extraSensorVisibility = getLocalSetting("extraSensorVisibility", null); 283 | if (extraSensorVisibility == null || extraSensorVisibility.length < maxTempSensors) { 284 | extraSensorVisibility = []; 285 | for(var i = 0; i < maxTempSensors; i++) { 286 | // Don't show any extra temperatures in the chart by default 287 | extraSensorVisibility.push(false); 288 | } 289 | } 290 | 291 | // Reset data of the temperature chart 292 | recordedTemperatures = []; 293 | for(var i = 0; i < maxHeaters; i++) { 294 | recordedTemperatures.push([]); 295 | } 296 | 297 | for(var i = 0; i < maxTempSensors; i++) { 298 | recordedTemperatures.push({ 299 | dashes: { show: extraSensorVisibility[i] }, 300 | lines: { show: false }, 301 | data: [] 302 | }); 303 | 304 | $("#table_extra tr input[type='checkbox']").eq(i).prop("checked", extraSensorVisibility[i]); 305 | } 306 | 307 | // Reset data of the layer chart 308 | layerData = []; 309 | maxLayerTime = 0; 310 | } 311 | -------------------------------------------------------------------------------- /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 | # Duet Web Control 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), Duet Web Control 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/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/3rd-party/bootstrap-notify.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Project: Bootstrap Notify = v3.1.5 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 | 9 | /* global define:false, require: false, jQuery:false */ 10 | 11 | (function (factory) { 12 | if (typeof define === 'function' && define.amd) { 13 | // AMD. Register as an anonymous module. 14 | define(['jquery'], factory); 15 | } else if (typeof exports === 'object') { 16 | // Node/CommonJS 17 | factory(require('jquery')); 18 | } else { 19 | // Browser globals 20 | factory(jQuery); 21 | } 22 | }(function ($) { 23 | // Create the defaults once 24 | var defaults = { 25 | element: 'body', 26 | position: null, 27 | type: "info", 28 | allow_dismiss: true, 29 | allow_duplicates: true, 30 | newest_on_top: false, 31 | showProgressbar: false, 32 | placement: { 33 | from: "top", 34 | align: "right" 35 | }, 36 | offset: 20, 37 | spacing: 10, 38 | z_index: 1031, 39 | delay: 5000, 40 | timer: 1000, 41 | url_target: '_blank', 42 | mouse_over: null, 43 | animate: { 44 | enter: 'animated fadeInDown', 45 | exit: 'animated fadeOutUp' 46 | }, 47 | onShow: null, 48 | onShown: null, 49 | onClose: null, 50 | onClosed: null, 51 | onClick: null, 52 | icon_type: 'class', 53 | template: '' 54 | }; 55 | 56 | String.format = function () { 57 | var args = arguments; 58 | var str = arguments[0]; 59 | return str.replace(/(\{\{\d\}\}|\{\d\})/g, function (str) { 60 | if (str.substring(0, 2) === "{{") return str; 61 | var num = parseInt(str.match(/\d/)[0]); 62 | return args[num + 1]; 63 | }); 64 | }; 65 | 66 | function isDuplicateNotification(notification) { 67 | var isDupe = false; 68 | 69 | $('[data-notify="container"]').each(function (i, el) { 70 | var $el = $(el); 71 | var title = $el.find('[data-notify="title"]').html().trim(); 72 | var message = $el.find('[data-notify="message"]').html().trim(); 73 | 74 | // The input string might be different than the actual parsed HTML string! 75 | // (
vs
for example) 76 | // So we have to force-parse this as HTML here! 77 | var isSameTitle = title === $("
" + notification.settings.content.title + "
").html().trim(); 78 | var isSameMsg = message === $("
" + notification.settings.content.message + "
").html().trim(); 79 | var isSameType = $el.hasClass('alert-' + notification.settings.type); 80 | 81 | if (isSameTitle && isSameMsg && isSameType) { 82 | //we found the dupe. Set the var and stop checking. 83 | isDupe = true; 84 | } 85 | return !isDupe; 86 | }); 87 | 88 | return isDupe; 89 | } 90 | 91 | function Notify(element, content, options) { 92 | // Setup Content of Notify 93 | var contentObj = { 94 | content: { 95 | message: typeof content === 'object' ? content.message : content, 96 | title: content.title ? content.title : '', 97 | icon: content.icon ? content.icon : '', 98 | url: content.url ? content.url : '#', 99 | target: content.target ? content.target : '-' 100 | } 101 | }; 102 | 103 | options = $.extend(true, {}, contentObj, options); 104 | this.settings = $.extend(true, {}, defaults, options); 105 | this._defaults = defaults; 106 | if (this.settings.content.target === "-") { 107 | this.settings.content.target = this.settings.url_target; 108 | } 109 | this.animations = { 110 | start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart', 111 | end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend' 112 | }; 113 | 114 | if (typeof this.settings.offset === 'number') { 115 | this.settings.offset = { 116 | x: this.settings.offset, 117 | y: this.settings.offset 118 | }; 119 | } 120 | 121 | //if duplicate messages are not allowed, then only continue if this new message is not a duplicate of one that it already showing 122 | if (this.settings.allow_duplicates || (!this.settings.allow_duplicates && !isDuplicateNotification(this))) { 123 | this.init(); 124 | } 125 | } 126 | 127 | $.extend(Notify.prototype, { 128 | init: function () { 129 | var self = this; 130 | 131 | this.buildNotify(); 132 | if (this.settings.content.icon) { 133 | this.setIcon(); 134 | } 135 | if (this.settings.content.url != "#") { 136 | this.styleURL(); 137 | } 138 | this.styleDismiss(); 139 | this.placement(); 140 | this.bind(); 141 | 142 | this.notify = { 143 | $ele: this.$ele, 144 | update: function (command, update) { 145 | var commands = {}; 146 | if (typeof command === "string") { 147 | commands[command] = update; 148 | } else { 149 | commands = command; 150 | } 151 | for (var cmd in commands) { 152 | switch (cmd) { 153 | case "type": 154 | this.$ele.removeClass('alert-' + self.settings.type); 155 | this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type); 156 | self.settings.type = commands[cmd]; 157 | this.$ele.addClass('alert-' + commands[cmd]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[cmd]); 158 | break; 159 | case "icon": 160 | var $icon = this.$ele.find('[data-notify="icon"]'); 161 | if (self.settings.icon_type.toLowerCase() === 'class') { 162 | $icon.removeClass(self.settings.content.icon).addClass(commands[cmd]); 163 | } else { 164 | if (!$icon.is('img')) { 165 | $icon.find('img'); 166 | } 167 | $icon.attr('src', commands[cmd]); 168 | } 169 | self.settings.content.icon = commands[command]; 170 | break; 171 | case "progress": 172 | var newDelay = self.settings.delay - (self.settings.delay * (commands[cmd] / 100)); 173 | this.$ele.data('notify-delay', newDelay); 174 | this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[cmd]).css('width', commands[cmd] + '%'); 175 | break; 176 | case "url": 177 | this.$ele.find('[data-notify="url"]').attr('href', commands[cmd]); 178 | break; 179 | case "target": 180 | this.$ele.find('[data-notify="url"]').attr('target', commands[cmd]); 181 | break; 182 | default: 183 | this.$ele.find('[data-notify="' + cmd + '"]').html(commands[cmd]); 184 | } 185 | } 186 | var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y); 187 | self.reposition(posX); 188 | }, 189 | close: function () { 190 | self.close(); 191 | } 192 | }; 193 | 194 | }, 195 | buildNotify: function () { 196 | var content = this.settings.content; 197 | this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target)); 198 | this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align); 199 | if (!this.settings.allow_dismiss) { 200 | this.$ele.find('[data-notify="dismiss"]').css('display', 'none'); 201 | } 202 | if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) { 203 | this.$ele.find('[data-notify="progressbar"]').remove(); 204 | } 205 | }, 206 | setIcon: function () { 207 | if (this.settings.icon_type.toLowerCase() === 'class') { 208 | this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon); 209 | } else { 210 | if (this.$ele.find('[data-notify="icon"]').is('img')) { 211 | this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon); 212 | } else { 213 | this.$ele.find('[data-notify="icon"]').append('Notify Icon'); 214 | } 215 | } 216 | }, 217 | styleDismiss: function () { 218 | this.$ele.find('[data-notify="dismiss"]').css({ 219 | position: 'absolute', 220 | right: '10px', 221 | top: '5px', 222 | zIndex: this.settings.z_index + 2 223 | }); 224 | }, 225 | styleURL: function () { 226 | this.$ele.find('[data-notify="url"]').css({ 227 | backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)', 228 | height: '100%', 229 | left: 0, 230 | position: 'absolute', 231 | top: 0, 232 | width: '100%', 233 | zIndex: this.settings.z_index + 1 234 | }); 235 | }, 236 | placement: function () { 237 | var self = this, 238 | offsetAmt = this.settings.offset.y, 239 | css = { 240 | display: 'inline-block', 241 | margin: '0px auto', 242 | position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'), 243 | transition: 'all .5s ease-in-out', 244 | zIndex: this.settings.z_index 245 | }, 246 | hasAnimation = false, 247 | settings = this.settings; 248 | 249 | $('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function () { 250 | offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing)); 251 | }); 252 | if (this.settings.newest_on_top === true) { 253 | offsetAmt = this.settings.offset.y; 254 | } 255 | css[this.settings.placement.from] = offsetAmt + 'px'; 256 | 257 | switch (this.settings.placement.align) { 258 | case "left": 259 | case "right": 260 | css[this.settings.placement.align] = this.settings.offset.x + 'px'; 261 | break; 262 | case "center": 263 | css.left = 0; 264 | css.right = 0; 265 | break; 266 | } 267 | this.$ele.css(css).addClass(this.settings.animate.enter); 268 | $.each(Array('webkit-', 'moz-', 'o-', 'ms-', ''), function (index, prefix) { 269 | self.$ele[0].style[prefix + 'AnimationIterationCount'] = 1; 270 | }); 271 | 272 | $(this.settings.element).append(this.$ele); 273 | 274 | if (this.settings.newest_on_top === true) { 275 | offsetAmt = (parseInt(offsetAmt) + parseInt(this.settings.spacing)) + this.$ele.outerHeight(); 276 | this.reposition(offsetAmt); 277 | } 278 | 279 | if ($.isFunction(self.settings.onShow)) { 280 | self.settings.onShow.call(this.$ele); 281 | } 282 | 283 | this.$ele.one(this.animations.start, function () { 284 | hasAnimation = true; 285 | }).one(this.animations.end, function () { 286 | self.$ele.removeClass(self.settings.animate.enter); 287 | if ($.isFunction(self.settings.onShown)) { 288 | self.settings.onShown.call(this); 289 | } 290 | }); 291 | 292 | setTimeout(function () { 293 | if (!hasAnimation) { 294 | if ($.isFunction(self.settings.onShown)) { 295 | self.settings.onShown.call(this); 296 | } 297 | } 298 | }, 600); 299 | }, 300 | bind: function () { 301 | var self = this; 302 | 303 | this.$ele.find('[data-notify="dismiss"]').on('click', function () { 304 | self.close(); 305 | }); 306 | 307 | if ($.isFunction(self.settings.onClick)) { 308 | this.$ele.on('click', function (event) { 309 | if (event.target != self.$ele.find('[data-notify="dismiss"]')[0]) { 310 | self.settings.onClick.call(this, event); 311 | } 312 | }); 313 | } 314 | 315 | this.$ele.mouseover(function () { 316 | $(this).data('data-hover', "true"); 317 | }).mouseout(function () { 318 | $(this).data('data-hover', "false"); 319 | }); 320 | this.$ele.data('data-hover', "false"); 321 | 322 | if (this.settings.delay > 0) { 323 | self.$ele.data('notify-delay', self.settings.delay); 324 | var timer = setInterval(function () { 325 | var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer; 326 | if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over === "pause") || self.settings.mouse_over != "pause") { 327 | var percent = ((self.settings.delay - delay) / self.settings.delay) * 100; 328 | self.$ele.data('notify-delay', delay); 329 | self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%'); 330 | } 331 | if (delay <= -(self.settings.timer)) { 332 | clearInterval(timer); 333 | self.close(); 334 | } 335 | }, self.settings.timer); 336 | } 337 | }, 338 | close: function () { 339 | var self = this, 340 | posX = parseInt(this.$ele.css(this.settings.placement.from)), 341 | hasAnimation = false; 342 | 343 | this.$ele.attr('data-closing', 'true').addClass(this.settings.animate.exit); 344 | self.reposition(posX); 345 | 346 | if ($.isFunction(self.settings.onClose)) { 347 | self.settings.onClose.call(this.$ele); 348 | } 349 | 350 | this.$ele.one(this.animations.start, function () { 351 | hasAnimation = true; 352 | }).one(this.animations.end, function () { 353 | $(this).remove(); 354 | if ($.isFunction(self.settings.onClosed)) { 355 | self.settings.onClosed.call(this); 356 | } 357 | }); 358 | 359 | setTimeout(function () { 360 | if (!hasAnimation) { 361 | self.$ele.remove(); 362 | if ($.isFunction(self.settings.onClosed)) { 363 | self.settings.onClosed.call(this); 364 | } 365 | } 366 | }, 600); 367 | }, 368 | reposition: function (posX) { 369 | var self = this, 370 | notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])', 371 | $elements = this.$ele.nextAll(notifies); 372 | if (this.settings.newest_on_top === true) { 373 | $elements = this.$ele.prevAll(notifies); 374 | } 375 | $elements.each(function () { 376 | $(this).css(self.settings.placement.from, posX); 377 | posX = (parseInt(posX) + parseInt(self.settings.spacing)) + $(this).outerHeight(); 378 | }); 379 | } 380 | }); 381 | 382 | $.notify = function (content, options) { 383 | var plugin = new Notify(this, content, options); 384 | return plugin.notify; 385 | }; 386 | $.notifyDefaults = function (options) { 387 | defaults = $.extend(true, {}, defaults, options); 388 | return defaults; 389 | }; 390 | 391 | $.notifyClose = function (selector) { 392 | 393 | if (typeof selector === "undefined" || selector === "all") { 394 | $('[data-notify]').find('[data-notify="dismiss"]').trigger('click'); 395 | }else if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){ 396 | $('.alert-' + selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click'); 397 | } else if(selector){ 398 | $(selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click'); 399 | } 400 | else { 401 | $('[data-notify-position="' + selector + '"]').find('[data-notify="dismiss"]').trigger('click'); 402 | } 403 | }; 404 | 405 | $.notifyCloseExcept = function (selector) { 406 | 407 | if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){ 408 | $('[data-notify]').not('.alert-' + selector).find('[data-notify="dismiss"]').trigger('click'); 409 | } else{ 410 | $('[data-notify]').not(selector).find('[data-notify="dismiss"]').trigger('click'); 411 | } 412 | }; 413 | 414 | 415 | })); 416 | 417 | 418 | -------------------------------------------------------------------------------- /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 | /* Change Step Dialog */ 54 | 55 | var stepChangeType, stepChangeIndex, stepChangeAxisIndex; 56 | 57 | function showStepDialog(type, index, currentValue, axisIndex) { 58 | stepChangeType = type; 59 | stepChangeIndex = index; 60 | stepChangeAxisIndex = axisIndex; 61 | 62 | $("#input_step_amount").val(currentValue); 63 | $("#input_step_amount_unit").text((type == "feedrate") ? T("mm/s") : T("mm")); 64 | $("#modal_change_step").modal("show"); 65 | } 66 | 67 | $("#modal_change_step").on("shown.bs.modal", function() { 68 | $("#modal_change_step input").focus(); 69 | }); 70 | 71 | $("#modal_change_step form").submit(function(e) { 72 | if (stepChangeType == "axis") { 73 | settings.axisMoveSteps[stepChangeAxisIndex][stepChangeIndex] = $("#input_step_amount").val(); 74 | } else if (stepChangeType == "amount") { 75 | settings.extruderAmounts[stepChangeIndex] = $("#input_step_amount").val(); 76 | } else { 77 | settings.extruderFeedrates[stepChangeIndex] = $("#input_step_amount").val(); 78 | } 79 | 80 | applyMovementSteps(); 81 | $("#modal_change_step").modal("hide"); 82 | 83 | e.preventDefault(); 84 | }); 85 | 86 | 87 | /* Host Prompt */ 88 | 89 | function showHostPrompt() { 90 | $('#input_host').val(getLocalSetting("lastHost", "")); 91 | $("#modal_host_input").modal("show"); 92 | } 93 | 94 | $("#modal_host_input").on("shown.bs.modal", function() { 95 | $("#input_host").focus(); 96 | }); 97 | 98 | $("#form_host").submit(function(e) { 99 | $("#modal_host_input").off("hide.bs.modal").modal("hide"); 100 | ajaxPrefix = settings.lastHost = $("#input_host").val(); // let the user decide if this shall be saved 101 | 102 | ajaxPrefix += "/"; 103 | if (ajaxPrefix.indexOf("://") == -1) { 104 | // Prepend http prefix if no URI scheme is given 105 | ajaxPrefix = "http://" + ajaxPrefix; 106 | } 107 | 108 | connect(sessionPassword, false); 109 | e.preventDefault(); 110 | }); 111 | 112 | 113 | /* Password prompt */ 114 | 115 | function showPasswordPrompt() { 116 | $('#input_password').val(""); 117 | $("#modal_pass_input").modal("show"); 118 | } 119 | 120 | $("#form_password").submit(function(e) { 121 | $("#modal_pass_input").off("hide.bs.modal").modal("hide"); 122 | connect($("#input_password").val(), false); 123 | e.preventDefault(); 124 | }); 125 | 126 | $("#modal_pass_input").on("shown.bs.modal", function() { 127 | $("#input_password").focus(); 128 | }); 129 | 130 | 131 | /* Filament Change Dialog */ 132 | 133 | var filamentChangeTool, changingFilament; 134 | 135 | function showFilamentDialog(tool, changeFilament) { 136 | // make list of all available filaments 137 | $("#div_filaments").children().remove(); 138 | $("#table_filaments > tbody > tr").each(function() { 139 | var filament = $(this).data("filament"); 140 | var isLoaded = false; 141 | for(var i = 0; i < toolMapping.length; i++) { 142 | if (toolMapping[i].hasOwnProperty("filament") && toolMapping[i].filament == filament) { 143 | isLoaded = true; 144 | break; 145 | } 146 | } 147 | 148 | if (!isLoaded) { 149 | $("#div_filaments").append(' ' + filament + ''); 150 | } 151 | }); 152 | 153 | // show notification or selection dialog 154 | if ($("#div_filaments").children().length == 0) { 155 | showMessage("warning", T("No Filaments"), T("There are no other filaments available to choose. Please go to the Filaments page and define more.")); 156 | } else { 157 | filamentChangeTool = tool; 158 | changingFilament = changeFilament; 159 | $("#modal_change_filament").modal("show"); 160 | } 161 | } 162 | 163 | $("body").on("click", ".a-load-filament", function(e) { 164 | $("#modal_change_filament").modal("hide"); 165 | 166 | var gcode = ""; 167 | if (lastStatusResponse != undefined && lastStatusResponse.currentTool != filamentChangeTool) { 168 | gcode = "T" + filamentChangeTool + "\n"; 169 | } 170 | if (changingFilament) { 171 | gcode += "M702\n"; 172 | } 173 | gcode += "M701 S\"" + $(this).data("filament") + "\""; 174 | if (!compatibilityMode) { gcode += "\nM703"; } 175 | sendGCode(gcode); 176 | 177 | e.preventDefault(); 178 | }); 179 | 180 | 181 | /* File Edit Dialog */ 182 | 183 | function showEditDialog(title, content, callback) { 184 | $("#modal_edit .modal-title").text(T("Editing {0}", title)); 185 | var edit = $("#modal_edit textarea").val(content).get(0); 186 | edit.selectionStart = edit.selectionEnd = 0; 187 | $("#modal_edit").modal("show"); 188 | $("#btn_save_file").off("click").click(function() { 189 | $("#modal_edit").modal("hide"); 190 | callback($("#modal_edit textarea").val()); 191 | }); 192 | } 193 | 194 | $("#modal_edit").on("shown.bs.modal", function() { 195 | $("#modal_edit textarea").focus(); 196 | }); 197 | 198 | $("#modal_edit div.modal-content").resize(function() { 199 | var contentHeight = $(this).height(); 200 | var headerHeight = $("#modal_edit div.modal-header").height(); 201 | var footerHeight = $("#modal_edit div.modal-footer").height(); 202 | $("#modal_edit div.modal-body").css("height", (contentHeight - headerHeight - footerHeight - 60) + "px"); 203 | }); 204 | 205 | $(document).delegate("#modal_edit textarea", "keydown", function(e) { 206 | var keyCode = e.keyCode || e.which; 207 | 208 | if (keyCode == 9) { 209 | e.preventDefault(); 210 | var start = $(this).get(0).selectionStart; 211 | var end = $(this).get(0).selectionEnd; 212 | 213 | // set textarea value to: text before caret + tab + text after caret 214 | $(this).val($(this).val().substring(0, start) 215 | + "\t" 216 | + $(this).val().substring(end)); 217 | 218 | // put caret at right position again 219 | $(this).get(0).selectionStart = $(this).get(0).selectionEnd = start + 1; 220 | } 221 | }); 222 | 223 | 224 | /* Start Scan Dialog (proprietary) */ 225 | 226 | $("#btn_start_scan").click(function() { 227 | if (!$(this).hasClass("disabled")) { 228 | if (vendor == "diabase") { 229 | // Properietary implemenation with extra steps 230 | $("#modal_start_scan").modal("show"); 231 | } else { 232 | // Basic open-source variant 233 | showTextInput(T("Start new 3D scan"), T("Please enter a name for the new scan:"), function(name) { 234 | if (filenameValid(name)) { 235 | // Let the firmware do the communication to the board 236 | sendGCode("M752 S360 P" + name); 237 | } else { 238 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes.")); 239 | } 240 | }, undefined, function() { 241 | showMessage("danger", T("Error"), T("The filename for a new scan must not be empty!")); 242 | }); 243 | } 244 | } 245 | }); 246 | 247 | $("input[name='scanMode']").change(function() { 248 | $(".scan-unit").text(($("input[name='scanMode']:checked").val() == 0) ? T("mm") : T("°")); 249 | }); 250 | 251 | $("#modal_start_scan input").keyup(function() { 252 | $("#btn_start_scan_modal").toggleClass("disabled", $("#modal_start_scan input:invalid").length > 0); 253 | }); 254 | 255 | $("#btn_toggle_laser").click(function(e) { 256 | if ($(this).hasClass("active")) { 257 | sendGCode("M755 P0"); 258 | $(this).removeClass("active").children("span.content").text(T("Activate Laser")); 259 | } else { 260 | sendGCode("M755 P1") 261 | $(this).addClass("active").children("span.content").text(T("Deactivate Laser")); 262 | } 263 | 264 | $(this).blur(); 265 | e.preventDefault(); 266 | }); 267 | 268 | $("#modal_start_scan form").submit(function(e) { 269 | if (!$("#btn_start_scan_modal").hasClass("disabled")) { 270 | // 1. Turn off the alignment laser if it is still active 271 | if ($("#btn_toggle_laser").hasClass("active")) { 272 | $("#btn_toggle_laser").removeClass("active").children("span.content").text(T("Activate Laser")); 273 | if (isConnected) { 274 | sendGCode("M755 P0"); 275 | } 276 | } 277 | 278 | // 2. Start a new scan 279 | var filename = $("#input_scan_filename").val(); 280 | var range = $("#input_scan_range").val(); 281 | var resolution = $("#input_scan_resolution").val(); 282 | var mode = $("input[name='scanMode']:checked").val(); 283 | 284 | // 3. Check the filename and start a new scan 285 | if (filenameValid(filename)) { 286 | sendGCode("M752 S" + range + " R" + resolution + " N" + mode + " P\"" + filename + "\""); 287 | } else { 288 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes.")); 289 | } 290 | 291 | // 4. Hide the modal dialog 292 | $("#modal_start_scan").modal("hide"); 293 | } 294 | 295 | e.preventDefault(); 296 | }); 297 | 298 | $("#modal_start_scan").on("hidden.bs.modal", function() { 299 | if ($("#btn_toggle_laser").hasClass("active")) { 300 | $("#btn_toggle_laser").removeClass("active").children("span.content").text(T("Activate Laser")); 301 | if (isConnected) { 302 | // Turn off laser again when the dialog is closed 303 | sendGCode("M755 P0"); 304 | } 305 | } 306 | }); 307 | 308 | 309 | /* Scanner Progress Dialogs */ 310 | 311 | function updateScannerDialogs(scanResponse) { 312 | var scanProgress = 100, postProcessingProgress = 100, uploadProgress = 100; 313 | 314 | if (scanResponse.status == "S" || scanResponse.status == "P" || scanResponse.status == "U") { 315 | // Scanner is active 316 | if (!$("#modal_scanner").hasClass("in")) { 317 | $("#btn_cancel_scan").removeClass("hidden").removeClass("disabled"); 318 | $("#btn_close_scan").addClass("hidden"); 319 | 320 | $("#modal_scanner .modal-title").text(T("Scanning...")); 321 | $("#p_scan_info").text(T("Please wait while a scan is being made. This may take a while...")); 322 | 323 | $("#modal_scanner").modal("show"); 324 | } 325 | 326 | // Update progress 327 | if (scanResponse.status == "S") { 328 | scanProgress = scanResponse.progress; 329 | postProcessingProgress = 0; 330 | uploadProgress = 0; 331 | } else if (scanResponse.status == "P") { 332 | scanProgress = 100; 333 | postProcessingProgress = scanResponse.progress; 334 | uploadProgress = 0; 335 | } else if (scanResponse.status == "U") { 336 | scanProgress = 100; 337 | postProcessingProgress = 100; 338 | uploadProgress = scanResponse.progress; 339 | $("#btn_cancel_scan").addClass("disabled"); 340 | } 341 | } else if (scanResponse.status == "C") { 342 | // Scanner calibration is running 343 | if (!$("#modal_scanner_calibration").hasClass("in")) { 344 | $("#btn_cancel_calibration").removeClass("disabled"); 345 | $("#modal_scanner_calibration").modal("show"); 346 | } 347 | 348 | // Update progress 349 | $("#progress_calibration").css("width", scanResponse.progress + "%"); 350 | $("#span_calibration_progress").text(T("{0} %", scanResponse.progress)); 351 | } else if (scanResponse.status == "I") { 352 | // Scanner is inactive 353 | if ($("#modal_scanner").hasClass("in") && $("#modal_scanner .modal-title").text() != T("Scan complete")) { 354 | $("#modal_scanner .modal-title").text(T("Scan complete")); 355 | $("#p_scan_info").text(T("Your 3D scan is now complete! You may download it from the file list next.")); 356 | 357 | $("#btn_cancel_scan").addClass("hidden"); 358 | $("#btn_close_scan").removeClass("hidden"); 359 | 360 | updateScanFiles(); 361 | $(".span-refresh-scans").addClass("hidden"); 362 | } 363 | 364 | if ($("#modal_scanner_calibration").hasClass("in")) { 365 | $("#modal_scanner_calibration").modal("hide"); 366 | } 367 | } 368 | 369 | // Update progress bars 370 | if ($("#modal_scanner").hasClass("in")) { 371 | $("#progress_scan").css("width", scanProgress + "%"); 372 | $("#span_scan_progress").text(T("{0} %", scanProgress)); 373 | 374 | $("#progress_scan_postprocessing").css("width", postProcessingProgress + "%"); 375 | $("#span_scan_postprocessing_progress").text(T("{0} %", postProcessingProgress)); 376 | 377 | $("#progress_scan_upload").css("width", uploadProgress + "%"); 378 | $("#span_scan_upload_progress").text(T("{0} %", uploadProgress)); 379 | } 380 | } 381 | 382 | $("#btn_cancel_scan").click(function() { 383 | if (!$(this).hasClass("disabled")) { 384 | sendGCode("M753"); 385 | $("#modal_scanner").modal("hide"); 386 | } 387 | }); 388 | 389 | $("#btn_cancel_calibration").click(function() { 390 | if (!$(this).hasClass("disabled")) { 391 | sendGCode("M753"); 392 | $(this).addClass("disabled"); 393 | } 394 | }); 395 | 396 | 397 | /* WiFi cam setup dialog (OEM) */ 398 | 399 | $("#modal_wifi_cam form").submit(function(e) { 400 | sendGCode("M118 P1 S\"WIFI " + $("#input_cam_ssid").val() + " " + $("#input_cam_password").val() + "\""); 401 | lastSentGCode = ""; 402 | $("#modal_wifi_cam").modal("hide"); 403 | e.preventDefault(); 404 | }); 405 | 406 | 407 | /* Message Box Dialog */ 408 | 409 | var messageBoxResponse = undefined; 410 | 411 | function updateMessageBox(response) { 412 | var timeout = response.timeout; 413 | response.timeout = 0; 414 | 415 | var stringifiedResponse = JSON.stringify(response); 416 | if (stringifiedResponse != messageBoxResponse) 417 | { 418 | messageBoxResponse = stringifiedResponse; 419 | showMessageBox(response.msg, response.title, response.mode, timeout, response.controls); 420 | } 421 | } 422 | 423 | function closeMessageBox() { 424 | if (messageBoxResponse != undefined) { 425 | if ($("#modal_messagebox").hasClass("in")) { 426 | $("#modal_messagebox").modal("hide"); 427 | } 428 | messageBoxResponse = undefined; 429 | } 430 | } 431 | 432 | 433 | var messageBoxTimer = undefined; 434 | 435 | function showMessageBox(message, title, mode, timeout, controls) { 436 | // Display message, title and optionally show Z controls 437 | $("#h3_messagebox").html(message); 438 | $("#h4_messagebox_title").html(title); 439 | $("#modal_messagebox div.modal-header").toggleClass("hidden", title == ""); 440 | 441 | // Toggle axis control visibility 442 | $("#div_x_controls").toggleClass("hidden", (controls & (1 << 0)) == 0); 443 | $("#div_y_controls").toggleClass("hidden", (controls & (1 << 1)) == 0); 444 | $("#div_z_controls").toggleClass("hidden", (controls & (1 << 2)) == 0); 445 | 446 | // Toggle button visibility 447 | $("#modal_messagebox div.modal-footer").toggleClass("hidden", mode == 0); 448 | $("#modal_messagebox [data-dismiss]").toggleClass("hidden", mode != 1); 449 | $("#btn_ack_messagebox").toggleClass("hidden", mode == 0 || mode == 1); 450 | $("#btn_cancel_messagebox").toggleClass("hidden", mode != 3); 451 | 452 | // Show message box 453 | var backdropValue = (mode != 1) ? "static" : "true"; 454 | $("#modal_messagebox").modal({ backdrop: backdropValue }); 455 | 456 | var data = $("#modal_messagebox").data("bs.modal"); 457 | data.options.backdrop = backdropValue; 458 | $("#modal_messagebox").data("bs.modal", data); 459 | 460 | // Take care of the timeouts 461 | if (messageBoxTimer != undefined) { 462 | clearTimeout(messageBoxTimer); 463 | } 464 | if (timeout > 0) { 465 | messageBoxTimer = setTimeout(function() { 466 | messageBoxTimer = undefined; 467 | $("#modal_messagebox").modal("hide"); 468 | }, timeout * 1000); 469 | } 470 | } 471 | 472 | $("#btn_ack_messagebox").click(function() { 473 | $("#modal_messagebox").modal("hide"); 474 | sendGCode("M292"); 475 | }); 476 | 477 | $("#btn_cancel_messagebox").click(function() { 478 | $("#modal_messagebox").modal("hide"); 479 | sendGCode("M292 P1"); 480 | }); 481 | 482 | $('#modal_messagebox').on("hide.bs.modal", function() { 483 | if (messageBoxTimer != undefined) { 484 | clearTimeout(messageBoxTimer); 485 | messageBoxTimer = undefined; 486 | } 487 | 488 | // FIXME: This is needed to ensure the backdrop always works as intended 489 | $('#modal_messagebox').removeData(); 490 | }); 491 | 492 | /* Set mesh grid area */ 493 | 494 | $("#a_define_mesh").click(function(e) { 495 | $("#div_mesh_x, #div_mesh_y").toggleClass("hidden", geometry == "delta"); 496 | $("#div_mesh_delta").toggleClass("hidden", geometry != "delta"); 497 | $("#modal_define_mesh").modal("show"); 498 | e.preventDefault(); 499 | }); 500 | 501 | 502 | $("#modal_define_mesh form").submit(function(e) { 503 | if (geometry != "delta") { 504 | var minX = $("#input_mesh_x_min").val(); 505 | var maxX = $("#input_mesh_x_max").val(); 506 | var spacingX = $("#input_mesh_x_spacing").val(); 507 | var minY = $("#input_mesh_y_min").val(); 508 | var maxY = $("#input_mesh_y_max").val(); 509 | var spacingY = $("#input_mesh_y_spacing").val(); 510 | sendGCode("M557 X" + minX + ":" + maxX + " Y" + minY + ":" + maxY + " S" + spacingX + ":" + spacingY); 511 | } else { 512 | var radius = $("#input_mesh_radius").val(); 513 | var spacing = $("#input_mesh_spacing").val(); 514 | sendGCode("M557 R" + radius + " S" + spacing); 515 | } 516 | 517 | $("#modal_define_mesh").modal("hide"); 518 | e.preventDefault(); 519 | }); 520 | 521 | /* Probe Cylinder (OEM) */ 522 | 523 | $("#a_probe_cylinder").click(function(e) { 524 | $("#modal_cylinder").modal("show"); 525 | e.preventDefault(); 526 | }); 527 | 528 | $("#modal_cylinder form").submit(function(e) { 529 | sendGCode("G28 Z\nG92 Z" + ($("#input_cylinder_diameter").val() / 2)); 530 | $("#modal_cylinder").modal("hide"); 531 | e.preventDefault(); 532 | }); 533 | -------------------------------------------------------------------------------- /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.toFixed(1) + " 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) || (i > 0 && vendor == "diabase"); 358 | $("#div_extruders > div > .extr-" + i).toggleClass("hidden", !toolHasDrive); 359 | } 360 | 361 | hideExtruderInputs = (drives.length < 2) || (vendor == "diabase"); 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 and highlight current tool 380 | $("#table_tools > tbody > tr").each(function() { 381 | $(this).toggleClass("active", $(this).data("tool") == toolNumber); 382 | $(this).find("th:first-child > a > span:first-child").css("text-decoration", ($(this).data("tool") == toolNumber) ? "underline" : ""); 383 | }); 384 | 385 | // Update tools on the Settings page 386 | $("#page_tools button.btn-select-tool").prop("title", T("Select this tool")).html(' ' + T("Select") + ''); 387 | $("#page_tools div[data-tool='" + toolNumber + "'] button.btn-select-tool").prop("title", T("Deselect this tool")).html(' ' + T("Deselect") + ''); 388 | } 389 | 390 | 391 | /* Control state management */ 392 | 393 | function enableControls() { 394 | $(".table-axis-positions td").css("cursor", "pointer"); 395 | $("nav input, #div_tools_heaters input, #div_content input").prop("disabled", false); // Generic inputs 396 | $("#page_tools label").removeClass("disabled"); // and on Settings page 397 | $(".machine-button").removeClass("disabled"); 398 | 399 | $(".btn-emergency-stop, .gcode-input button[type=submit], .gcode").removeClass("disabled"); // Navbar 400 | $(".bed-temp, .gcode, .heater-temp, .btn-upload").removeClass("disabled"); // List items and Upload buttons 401 | 402 | $(".mobile-home-buttons button, #btn_homeall, .table-move a").removeClass("disabled"); // Move buttons 403 | $("#btn_bed_dropdown").removeClass("disabled"); // Automatic Bed Compensation 404 | $("#panel_extrude label.btn, #panel_extrude button").removeClass("disabled"); // Extruder Control 405 | $("#panel_control_misc label.btn").removeClass("disabled"); // ATX Power 406 | for(var fan = -1; fan < maxFans; fan++) { 407 | var fanID = (fan == -1) ? "tool": fan; 408 | $("#slider_fan_control_" + fanID).slider("enable"); // Fan 409 | $("#slider_fan_job_" + fanID).slider("enable"); // Control 410 | } 411 | 412 | $("#page_scanner button").removeClass("disabled"); // Scanner 413 | $("#page_job .checkbox, #btn_baby_down, #btn_baby_up").removeClass("disabled"); // Job Control 414 | $(".table-fan-control tr > td:not(:first-child) > button").removeClass("disabled"); // Fan Control 415 | $("#slider_speed").slider("enable"); // Speed Factor 416 | for(var extr = 0; extr < maxExtruders; extr++) { 417 | $("#slider_extr_" + extr).slider("enable"); // Extrusion Factors 418 | } 419 | 420 | $(".online-control").removeClass("hidden"); // G-Code/Macro Files 421 | 422 | $('[data-setting="settingsOnDuet"]').prop("disabled", location.host == ""); 423 | $(".btn-apply-settings, .btn-reset-settings").removeClass("disabled"); // Settings 424 | } 425 | 426 | function disableControls() { 427 | $(".table-axis-positions td").css("cursor", ""); 428 | $("nav input, #div_tools_heaters input, #div_content input").prop("disabled", true); // Generic inputs 429 | $("#page_general input, #page_ui input, #page_listitems input").prop("disabled", false); // ... except ... 430 | $("#page_tools label").addClass("disabled"); // ... for Settings 431 | $(".machine-button").addClass("disabled"); 432 | 433 | $(".btn-emergency-stop, .gcode-input button[type=submit], .gcode").addClass("disabled"); // Navbar 434 | $(".bed-temp, .gcode, .heater-temp, .btn-upload").addClass("disabled"); // List items and Upload buttons 435 | 436 | $(".mobile-home-buttons button, #btn_homeall, #table_move_head a").addClass("disabled"); // Move buttons 437 | $("#btn_bed_dropdown").addClass("disabled"); // Automatic Bed Compensation 438 | $("#panel_extrude label.btn, #panel_extrude button").addClass("disabled"); // Extruder Control 439 | $("#panel_control_misc label.btn").addClass("disabled"); // ATX Power 440 | for(var fan = -1; fan < maxFans; fan++) { 441 | var fanID = (fan == -1) ? "tool": fan; 442 | $("#slider_fan_control_" + fanID).slider("disable"); // Fan 443 | $("#slider_fan_job_" + fanID).slider("disable"); // Control 444 | } 445 | 446 | $("#page_scanner button").addClass("disabled"); // Scanner 447 | $("#btn_pause, #page_job .checkbox, #btn_baby_down, #btn_baby_up").addClass("disabled"); // Job Control 448 | $(".table-fan-control tr > td:not(:first-child) > button").addClass("disabled"); // Fan Control 449 | $("#slider_speed").slider("disable"); // Speed Factor 450 | for(var extr = 0; extr < maxExtruders; extr++) { 451 | $("#slider_extr_" + extr).slider("disable"); // Extrusion Factors 452 | } 453 | 454 | $(".online-control").addClass("hidden"); // G-Code/Macro Files 455 | 456 | $(".btn-apply-settings, .btn-reset-settings").toggleClass("disabled", $("[data-setting='settingsOnDuet']").is(":checked")); 457 | } 458 | 459 | 460 | /* Window size queries */ 461 | 462 | function windowIsXsSm() { 463 | return window.matchMedia('(max-width: 991px)').matches; 464 | } 465 | 466 | function windowIsMdLg() { 467 | return window.matchMedia('(min-width: 992px)').matches; 468 | } 469 | 470 | 471 | /* Misc */ 472 | 473 | function arraysEqual(a, b) { 474 | if (a != undefined && b != undefined && a.length == b.length) { 475 | for(var i = 0; i < a.length; i++) { 476 | if (a[i].constructor === Array && b[i].constructor === Array) { 477 | if (!arraysEqual(a[i], b[i])) { 478 | return false; 479 | } 480 | } else if (a[i] != b[i]) { 481 | return false; 482 | } 483 | } 484 | return true; 485 | } 486 | return false; 487 | } 488 | 489 | function log(style, message) { 490 | var entry = '
    '; 491 | entry += '
    ' + (new Date()).toLocaleTimeString() + '
    '; 492 | entry += '
    ' + message + '
    '; 493 | $("#console_log").prepend(entry); 494 | } 495 | 496 | var audioContext = new (window.AudioContext || window.webkitAudioContext); 497 | function beep(frequency, duration) { 498 | var oscillator = audioContext.createOscillator(); 499 | 500 | oscillator.type = "sine"; 501 | oscillator.frequency.value = frequency; 502 | oscillator.connect(audioContext.destination); 503 | oscillator.start(); 504 | 505 | setTimeout(function() { 506 | oscillator.disconnect(); 507 | }, duration); 508 | } 509 | -------------------------------------------------------------------------------- /core/js/settings.js: -------------------------------------------------------------------------------- 1 | /* Settings management for Duet Web Control 2 | * 3 | * written by Christian Hammacher (c) 2016-2018 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 | updateInterval: 250, // in ms 12 | extendedStatusInterval: 10, // nth status request will include extended values 13 | maxRetries: 2, // number of AJAX retries before the connection is terminated 14 | 15 | haltedReconnectDelay: 10000, // in ms (increased from 5000 for Duet WiFi) 16 | updateReconnectDelay: 20000, // in ms 17 | dwsReconnectDelay: 45000, // in ms 18 | dwcReconnectDelay: 225000, // in ms (only for SPIFFS updates) 19 | 20 | showConnect: false, // whether the Connect button is shown on remote connections 21 | confirmStop: false, // ask for confirmation when pressing Emergency STOP 22 | useKiB: true, // display file sizes in KiB instead of KB 23 | theme: "default", // name of the theme to use 24 | scrollContent: true, // make the main content scrollable on md+ resolutions 25 | showFanControl: true, // show fan sliders 26 | showFanRPM: false, // show fan RPM in sensors 27 | settingsOnDuet: true, // store the DWC settings on the Duet 28 | language: "en", 29 | 30 | moveFeedrate: 6000, // in mm/min 31 | axisMoveSteps: [ // in mm 32 | [100, 50, 10, 1, 0.1], 33 | [100, 50, 10, 1, 0.1], 34 | [50, 25, 5, 0.5, 0.05], 35 | [100, 50, 10, 1, 0.1], 36 | [100, 50, 10, 1, 0.1], 37 | [100, 50, 10, 1, 0.1], 38 | [100, 50, 10, 1, 0.1], 39 | [100, 50, 10, 1, 0.1], 40 | [100, 50, 10, 1, 0.1], 41 | ], 42 | extruderAmounts: [100, 50, 20, 10, 5, 1], // in mm 43 | extruderFeedrates: [60, 30, 15, 5, 1], // in mm/s 44 | babysteppingZ: 0.05, // in mm 45 | showATXControl: false, // show ATX control 46 | 47 | uppercaseGCode: true, // convert G-Codes to upper-case before sending them 48 | 49 | doTfree: true, // tool 50 | doTpre: true, // change 51 | doTpost: true, // options 52 | 53 | useHtmlNotifications: false, // use HTML5-based notifications 54 | autoCloseUserMessages: false, // whether M117 messages are automatically closed 55 | autoCloseErrorMessages: false, // whether error messages by the firmware are automatically closed 56 | showEmptyResponses: false, // show successful pop-up notification for G-codes that returned no response 57 | showInfoMessages: true, // show info messages 58 | showWarningMessages: true, // show warning messages 59 | showErrorMessages: true, // show error messages 60 | notificationTimeout: 5000, // timeout of pop-up notifications in ms 61 | maxNotifications: 3, // maximum number of simultaneously opened notifications 62 | 63 | webcamURL: "", 64 | webcamInterval: 5000, // in ms 65 | webcamFix: false, // do not append extra HTTP qualifier when reloading images 66 | webcamEmbedded: false, // use iframe to embed webcam stream 67 | webcamRotation: 0, 68 | webcamFlip: "none", 69 | 70 | defaultActiveTemps: [0, 180, 190, 200, 210, 220, 235], 71 | defaultStandbyTemps: [0, 95, 120, 140, 155, 170], 72 | defaultBedTemps: [0, 55, 60, 65, 90, 110, 120] 73 | }; 74 | var needsInitialSettingsUpload = false; 75 | 76 | var defaultSettings = jQuery.extend(true, {}, settings); // need to do this to get a valid copy 77 | 78 | var themeInclude; 79 | 80 | var rememberedGCodes = ["M0", "M1", "M84", "M561"], defaultGCodes = rememberedGCodes.slice(); 81 | 82 | 83 | /* Safe wrappers for localStorage */ 84 | 85 | function getLocalSetting(key, defaultValue) { 86 | if (typeof localStorage === "undefined") { 87 | return defaultValue; 88 | } 89 | 90 | var value = localStorage.getItem(key); 91 | if (value == undefined || value.length == 0 || 92 | (defaultValue != undefined && value.constructor !== defaultValue.constructor)) { 93 | return defaultValue; 94 | } 95 | return JSON.parse(value); 96 | } 97 | 98 | function setLocalSetting(key, value) { 99 | if (typeof localStorage !== "undefined") { 100 | localStorage.setItem(key, JSON.stringify(value)); 101 | } 102 | } 103 | 104 | function removeLocalSetting(key) { 105 | if (typeof localStorage !== "undefined") { 106 | localStorage.removeItem(key); 107 | } 108 | } 109 | 110 | 111 | /* Setting methods */ 112 | 113 | function loadSettings() { 114 | // Delete cookie (usually not used unless a really old web interface version was used before) 115 | document.cookie = "settings=; expires=Thu, 01 Jan 1970 00:00:00 UTC"; 116 | 117 | // Try to parse the stored settings (if any) 118 | var loadedSettings = getLocalSetting("settings", null); 119 | if (loadedSettings != null) { 120 | for(var key in settings) { 121 | // Try to copy each setting if their types are equal 122 | if (loadedSettings.hasOwnProperty(key) && loadedSettings[key] != null && settings[key].constructor === loadedSettings[key].constructor) { 123 | settings[key] = loadedSettings[key]; 124 | } 125 | } 126 | 127 | // Backward-compatibility 128 | if (loadedSettings.hasOwnProperty("useDarkTheme")) { 129 | settings.theme = loadedSettings.useDarkTheme ? "dark" : "default"; 130 | } 131 | } 132 | 133 | // Do NOT allow storage of the settings on the Duet if this is running on localhost 134 | if (location.host == "") { 135 | settings.settingsOnDuet = false; 136 | defaultSettings = jQuery.extend(true, {}, settings); 137 | } 138 | 139 | // See if we need to fetch the settings once again from the Duet 140 | if (settings.settingsOnDuet) { 141 | $.ajax(ajaxPrefix + "dwc.json", { 142 | type: "GET", 143 | dataType: "json", 144 | global: false, 145 | error: function(jqxhr, xhrsettings, thrownError) { 146 | needsInitialSettingsUpload = (jqxhr.status == 404); 147 | settingsLoaded(); 148 | }, 149 | success: function(response) { 150 | for(var key in settings) { 151 | // Try to copy each setting if their types are equal 152 | if (response.hasOwnProperty(key) && response[key] != null && settings[key].constructor === response[key].constructor) { 153 | settings[key] = response[key]; 154 | } 155 | } 156 | settingsLoaded(); 157 | } 158 | }); 159 | } else { 160 | settingsLoaded(); 161 | } 162 | } 163 | 164 | function settingsLoaded() { 165 | // Apply them 166 | applySettings(); 167 | 168 | // Try to load the translation data 169 | if (translationData == undefined) { 170 | $.ajax("language.xml", { 171 | type: "GET", 172 | dataType: "xml", 173 | global: false, 174 | error: pageLoadComplete, 175 | success: function(response) { 176 | translationData = response; 177 | 178 | if (translationData.children == undefined) { 179 | // Internet Explorer and Edge cannot deal with XML files in the way we want. 180 | // Disable translations for those browsers. 181 | translationData = undefined; 182 | $("#dropdown_language, #label_language").addClass("hidden"); 183 | } else { 184 | $("#dropdown_language ul > li:not(:first-child)").remove(); 185 | for(var i = 0; i < translationData.children[0].children.length; i++) { 186 | var id = translationData.children[0].children[i].tagName; 187 | var name = translationData.children[0].children[i].attributes["name"].value; 188 | $("#dropdown_language ul").append('
  • ' + name + '
  • '); 189 | if (settings.language == id) { 190 | $("#btn_language > span:first-child").text(name); 191 | } 192 | } 193 | translatePage(); 194 | } 195 | pageLoadComplete(); 196 | } 197 | }); 198 | } 199 | } 200 | 201 | function applySettings() { 202 | /* Apply settings */ 203 | 204 | // Show connect button 205 | $(".btn-connect").toggleClass("hidden", !settings.showConnect && location.host != ""); 206 | 207 | // Set AJAX timeout 208 | if (settings.maxRetries > 50) { settings.maxRetries = 50; } 209 | $.ajaxSetup({ timeout: sessionTimeout / (settings.maxRetries + 1) }); 210 | 211 | // Webcam 212 | if (settings.webcamURL != "") { 213 | $("#panel_webcam").removeClass("hidden"); 214 | $("#img_webcam").toggleClass("hidden", settings.webcamEmbedded); 215 | $("#div_ifm_webcam").toggleClass("hidden", !settings.webcamEmbedded); 216 | 217 | $("#img_webcam, #ifm_webcam").toggleClass("rotate-90", settings.webcamRotation == 90); 218 | $("#img_webcam, #ifm_webcam").toggleClass("rotate-180", settings.webcamRotation == 180); 219 | $("#img_webcam, #ifm_webcam").toggleClass("rotate-270", settings.webcamRotation == 270); 220 | $("#img_webcam, #ifm_webcam").toggleClass("flip-x", settings.webcamFlip == "x" || settings.webcamFlip == "both"); 221 | $("#img_webcam, #ifm_webcam").toggleClass("flip-y", settings.webcamFlip == "y" || settings.webcamFlip == "both"); 222 | 223 | updateWebcam(true); 224 | updateCalibrationWebcam(true); 225 | } else { 226 | $("#panel_webcam").addClass("hidden"); 227 | } 228 | 229 | // Movement steps 230 | applyMovementSteps(); 231 | 232 | // Babystepping 233 | $("#btn_baby_down > span.content").text(T("{0} mm", (-settings.babysteppingZ))); 234 | $("#btn_baby_up > span.content").text(T("{0} mm ", "+" + settings.babysteppingZ)); 235 | $(".babystepping").toggleClass("hidden", settings.babysteppingZ <= 0); 236 | 237 | // Show/Hide Fan Controls 238 | $(".fan-control").toggleClass("hidden", !settings.showFanControl); 239 | 240 | // Show/Hide Fan RPM 241 | $(".fan-rpm").toggleClass("hidden", !settings.showFanRPM); 242 | $("#th_probe, #td_probe").css("border-right", settings.showFanRPM ? "" : "0px"); 243 | 244 | // Show/Hide ATX Power 245 | $(".atx-control").toggleClass("hidden", !settings.showATXControl); 246 | $("#panel_control_misc").toggleClass("hidden", !settings.showFanControl && !settings.showATXControl); 247 | 248 | // Apply or revoke theme 249 | if (themeInclude != undefined) { 250 | themeInclude.remove(); 251 | themeInclude = undefined; 252 | } 253 | 254 | switch (settings.theme) { 255 | case "default": // Default Bootstrap theme 256 | themeInclude = $(''); 257 | themeInclude.appendTo("head"); 258 | break; 259 | 260 | case "none": // No theme at all 261 | applyThemeColors(); 262 | break; 263 | 264 | default: // Custom theme 265 | themeInclude = $(''); 266 | themeInclude.appendTo("head"); 267 | break; 268 | 269 | } 270 | 271 | // Make main content scrollable on md+ screens or restore default behavior 272 | $("#div_content").css("overflow-y", (settings.scrollContent) ? "auto" : "").resize(); 273 | 274 | /* Set values on the Settings page */ 275 | 276 | // Set input values 277 | for(var key in settings) { 278 | var element = $('[data-setting="' + key + '"]'); 279 | if (element.length != 0) { 280 | var type = element.attr("type"); 281 | if (type == "checkbox") { 282 | element.prop("checked", settings[key]); 283 | } else if (type == "number") { 284 | var factor = element.data("factor"); 285 | if (factor == undefined) { 286 | factor = 1; 287 | } 288 | element.val(settings[key] / factor); 289 | } else if (type != "button") { 290 | element.val(settings[key]); 291 | } 292 | } 293 | } 294 | 295 | // Set theme selection 296 | $("#btn_theme").data("theme", settings.theme); 297 | var themeName = settings.theme == "default" ? "Bootstrap" : (settings.theme == "none" ? T("None") : settings.theme); 298 | $("#btn_theme > span:first-child").text(themeName); 299 | 300 | // Language is set in XML AJAX handler 301 | 302 | // Default head temperatures 303 | clearHeadTemperatures(); 304 | settings.defaultActiveTemps.forEach(function(temp) { 305 | addHeadTemperature(temp, "active"); 306 | }); 307 | settings.defaultStandbyTemps.forEach(function(temp) { 308 | addHeadTemperature(temp, "standby"); 309 | }); 310 | 311 | // Default bed temperatures 312 | clearBedTemperatures(); 313 | settings.defaultBedTemps.forEach(function(temp) { 314 | addBedTemperature(temp); 315 | }); 316 | 317 | // Force GUI update to apply half Z movements in the axes 318 | updateGui(); 319 | } 320 | 321 | function applyMovementSteps() { 322 | // Check values 323 | for(var axis = 0; axis < settings.axisMoveSteps.length; axis++) { 324 | if (settings.axisMoveSteps[axis].length < 5) { 325 | settings.axisMoveSteps[axis].splice(1, 0, (axis == 2) ? 25 : 50); 326 | } 327 | } 328 | 329 | // Axis apperance 330 | for(var i = 0; i < axisNames.length; i++) { 331 | // Set Home button names+titles 332 | var axis = axisNames[i]; 333 | var axisIndex = axisOrder.indexOf(axis); 334 | $(".btn-home[data-axis='" + i + "']").text(T("Home " + axis)).prop("title", T("Home " + axis + " axis (G28 " + axis + ")")); 335 | 336 | // Set labels and values for decrease and increase buttons 337 | var buttonIndex = 0; 338 | $("#page_control a.btn-move[data-axis='" + i + "'][data-amount^='-']").each(function() { 339 | var decreaseVal = -settings.axisMoveSteps[axisIndex][buttonIndex++]; 340 | $(this).data("amount", decreaseVal).contents().last().replaceWith(" " + axis + decreaseVal); 341 | }); 342 | buttonIndex = 0; 343 | $("#modal_messagebox button.btn-move[data-axis-letter='" + axis + "'][data-amount^='-']").each(function() { 344 | var decreaseVal = -settings.axisMoveSteps[axisIndex][buttonIndex++]; 345 | $(this).data("amount", decreaseVal).children("span:last-child").text(axis + decreaseVal); 346 | }); 347 | buttonIndex = 0; 348 | $("#modal_start_scan button.btn-move[data-axis-letter='" + axis + "'][data-amount^='-']").each(function() { 349 | var decreaseVal = -settings.axisMoveSteps[axisIndex][buttonIndex++]; 350 | $(this).data("amount", decreaseVal).children("span:last-child").text(axis + decreaseVal); 351 | }); 352 | 353 | buttonIndex = settings.axisMoveSteps[axisIndex].length - 1; 354 | $("#page_control a.btn-move[data-axis='" + i + "']:not([data-amount^='-'])").each(function() { 355 | var increaseVal = settings.axisMoveSteps[axisIndex][buttonIndex--]; 356 | $(this).data("amount", increaseVal).contents().first().replaceWith(axis + "+" + increaseVal + " "); 357 | }); 358 | buttonIndex = settings.axisMoveSteps[axisIndex].length - 1; 359 | $("#modal_messagebox button.btn-move[data-axis-letter='" + axis + "']:not([data-amount^='-'])").each(function() { 360 | var increaseVal = settings.axisMoveSteps[axisIndex][buttonIndex--]; 361 | $(this).data("amount", increaseVal).children("span:first-child").text(axis + "+" + increaseVal); 362 | }); 363 | buttonIndex = settings.axisMoveSteps[axisIndex].length - 1; 364 | $("#modal_start_scan button.btn-move[data-axis-letter='" + axis + "']:not([data-amount^='-'])").each(function() { 365 | var increaseVal = settings.axisMoveSteps[axisIndex][buttonIndex--]; 366 | $(this).data("amount", increaseVal).children("span:first-child").text(axis + "+" + increaseVal); 367 | }); 368 | 369 | // Set headers for position cells in the Machine Status panel 370 | $(".table-axis-positions th[data-axis='" + i + "']").text(axisNames[i]); 371 | } 372 | 373 | // Extruder amounts 374 | var amountIndex = 0; 375 | $("#div_feed > div.btn-group > label").each(function() { 376 | var amount = settings.extruderAmounts[amountIndex++]; 377 | $(this).children("input").prop("value", amount); 378 | $(this).children("span").text(amount); 379 | }); 380 | 381 | // Extruder feedrates 382 | var feedrateIndex = 0; 383 | $("#div_feedrate > div.btn-group > label").each(function() { 384 | var feedrate = settings.extruderFeedrates[feedrateIndex++]; 385 | $(this).children("input").prop("value", feedrate); 386 | $(this).children("span").text(feedrate); 387 | }); 388 | } 389 | 390 | function saveSettings() { 391 | // Get input values 392 | for(var key in settings) { 393 | var element = $('[data-setting="' + key + '"]'); 394 | if (element.length != 0) { 395 | var type = element.attr("type"); 396 | if (type == "checkbox") { 397 | settings[key] = element.is(":checked"); 398 | } else if (type == "number") { 399 | var min = element.data("min"); 400 | var max = element.data("max"); 401 | var factor = element.data("factor"); 402 | if (factor == undefined) { 403 | factor = 1; 404 | } 405 | settings[key] = constrainSetting(element.val() * factor, defaultSettings[key], min, max); 406 | } else { 407 | settings[key] = element.val(); 408 | } 409 | } 410 | } 411 | 412 | // Save theme 413 | settings.theme = $("#btn_theme").data("theme"); 414 | 415 | // Save language 416 | if (settings.language != $("#btn_language").data("language")) { 417 | showMessage("success", T("Language has changed"), T("You have changed the current language. Please reload the web interface to apply this change."), 0); 418 | } 419 | settings.language = $("#btn_language").data("language"); 420 | 421 | // G-Codes for autocompletion 422 | rememberedGCodes = []; 423 | $("#table_gcodes > tbody > tr").each(function() { 424 | rememberedGCodes.push($(this).find("th:eq(0)").text()); 425 | }); 426 | saveGCodes(); 427 | 428 | // Default Heater Temperatures 429 | settings.defaultActiveTemps = []; 430 | $("#ul_active_temps > li").each(function() { 431 | settings.defaultActiveTemps.push($(this).data("temperature")); 432 | }); 433 | settings.defaultActiveTemps = settings.defaultActiveTemps.sort(function(a, b) { return a - b; }); 434 | settings.defaultStandbyTemps = []; 435 | $("#ul_standby_temps > li").each(function() { 436 | settings.defaultStandbyTemps.push($(this).data("temperature")); 437 | }); 438 | settings.defaultStandbyTemps = settings.defaultStandbyTemps.sort(function(a, b) { return a - b; }); 439 | 440 | // Default Bed Temperatures 441 | settings.defaultBedTemps = []; 442 | $("#ul_bed_temps > li").each(function() { 443 | settings.defaultBedTemps.push($(this).data("temperature")); 444 | }); 445 | settings.defaultBedTemps = settings.defaultBedTemps.sort(function(a, b) { return a - b; }); 446 | 447 | // Save Settings 448 | setLocalSetting("settings", settings); 449 | if (settings.settingsOnDuet) { 450 | uploadTextFile("0:/www/dwc.json", JSON.stringify(settings), function() { 451 | // Tell DWC clients to reload its config 452 | sendGCode("M118 P3 S\"{reloadSettings}\"", false); 453 | }, false); 454 | } 455 | } 456 | 457 | function constrainSetting(value, defaultValue, minValue, maxValue) { 458 | if (isNaN(value)) { 459 | return defaultValue; 460 | } 461 | if (value < minValue) { 462 | return minValue; 463 | } 464 | if (value > maxValue) { 465 | return maxValue; 466 | } 467 | 468 | return value; 469 | } 470 | 471 | 472 | /* Remembered G-Codes */ 473 | 474 | function loadGCodes() { 475 | rememberedGCodes = getLocalSetting("rememberedGCodes", defaultGCodes); 476 | applyGCodes(); 477 | } 478 | 479 | function applyGCodes() { 480 | $("#table_gcodes > tbody").children().remove(); 481 | for(var index in rememberedGCodes) { 482 | var item = '' + rememberedGCodes[index] + ''; 483 | item += ''; 485 | $("#table_gcodes > tbody").append(item); 486 | } 487 | } 488 | 489 | function saveGCodes() { 490 | setLocalSetting("rememberedGCodes", rememberedGCodes); 491 | } 492 | 493 | 494 | /* Setting events */ 495 | 496 | // Apply & Reset settings 497 | 498 | $(".btn-reset-settings").click(function(e) { 499 | showConfirmationDialog(T("Reset Settings"), T("Are you sure you want to revert to Factory Settings?"), function() { 500 | if (defaultSettings.language != settings.language) { 501 | showMessage("info", T("Language has changed"), T("You have changed the current language. Please reload the web interface to apply this change."), 0); 502 | } 503 | 504 | settings = jQuery.extend(true, {}, defaultSettings); 505 | if (vendor != undefined) { 506 | $.ajax(ajaxPrefix + "dwc_factory.json", { 507 | type: "GET", 508 | dataType: "json", 509 | global: false, 510 | error: function(jqxhr, xhrsettings, thrownError) { 511 | settings = jQuery.extend(true, {}, defaultSettings); 512 | settingsLoaded(); 513 | }, 514 | success: function(response) { 515 | for(var key in settings) { 516 | // Try to copy each setting if their types are equal 517 | if (response.hasOwnProperty(key) && response[key] != null && settings[key].constructor === response[key].constructor) { 518 | settings[key] = response[key]; 519 | } 520 | } 521 | settingsLoaded(); 522 | } 523 | }); 524 | } else { 525 | $("#btn_language").data("language", "en").children("span:first-child").text("English"); 526 | $("#btn_theme").data("theme", "default").children("span:first-child").text(T("Bootstrap")); 527 | 528 | applySettings(); 529 | saveSettings(); 530 | } 531 | 532 | rememberedGCodes = jQuery.extend(true, {}, defaultGCodes); 533 | applyGCodes(); 534 | saveGCodes(); 535 | 536 | removeLocalSetting("extraSensorVisibility"); 537 | resetChartData(); 538 | 539 | removeLocalSetting("cachedFileInfo"); 540 | }); 541 | e.preventDefault(); 542 | }); 543 | 544 | $("#frm_settings").submit(function(e) { 545 | e.preventDefault(); 546 | if ($(".btn-apply-settings").hasClass("disabled")) { 547 | return; 548 | } 549 | 550 | saveSettings(); 551 | if (!settings.settingsOnDuet) { 552 | applySettings(); 553 | showMessage("success", "", "" + T("Settings applied!") + ""); 554 | } 555 | applyGCodes(); 556 | }); 557 | 558 | $("#frm_settings > ul > li a").on("shown.bs.tab", function(e) { 559 | $("#frm_settings > ul li").removeClass("active"); 560 | var links = $('#frm_settings > ul > li a[href="' + $(this).attr("href") + '"]'); 561 | $.each(links, function() { 562 | $(this).parent().addClass("active"); 563 | }); 564 | }); 565 | 566 | // User Interface 567 | 568 | $("#dropdown_theme > ul").on("click", "a", function(e) { 569 | $("#btn_theme > span:first-child").text($(this).text()); 570 | $("#btn_theme").data("theme", $(this).data("theme")); 571 | e.preventDefault(); 572 | }); 573 | 574 | $("#dropdown_language > ul").on("click", "a", function(e) { 575 | $("#btn_language > span:first-child").text($(this).text()); 576 | $("#btn_language").data("language", $(this).data("language")); 577 | e.preventDefault(); 578 | }); 579 | 580 | $('a[href="#page_ui"]').on('shown.bs.tab', function () { 581 | $("#btn_clear_cache").toggleClass("disabled", $.isEmptyObject(cachedFileInfo)); 582 | }); 583 | 584 | $("#btn_clear_cache").click(function(e) { 585 | gcodeUpdateIndex = -1; 586 | clearFileCache(); 587 | $("#btn_clear_cache").addClass("disabled"); 588 | e.preventDefault(); 589 | }); 590 | 591 | $("[data-setting='useHtmlNotifications']").change(function() { 592 | if ($(this).prop("checked")) { 593 | if (!("Notification" in window)) { 594 | // Don't allow this option to be set if the browser doesn't support it 595 | $(this).prop("checked", false); 596 | alert(T("This browser does not support desktop notification")); 597 | } 598 | else if (Notification.permission !== 'denied') { 599 | $(this).prop("checked", false); 600 | Notification.requestPermission(function(permission) { 601 | if (!('permission' in Notification)) { 602 | Notification.permission = permission; 603 | } 604 | 605 | if (permission === "granted") { 606 | // Don't allow this option to be set unless permission has been granted 607 | $("[data-setting='useHtmlNotifications']").prop("checked", true); 608 | } 609 | }); 610 | } 611 | } 612 | }); 613 | 614 | $("[data-setting='settingsOnDuet']").change(function() { 615 | $(".btn-apply-settings, .btn-reset-settings").toggleClass("disabled", !isConnected && $(this).is(":checked")); 616 | }); 617 | 618 | // List Items 619 | 620 | $("input[name='temp_selection']:radio").change(function() { 621 | if ($(this).val() == "active") { 622 | $("#ul_active_temps").removeClass("hidden"); 623 | $("#ul_standby_temps").addClass("hidden"); 624 | } else { 625 | $("#ul_standby_temps").removeClass("hidden"); 626 | $("#ul_active_temps").addClass("hidden"); 627 | } 628 | }); 629 | 630 | $("#btn_add_head_temp").click(function(e) { 631 | var temperature = constrainSetting($("#input_add_head_temp").val(), 0, -273.15, 300); 632 | var type = $('input[name="temp_selection"]:checked').val(); 633 | 634 | var item = '
  • ' + temperature + ' °C'; 635 | item += '
  • '; 637 | $("#ul_" + type + "_temps").append(item); 638 | 639 | e.preventDefault(); 640 | }); 641 | 642 | $("#btn_add_bed_temp").click(function(e) { 643 | var temperature = constrainSetting($("#input_add_bed_temp").val(), 0, -273.15, 180); 644 | 645 | var item = '
  • ' + temperature + ' °C'; 646 | item += '
  • '; 648 | $("#ul_bed_temps").append(item); 649 | 650 | e.preventDefault(); 651 | }); 652 | 653 | $("body").on("click", ".btn-delete-parent", function(e) { 654 | $(this).parents("tr, li").remove(); 655 | e.preventDefault(); 656 | }); 657 | 658 | $("#page_listitems input").on("input", function() { 659 | $("#btn_add_head_temp").toggleClass("disabled", isNaN(parseFloat($("#input_add_head_temp").val()))); 660 | $("#btn_add_bed_temp").toggleClass("disabled", isNaN(parseFloat($("#input_add_bed_temp").val()))); 661 | }); 662 | 663 | $("#page_listitems input").keydown(function(e) { 664 | if (e.which == 13) { 665 | var button = $(this).closest("div:not(.input-group):eq(0)").find("button"); 666 | if (!button.hasClass("disabled")) { 667 | button.click(); 668 | } 669 | e.preventDefault(); 670 | } 671 | }); 672 | 673 | // Machine Properties 674 | 675 | $("#btn_fw_diagnostics").click(function() { 676 | if (isConnected) { 677 | sendGCode("M122"); 678 | showPage("console"); 679 | } 680 | }); 681 | 682 | // Tools 683 | 684 | $("#btn_add_tool").click(function(e) { 685 | var gcode = "M563 P" + $("#input_tool_number").val() + " S\"" + $("#input_tool_name").val() + "\""; 686 | 687 | var drives = $("input[name='tool_drives']:checked"); 688 | if (drives.length > 0) { 689 | var driveList = []; 690 | drives.each(function() { driveList.push($(this).val()); }); 691 | gcode += " D" + driveList.reduce(function(a, b) { return a + ":" + b; }); 692 | } 693 | 694 | var heaters = $("input[name='tool_heaters']:checked"); 695 | if (heaters.length > 0) { 696 | var heaterList = []; 697 | heaters.each(function() { heaterList.push($(this).val()); }); 698 | gcode += " H" + heaterList.reduce(function(a, b) { return a + ":" + b; }); 699 | } 700 | 701 | sendGCode(gcode); 702 | extendedStatusCounter = settings.extendedStatusInterval; 703 | 704 | e.preventDefault(); 705 | }); 706 | 707 | // Display toggle for settings sub-pages 708 | 709 | $('a[href="#page_general"], a[href="#page_ui"], a[href="#page_listitems"]').on('shown.bs.tab', function () { 710 | $("#row_save_settings").removeClass("hidden"); 711 | }); 712 | 713 | $('a[href="#page_machine"], a[href="#page_tools"], a[href="#page_sysedit"], a[href="#page_display"]').on('shown.bs.tab', function () { 714 | $("#row_save_settings").addClass("hidden"); 715 | }); 716 | 717 | -------------------------------------------------------------------------------- /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, refreshSysFiles; 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 | if (errorThrown == "abort") { 56 | // Ignore this error if this request was cancelled intentionally 57 | return; 58 | } 59 | 60 | // Retry upload if this file is smaller than 1MiB 61 | if (file.size < 1048576) { 62 | if (!this.hasOwnProperty("retryCount")) { 63 | this.retryCount = 1; 64 | } else { 65 | this.retryCount++; 66 | } 67 | 68 | if (this.retryCount <= settings.maxRetries) { 69 | $.ajax(this); 70 | return; 71 | } 72 | } 73 | 74 | // Resume status updates and report and error if that failed 75 | startUpdates(); 76 | showMessage("danger", T("Error"), T("Could not update file {0}!", filename)); 77 | console.log("Text file upload failed!\nStatus: " + textStatus + "\nError: " + errorThrown); 78 | }, 79 | success: function(response) { 80 | if (response.err == 0) { 81 | if (showNotification) { 82 | showMessage("success", T("File Updated"), T("The file {0} has been successfully uploaded.", filename)); 83 | if (lastStatusResponse != undefined && lastStatusResponse.status == 'I') { 84 | if (filename.toLowerCase() == "0:/sys/config.g") { 85 | showConfirmationDialog(T("Reboot Duet?"), T("You have just uploaded a config file. Would you like to reboot your Duet now?"), function() { 86 | // Perform software reset 87 | sendGCode("M999"); 88 | }); 89 | } 90 | } 91 | } 92 | } else { 93 | showMessage("danger", T("Error"), T("Could not update file {0}!", filename)); 94 | } 95 | 96 | startUpdates(); 97 | if (callback != undefined) { 98 | callback(); 99 | } 100 | } 101 | }); 102 | } 103 | 104 | function startUpload(type, files, fromCallback) { 105 | // Unzip files if necessary 106 | if (type == "filament" || type == "macro" || type == "generic") { 107 | var containsZip = false; 108 | $.each(files, function() { 109 | if (this.name.toLowerCase().match("\\.zip$") != null) { 110 | uploadedDWC |= this.name.toLowerCase().match("^duetwebcontrol.*\\.zip") != null; 111 | 112 | var fileReader = new FileReader(); 113 | fileReader.onload = (function(theFile) { 114 | return function(e) { 115 | try { 116 | var zipFile = new JSZip(), filesToUnpack = 0, filesUnpacked = 0; 117 | zipFile.loadAsync(e.target.result).then(function(zip) { 118 | var zipFiles = []; 119 | $.each(zip.files, function(index, zipEntry) { 120 | if (!zipEntry.dir && zipEntry.name.indexOf(".") != 0 && zipEntry.name.match("README") == null) { 121 | var zipName = zipEntry.name; 122 | if (type != "filament") { 123 | zipName = zipEntry.name.split("/"); 124 | zipName = zipName[zipName.length - 1]; 125 | } 126 | 127 | filesToUnpack++; 128 | zipEntry.async("arraybuffer").then(function(buffer) { 129 | // See above. FileAPI isn't supported by IE+Edge 130 | //var unpackedFile = new File([zipEntry.asArrayBuffer()], zipName, { type: "application/octet-stream", lastModified: zipEntry.date }); 131 | var unpackedFile = new Blob([buffer], { type: "application/octet-stream" }); 132 | unpackedFile.name = zipName; 133 | unpackedFile.lastModified = zipEntry.date; 134 | zipFiles.push(unpackedFile); 135 | 136 | filesUnpacked++; 137 | if (filesToUnpack == filesUnpacked) { 138 | // This had to be moved because the new JSZip API is completely async 139 | startUpload(type, zipFiles, true); 140 | } 141 | }); 142 | } 143 | }); 144 | 145 | if (zip.files.length == 0) { 146 | showMessage("warning", T("Error"), T("The archive {0} does not contain any files!", theFile.name)); 147 | } 148 | }); 149 | } catch(e) { 150 | console.log("ZIP error: " + e); 151 | showMessage("danger", T("Error"), T("Could not read contents of file {0}!", theFile.name)); 152 | } 153 | } 154 | })(this); 155 | fileReader.readAsArrayBuffer(this); 156 | 157 | containsZip = true; 158 | return false; 159 | } 160 | }); 161 | 162 | if (containsZip) { 163 | // We're relying on an async task which will trigger this method again when required 164 | return false; 165 | } 166 | } 167 | 168 | // Safety check for Upload and Start 169 | if (type == "start" && files.length > 1) { 170 | showMessage("warning", T("Error"), T("You can only upload and start one file at once!")); 171 | return false; 172 | } 173 | 174 | // Safety check for Filament uploads 175 | if (type == "filament") { 176 | var hadError = false; 177 | $.each(files, function() { 178 | if (this.name.indexOf("/") == -1) { 179 | showMessage("danger", T("Error"), T("You cannot upload single files. Please upload only ZIP files that contain at least one directory per filament configuration.")); 180 | hadError = true; 181 | return false; 182 | } 183 | }); 184 | if (hadError) { 185 | return false; 186 | } 187 | } 188 | 189 | // Initialize some values 190 | stopUpdates(); 191 | isUploading = true; 192 | uploadType = type; 193 | uploadTotalBytes = uploadedTotalBytes = uploadedFileCount = 0; 194 | uploadFiles = files; 195 | $.each(files, function() { 196 | uploadTotalBytes += this.size; 197 | }); 198 | uploadRows = []; 199 | if (!fromCallback) { 200 | uploadedDWC = false; 201 | } 202 | uploadIncludedConfig = false; 203 | uploadFirmwareFile = uploadDWCFile = uploadDWSFile = uploadDWSSFile = undefined; 204 | uploadFilesSkipped = uploadHadError = refreshSysFiles = false; 205 | 206 | // Reset modal dialog 207 | $("#modal_upload").data("backdrop", "static"); 208 | $("#modal_upload .close, #modal_upload button[data-dismiss='modal']").addClass("hidden"); 209 | $("#btn_cancel_upload, #modal_upload p").removeClass("hidden"); 210 | $("#modal_upload h4").text(T("Uploading File(s), {0}% Complete", 0)); 211 | 212 | // Add files to the table 213 | $("#table_upload_files > tbody").children().remove(); 214 | $.each(files, function() { 215 | if (type == "generic") { 216 | // config and firmware files can be always uploaded 217 | uploadIncludedConfig |= (this.name == "config.g"); 218 | if (this.name.toUpperCase().match("^" + firmwareFileName.toUpperCase() + ".*\.BIN") != null) { 219 | uploadFirmwareFile = this.name; 220 | targetFirmwareFileName = firmwareFileName; 221 | } 222 | else if (allowCombinedFirmware && this.name.toUpperCase().match("^DUET2COMBINEDFIRMWARE.*\.BIN") != null) { 223 | uploadFirmwareFile = this.name; 224 | targetFirmwareFileName = "Duet2CombinedFirmware"; 225 | } 226 | 227 | // See if a new DWC version is being installed 228 | uploadedDWC |= this.name.indexOf("reprap.htm") != -1; 229 | 230 | // DuetWiFi-specific files can be used only on a Duet WiFi 231 | if (boardType.indexOf("duetwifi") == 0) { 232 | if (this.name.toUpperCase().match("^DUETWIFISOCKETSERVER.*\.BIN") != null) { 233 | uploadDWSSFile = this.name; 234 | } else if (this.name.toUpperCase().match("^DUETWIFISERVER.*\.BIN") != null) { 235 | uploadDWSFile = this.name; 236 | } else if (this.name.toUpperCase().match("^DUETWEBCONTROL.*\.BIN") != null) { 237 | uploadDWCFile = this.name; 238 | } 239 | } 240 | } 241 | 242 | var row = ' ' + this.name + ''; 243 | row += '' + formatSize(this.size) + ''; 244 | row += '
    '; 245 | 246 | var rowElem = $(row); 247 | rowElem.appendTo("#table_upload_files > tbody"); 248 | uploadRows.push(rowElem); 249 | }); 250 | $("#modal_upload").modal("show"); 251 | 252 | // Start file upload 253 | uploadNextFile(); 254 | } 255 | 256 | function uploadNextFile() { 257 | // Prepare some upload values 258 | var file = uploadFiles[uploadedFileCount]; 259 | uploadFileName = file.name; 260 | uploadFileSize = file.size; 261 | uploadStartTime = new Date(); 262 | uploadPosition = 0; 263 | 264 | // Check if this file should be skipped 265 | if (uploadType == "generic") { 266 | var skipFile = false; 267 | 268 | if (!filenameValid(uploadFileName)) { 269 | // Skip files that contain invalid characters 270 | skipFile = true; 271 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes.")); 272 | } else if (boardType.indexOf("duetwifi") != 0) { 273 | var lcName = uploadFileName.toLowerCase(); 274 | 275 | // Skip DuetWebControl*.bin on wired Duets 276 | if (lcName.match("^duetwebcontrol.*\\.bin") != null) { 277 | skipFile = true; 278 | } 279 | 280 | // Skip DuetWiFiServer*.bin and DuetWiFiSocketServer*.bin on wired Duets 281 | if (lcName.match("^duetwifiserver.*\\.bin") != null || lcName.match("^duetwifisocketserver.*\\.bin") != null) { 282 | skipFile = true; 283 | } 284 | } 285 | 286 | if (skipFile) { 287 | fileUploadSkipped(); 288 | return; 289 | } 290 | } 291 | 292 | // Determine the right path 293 | var targetPath = ""; 294 | switch (uploadType) { 295 | case "gcode": // Upload G-Code 296 | case "start": // Upload & Start G-Code 297 | targetPath = currentGCodeDirectory + "/" + uploadFileName; 298 | clearFileCache(targetPath); 299 | break; 300 | 301 | case "macro": // Upload Macro 302 | targetPath = currentMacroDirectory + "/" + uploadFileName; 303 | break; 304 | 305 | case "filament": // Filament (only to sub-directories) 306 | targetPath = "0:/filaments/" + uploadFileName; 307 | break; 308 | 309 | case "menu": // Upload Display items 310 | targetPath = "0:/menu/" + uploadFileName; 311 | break; 312 | 313 | default: // Generic Upload (on the Settings page) 314 | var fileExts = uploadFileName.split('.'); 315 | var fileExt = fileExts.pop().toLowerCase(); 316 | if (fileExt == "gz") { 317 | // If this file was compressed, try to get the actual extension 318 | fileExt = fileExts.pop(); 319 | } 320 | 321 | switch (fileExt) { 322 | case "ico": 323 | case "html": 324 | case "htm": 325 | case "xml": 326 | targetPath = "0:/www/" + uploadFileName; 327 | break; 328 | 329 | case "css": 330 | case "map": 331 | targetPath = "0:/www/css/" + uploadFileName; 332 | break; 333 | 334 | case "eot": 335 | case "svg": 336 | case "ttf": 337 | case "woff": 338 | case "woff2": 339 | targetPath = "0:/www/fonts/" + uploadFileName; 340 | break; 341 | 342 | case "jpeg": 343 | case "jpg": 344 | case "png": 345 | targetPath = "0:/www/img/" + uploadFileName; 346 | break; 347 | 348 | case "js": 349 | targetPath = "0:/www/js/" + uploadFileName; 350 | break; 351 | 352 | default: 353 | targetPath = "0:/sys/" + uploadFileName; 354 | refreshSysFiles = true; 355 | break; 356 | } 357 | } 358 | 359 | // Update the GUI 360 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(T("Starting")); 361 | uploadRows[uploadedFileCount].find(".glyphicon").removeClass("glyphicon-asterisk").addClass("glyphicon-cloud-upload"); 362 | 363 | // Begin another POST file upload 364 | uploadRequest = $.ajax(ajaxPrefix + "rr_upload?name=" + encodeURIComponent(targetPath) + "&time=" + encodeURIComponent(timeToStr(new Date(file.lastModified))), { 365 | data: file, 366 | dataType: "json", 367 | processData: false, 368 | contentType: false, 369 | timeout: 0, 370 | type: "POST", 371 | global: false, 372 | error: function(jqxhr, textStatus, errorThrown) { 373 | if (errorThrown == "abort") { 374 | // Ignore this error if this request was cancelled intentionally 375 | return; 376 | } 377 | 378 | // Retry upload if this file is smaller than 1MiB 379 | if (file.size < 1048576) { 380 | if (!this.hasOwnProperty("retryCount")) { 381 | this.retryCount = 1; 382 | } else { 383 | this.retryCount++; 384 | } 385 | 386 | if (this.retryCount <= settings.maxRetries) { 387 | uploadRows[uploadedFileCount].find(".progress-bar").removeClass("progress-bar-info").addClass("progress-bar-warning"); 388 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(T("Retrying")); 389 | 390 | $.ajax(this); 391 | return; 392 | } 393 | } 394 | 395 | // If that failed or if the file was bigger than 1MiB, report an error 396 | finishCurrentUpload(false); 397 | }, 398 | success: function(data) { 399 | if (isUploading) { 400 | finishCurrentUpload(data.err == 0); 401 | } 402 | }, 403 | xhr: function() { 404 | var xhr = new window.XMLHttpRequest(); 405 | xhr.upload.addEventListener("progress", function(event) { 406 | if (isUploading && event.lengthComputable) { 407 | // Calculate current upload speed (Date is based on milliseconds) 408 | uploadSpeed = event.loaded / (((new Date()) - uploadStartTime) / 1000); 409 | 410 | // Update global progress 411 | uploadedTotalBytes += (event.loaded - uploadPosition); 412 | uploadPosition = event.loaded; 413 | 414 | var uploadTitle = T("Uploading File(s), {0}% Complete", ((uploadedTotalBytes / uploadTotalBytes) * 100).toFixed(0)); 415 | if (uploadSpeed > 0) { 416 | uploadTitle += " (" + formatUploadSpeed(uploadSpeed) + ")"; 417 | } 418 | $("#modal_upload h4").text(uploadTitle); 419 | 420 | // Update progress bar 421 | var progress = ((event.loaded / event.total) * 100).toFixed(0); 422 | uploadRows[uploadedFileCount].find(".progress-bar").css("width", progress + "%"); 423 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(progress + " %"); 424 | } 425 | }, false); 426 | return xhr; 427 | } 428 | }); 429 | } 430 | 431 | function finishCurrentUpload(success) { 432 | // Keep the progress updated 433 | if (!success) { 434 | uploadHadError = true; 435 | uploadedTotalBytes += (uploadFileSize - uploadPosition); 436 | } 437 | 438 | // Update glyphicon and progress bar 439 | uploadRows[uploadedFileCount].find(".glyphicon").removeClass("glyphicon-cloud-upload").addClass(success ? "glyphicon-ok" : "glyphicon-alert"); 440 | uploadRows[uploadedFileCount].find(".progress-bar").removeClass("progress-bar-info progress-bar-warning").addClass(success ? "progress-bar-success" : "progress-bar-danger").css("width", "100%"); 441 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(success ? "100 %" : T("ERROR")); 442 | 443 | // Go on with upload logic if we're still busy 444 | if (isUploading) { 445 | uploadedFileCount++; 446 | if (uploadFiles.length > uploadedFileCount) { 447 | // Upload the next file 448 | uploadNextFile(); 449 | } else { 450 | // No more files to upload - we're done 451 | finishUpload(!uploadHadError); 452 | } 453 | } 454 | } 455 | 456 | function fileUploadSkipped() { 457 | // Keep the progress updated 458 | uploadedTotalBytes += (uploadFileSize - uploadPosition); 459 | 460 | // Update glyphicon and progress bar 461 | uploadRows[uploadedFileCount].find(".glyphicon").removeClass("glyphicon-cloud-asterisk").addClass("glyphicon-warning-sign"); 462 | uploadRows[uploadedFileCount].find(".progress-bar").removeClass("progress-bar-info").addClass("progress-bar-warning").css("width", "100%"); 463 | uploadRows[uploadedFileCount].find(".progress-bar > span").text(T("SKIPPED")); 464 | 465 | // Go on with upload logic if we're still busy 466 | uploadFilesSkipped = true; 467 | if (isUploading) { 468 | uploadedFileCount++; 469 | if (uploadFiles.length > uploadedFileCount) { 470 | // Upload the next file 471 | uploadNextFile(); 472 | } else { 473 | // No more files to upload - we're done 474 | finishUpload(true); 475 | } 476 | } 477 | } 478 | 479 | function cancelUpload() { 480 | isUploading = uploadFilesSkipped = false; 481 | finishCurrentUpload(false); 482 | uploadRequest.abort(); 483 | finishUpload(false); 484 | $("#modal_upload h4").text(T("Upload Cancelled!")); 485 | startUpdates(); 486 | } 487 | 488 | function finishUpload(success) { 489 | // Reset upload variables 490 | isUploading = false; 491 | $("#input_file_upload").val(""); 492 | 493 | // Set some values in the modal dialog 494 | $("#modal_upload h4").text(T("Upload Complete!")); 495 | $("#btn_cancel_upload, #modal_upload p").addClass("hidden"); 496 | $("#modal_upload .close, #modal_upload button[data-dismiss='modal']").removeClass("hidden"); 497 | 498 | if (success) { 499 | // If everything went well, update the GUI immediately 500 | uploadHasFinished(true); 501 | } else { 502 | // In case an upload has been aborted, give the firmware some time to recover 503 | setTimeout(function() { uploadHasFinished(false); }, 1000); 504 | } 505 | } 506 | 507 | function uploadHasFinished(success) { 508 | // Make sure the G-Codes and Macro pages are updated 509 | if (uploadType == "gcode" || uploadType == "start") { 510 | gcodeUpdateIndex = -1; 511 | if (currentPage == "files") { 512 | updateGCodeFiles(); 513 | } 514 | } else if (uploadType == "macro") { 515 | macroUpdateIndex = -1; 516 | if (currentPage == "control" || currentPage == "macros") { 517 | updateMacroFiles(); 518 | } 519 | } else if (uploadType == "filament") { 520 | if (currentPage == "filaments") { 521 | updateFilaments(); 522 | } 523 | } else if (uploadType == "menu") { 524 | updateDisplayFiles(); 525 | } else if (refreshSysFiles) { 526 | updateSysFiles(); 527 | } 528 | 529 | // Start polling again 530 | startUpdates(); 531 | 532 | // Deal with different upload types 533 | if (success) { 534 | // Check if a job is supposed to be started 535 | if (uploadType == "start") { 536 | waitingForJobStart = true; 537 | sendGCode('M32 "' + currentGCodeDirectory + "/" + uploadFileName + '"'); 538 | } 539 | 540 | // Ask for firmware/DWC update if it's safe to do 541 | else if (lastStatusResponse != undefined && (lastStatusResponse.status == 'I' || lastStatusResponse.status == 'O')) { 542 | // Test for firmware update before we test for a new config file, because a firmware update includes a reset 543 | if (uploadFirmwareFile != undefined && uploadDWSFile == undefined && uploadDWSSFile == undefined && uploadDWCFile == undefined) { 544 | $("#modal_upload").modal("hide"); 545 | showConfirmationDialog(T("Perform Firmware Update?"), T("You have just uploaded a firmware file. Would you like to update your Duet now?"), startFirmwareUpdate); 546 | } else if ((uploadDWSFile != undefined || uploadDWSSFile != undefined) && uploadFirmwareFile == undefined && uploadDWCFile == undefined) { 547 | $("#modal_upload").modal("hide"); 548 | 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); 549 | } else if (uploadDWCFile != undefined && uploadFirmwareFile == undefined && uploadDWSFile == undefined && uploadDWSSFile == undefined) { 550 | $("#modal_upload").modal("hide"); 551 | 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); 552 | } else if (uploadFirmwareFile != undefined || uploadDWSFile != undefined || uploadDWSSFile != undefined || uploadDWCFile != undefined) { 553 | $("#modal_upload").modal("hide"); 554 | showConfirmationDialog(T("Perform Firmware Update?"), T("You have just uploaded multiple firmware files. Would you like to install them now?"), startFirmwareUpdates); 555 | } else if (uploadIncludedConfig) { 556 | $("#modal_upload").modal("hide"); 557 | showConfirmationDialog(T("Reboot Duet?"), T("You have just uploaded a config file. Would you like to reboot your Duet now?"), function() { 558 | // Perform software reset 559 | sendGCode("M999"); 560 | }); 561 | } else if (uploadedDWC) { 562 | $("#modal_upload").modal("hide"); 563 | 564 | if (sessionPassword != defaultPassword) { 565 | connect(sessionPassword, false); 566 | 567 | showConfirmationDialog(T("Reload Page?"), T("You have just updated Duet Web Control. Would you like to reload the page now?"), function() { 568 | sendGCode("M118 P3 S\"{reloadDWC}\""); 569 | }); 570 | } else { 571 | sendGCode("M118 P3 S\"{reloadDWC}\""); 572 | } 573 | } 574 | } 575 | } 576 | } 577 | 578 | function startFirmwareUpdates() { 579 | var sParams = [], fwFile, dwsFile, dwcFile; 580 | if (uploadFirmwareFile != undefined) { 581 | fwFile = "0:/sys/" + uploadFirmwareFile; 582 | sParams.push("0"); 583 | } 584 | if (uploadDWSSFile != undefined) { 585 | dwsFile = "0:/sys/" + uploadDWSSFile; 586 | sParams.push("1"); 587 | } else if (uploadDWSFile != undefined) { 588 | dwsFile = "0:/sys/" + uploadDWSFile; 589 | sParams.push("1"); 590 | } 591 | if (uploadDWCFile != undefined) { 592 | dwcFile = "0:/sys/" + uploadDWCFile; 593 | sParams.push("2"); 594 | } 595 | 596 | // Stop status updates so the user doesn't see any potential error messages 597 | stopUpdates(); 598 | 599 | // Move the file(s) into place 600 | moveFile(fwFile, "0:/sys/" + targetFirmwareFileName + ".bin", function() { 601 | moveFile(dwsFile, "0:/sys/DuetWiFiServer.bin", function() { 602 | moveFile(dwcFile, "0:/sys/DuetWebControl.bin", function() { 603 | // Initiate firmware updates 604 | var sParam = sParams.join(":"); 605 | sendGCode("M997 S" + sParam); 606 | 607 | // Resume the status update loop 608 | startUpdates(); 609 | }, startUpdates); 610 | }, startUpdates); 611 | }, startUpdates); 612 | } 613 | 614 | function startFirmwareUpdate() { 615 | // Stop status updates so the user doesn't see any potential error messages 616 | stopUpdates(); 617 | 618 | // The filename is hardcoded in the firmware binary, so try to rename the uploaded file first 619 | moveFile("0:/sys/" + uploadFirmwareFile, "0:/sys/" + targetFirmwareFileName + ".bin", function() { 620 | // Rename succeeded and flashing can be performed now 621 | sendGCode("M997 S0"); 622 | startUpdates(); 623 | }, startUpdates); 624 | } 625 | 626 | function startDWSUpdate() { 627 | // Stop status updates so the user doesn't see any potential error messages 628 | stopUpdates(); 629 | 630 | // We prefer DWSS over DWS. Move it into place and start the update 631 | if (uploadDWSSFile != undefined) { 632 | moveFile("0:/sys/" + uploadDWSSFile, "0:/sys/DuetWiFiServer.bin", function() { 633 | // Rename succeeded and flashing can be performed now 634 | sendGCode("M997 S1"); 635 | startUpdates(); 636 | }, startUpdates); 637 | } else { 638 | moveFile("0:/sys/" + uploadDWSFile, "0:/sys/DuetWiFiServer.bin", function() { 639 | // Rename succeeded and flashing can be performed now 640 | sendGCode("M997 S1"); 641 | startUpdates(); 642 | }, startUpdates); 643 | } 644 | } 645 | 646 | function startDWCUpdate() { 647 | // Stop status updates so the user doesn't see any potential error messages 648 | stopUpdates(); 649 | 650 | // The filename is hardcoded in the firmware binary, so try to rename the uploaded file first 651 | moveFile("0:/sys/" + uploadDWCFile, "0:/sys/DuetWebControl.bin", function() { 652 | // Rename succeeded and flashing can be performed now 653 | sendGCode("M997 S2"); 654 | startUpdates(); 655 | }, startUpdates); 656 | } 657 | 658 | 659 | /* Upload events */ 660 | 661 | $("#btn_cancel_upload").click(function() { 662 | cancelUpload(); 663 | }); 664 | 665 | $(".btn-upload").click(function(e) { 666 | if (!$(this).is(".disabled")) { 667 | var type = $(this).data("type"), filter = ""; 668 | if (type == "gcode" || type == "start") { 669 | filter = ".g,.gcode,.gc,.gco,.nc,.ngc,.tap"; 670 | } else if (type == "filament") { 671 | filter = ".zip"; 672 | } else if (type == "generic") { 673 | filter=".zip,.bin,.csv,.g,.json,.htm,.html,.ico,.xml,.css,.map,.js,.ttf,.eot,.svg,.woff,.woff2,.jpeg,.jpg,.png,.gz"; 674 | } 675 | $("#input_file_upload").prop("accept", filter).data("type", type).click(); 676 | } 677 | e.preventDefault(); 678 | }); 679 | 680 | ["start", "gcode", "macro", "filament", "generic"].forEach(function(type) { 681 | var child = $(".btn-upload[data-type='" + type + "']"); 682 | 683 | // Drag Enter 684 | child.on("dragover", function(e) { 685 | $(this).removeClass($(this).data("style")).addClass("btn-success"); 686 | e.preventDefault(); 687 | e.stopPropagation(); 688 | }); 689 | 690 | // Drag Leave 691 | child.on("dragleave", function(e) { 692 | $(this).removeClass("btn-success").addClass($(this).data("style")); 693 | e.preventDefault(); 694 | e.stopPropagation(); 695 | }); 696 | 697 | // Drop 698 | child.on("drop", function(e) { 699 | $(this).removeClass("btn-success").addClass($(this).data("style")); 700 | e.preventDefault(); 701 | e.stopPropagation(); 702 | 703 | var files = e.originalEvent.dataTransfer.files; 704 | if (!$(this).hasClass("disabled") && files != null && files.length > 0) { 705 | // Start new file upload 706 | startUpload($(this).data("type"), files, false); 707 | } 708 | }); 709 | }); 710 | 711 | $("#input_file_upload").change(function(e) { 712 | if (this.files.length > 0) { 713 | // For POST uploads we need file blobs 714 | startUpload($(this).data("type"), this.files, false); 715 | } 716 | }); 717 | 718 | $('#modal_upload').on('hidden.bs.modal', function (e) { 719 | if (uploadFilesSkipped) { 720 | showMessage("warning", T("Warning"), T("Some files were not uploaded because they were not suitable for your board.")); 721 | } 722 | }) 723 | -------------------------------------------------------------------------------- /core/css/bootstrap.theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | --------------------------------------------------------------------------------