|
@@ -16,7 +16,62 @@
|
16
|
16
|
|
17
|
17
|
$(function(){
|
18
|
18
|
|
19
|
|
-var marlin_config = 'https://api.github.com/repos/MarlinFirmware/Marlin/contents/Marlin';
|
|
19
|
+/**
|
|
20
|
+ * Github API useful GET paths. (Start with "https://api.github.com/repos/:owner/:repo/")
|
|
21
|
+ *
|
|
22
|
+ * contributors Get a list of contributors
|
|
23
|
+ * tags Get a list of tags
|
|
24
|
+ * contents/[path]?ref=branch/tag/commit Get the contents of a file
|
|
25
|
+ */
|
|
26
|
+
|
|
27
|
+ // GitHub
|
|
28
|
+ // Warning! Limited to 60 requests per hour!
|
|
29
|
+var config = {
|
|
30
|
+ type: 'github',
|
|
31
|
+ host: 'https://api.github.com',
|
|
32
|
+ owner: 'thinkyhead',
|
|
33
|
+ repo: 'Marlin',
|
|
34
|
+ ref: 'marlin_configurator',
|
|
35
|
+ path: 'Marlin/configurator/config'
|
|
36
|
+};
|
|
37
|
+/**/
|
|
38
|
+
|
|
39
|
+/* // Remote
|
|
40
|
+var config = {
|
|
41
|
+ type: 'remote',
|
|
42
|
+ host: 'http://www.thinkyhead.com',
|
|
43
|
+ path: '_marlin/config'
|
|
44
|
+};
|
|
45
|
+/**/
|
|
46
|
+
|
|
47
|
+/* // Local
|
|
48
|
+var config = {
|
|
49
|
+ type: 'local',
|
|
50
|
+ path: 'config'
|
|
51
|
+};
|
|
52
|
+/**/
|
|
53
|
+
|
|
54
|
+function github_command(conf, command, path) {
|
|
55
|
+ var req = conf.host+'/repos/'+conf.owner+'/'+conf.repo+'/'+command;
|
|
56
|
+ if (path) req += '/' + path;
|
|
57
|
+ return req;
|
|
58
|
+}
|
|
59
|
+function config_path(item) {
|
|
60
|
+ var path = '', ref = '';
|
|
61
|
+ switch(config.type) {
|
|
62
|
+ case 'github':
|
|
63
|
+ path = github_command(config, 'contents', config.path);
|
|
64
|
+ if (config.ref !== undefined) ref = '?ref=' + config.ref;
|
|
65
|
+ break;
|
|
66
|
+ case 'remote':
|
|
67
|
+ path = config.host + '/' + config.path + '/';
|
|
68
|
+ break;
|
|
69
|
+ case 'local':
|
|
70
|
+ path = config.path + '/';
|
|
71
|
+ break;
|
|
72
|
+ }
|
|
73
|
+ return path + '/' + item + ref;
|
|
74
|
+}
|
20
|
75
|
|
21
|
76
|
// Extend builtins
|
22
|
77
|
String.prototype.lpad = function(len, chr) {
|
|
@@ -25,6 +80,7 @@ String.prototype.lpad = function(len, chr) {
|
25
|
80
|
if (need > 0) { s = new Array(need+1).join(chr) + s; }
|
26
|
81
|
return s;
|
27
|
82
|
};
|
|
83
|
+
|
28
|
84
|
String.prototype.prePad = function(len, chr) { return len ? this.lpad(len, chr) : this; };
|
29
|
85
|
String.prototype.zeroPad = function(len) { return this.prePad(len, '0'); };
|
30
|
86
|
String.prototype.toHTML = function() { return jQuery('<div>').text(this).html(); };
|
|
@@ -36,6 +92,19 @@ Number.prototype.limit = function(m1, m2) {
|
36
|
92
|
if (m2 == null) return this > m1 ? m1 : this;
|
37
|
93
|
return this < m1 ? m1 : this > m2 ? m2 : this;
|
38
|
94
|
};
|
|
95
|
+Date.prototype.fileStamp = function(filename) {
|
|
96
|
+ var fs = this.getFullYear()
|
|
97
|
+ + ((this.getMonth()+1)+'').zeroPad(2)
|
|
98
|
+ + (this.getDate()+'').zeroPad(2)
|
|
99
|
+ + (this.getHours()+'').zeroPad(2)
|
|
100
|
+ + (this.getMinutes()+'').zeroPad(2)
|
|
101
|
+ + (this.getSeconds()+'').zeroPad(2);
|
|
102
|
+
|
|
103
|
+ if (filename !== undefined)
|
|
104
|
+ return filename.replace(/^(.+)(\.\w+)$/g, '$1-['+fs+']$2');
|
|
105
|
+
|
|
106
|
+ return fs;
|
|
107
|
+}
|
39
|
108
|
|
40
|
109
|
/**
|
41
|
110
|
* selectField.addOptions takes an array or keyed object
|
|
@@ -49,6 +118,12 @@ $.fn.extend({
|
49
|
118
|
sel.append( $('<option>',{value:isArr?v:k}).text(v) );
|
50
|
119
|
});
|
51
|
120
|
});
|
|
121
|
+ },
|
|
122
|
+ noSelect: function() {
|
|
123
|
+ return this
|
|
124
|
+ .attr('unselectable', 'on')
|
|
125
|
+ .css('user-select', 'none')
|
|
126
|
+ .on('selectstart', false);
|
52
|
127
|
}
|
53
|
128
|
});
|
54
|
129
|
|
|
@@ -62,10 +137,11 @@ var configuratorApp = (function(){
|
62
|
137
|
boards_file = 'boards.h',
|
63
|
138
|
config_file = 'Configuration.h',
|
64
|
139
|
config_adv_file = 'Configuration_adv.h',
|
|
140
|
+ $msgbox = $('#message'),
|
65
|
141
|
$form = $('#config_form'),
|
66
|
142
|
$tooltip = $('#tooltip'),
|
67
|
|
- $config = $('#config_text'),
|
68
|
|
- $config_adv = $('#config_adv_text'),
|
|
143
|
+ $config = $('#config_text pre'),
|
|
144
|
+ $config_adv = $('#config_adv_text pre'),
|
69
|
145
|
define_list = [[],[]],
|
70
|
146
|
boards_list = {},
|
71
|
147
|
therms_list = {},
|
|
@@ -88,6 +164,9 @@ var configuratorApp = (function(){
|
88
|
164
|
// Make tabs for all the fieldsets
|
89
|
165
|
this.makeTabsForFieldsets();
|
90
|
166
|
|
|
167
|
+ // No selection on errors
|
|
168
|
+ $msgbox.noSelect();
|
|
169
|
+
|
91
|
170
|
// Make a droppable file uploader, if possible
|
92
|
171
|
var $uploader = $('#file-upload');
|
93
|
172
|
var fileUploader = new BinaryFileUploader({
|
|
@@ -99,41 +178,98 @@ var configuratorApp = (function(){
|
99
|
178
|
|
100
|
179
|
// Make the disclosure items work
|
101
|
180
|
$('.disclose').click(function(){
|
102
|
|
- var $dis = $(this), $pre = $dis.next('pre');
|
|
181
|
+ var $dis = $(this), $pre = $dis.nextAll('pre:first');
|
103
|
182
|
var didAnim = function() {$dis.toggleClass('closed almost');};
|
104
|
183
|
$dis.addClass('almost').hasClass('closed')
|
105
|
|
- ? $pre.slideDown(500, didAnim)
|
106
|
|
- : $pre.slideUp(500, didAnim);
|
|
184
|
+ ? $pre.slideDown(200, didAnim)
|
|
185
|
+ : $pre.slideUp(200, didAnim);
|
107
|
186
|
});
|
108
|
187
|
|
109
|
188
|
// Read boards.h, Configuration.h, Configuration_adv.h
|
110
|
189
|
var ajax_count = 0, success_count = 0;
|
111
|
190
|
var loaded_items = {};
|
112
|
191
|
var config_files = [boards_file, config_file, config_adv_file];
|
113
|
|
- var isGithub = marlin_config.match('api.github');
|
|
192
|
+ var isGithub = config.type == 'github';
|
|
193
|
+ var rateLimit = 0;
|
114
|
194
|
$.each(config_files, function(i,fname){
|
|
195
|
+ var url = config_path(fname);
|
115
|
196
|
$.ajax({
|
116
|
|
- url: marlin_config+'/'+fname,
|
|
197
|
+ url: url,
|
117
|
198
|
type: 'GET',
|
118
|
|
- dataType: isGithub ? 'jsonp' : 'script',
|
|
199
|
+ dataType: isGithub ? 'jsonp' : undefined,
|
119
|
200
|
async: true,
|
120
|
201
|
cache: false,
|
|
202
|
+ error: function(req, stat, err) {
|
|
203
|
+ self.log(req, 1);
|
|
204
|
+ if (req.status == 200) {
|
|
205
|
+ if (typeof req.responseText === 'string') {
|
|
206
|
+ var txt = req.responseText;
|
|
207
|
+ loaded_items[fname] = function(){ self.fileLoaded(fname, txt); };
|
|
208
|
+ success_count++;
|
|
209
|
+ // self.setMessage('The request for "'+fname+'" may be malformed.', 'error');
|
|
210
|
+ }
|
|
211
|
+ }
|
|
212
|
+ else {
|
|
213
|
+ self.setRequestError(req.status ? req.status : '(Access-Control-Allow-Origin?)', url);
|
|
214
|
+ }
|
|
215
|
+ },
|
121
|
216
|
success: function(txt) {
|
122
|
|
- loaded_items[fname] = function(){ self.fileLoaded(fname, isGithub ? atob(txt.data.content) : txt); };
|
123
|
|
- success_count++;
|
|
217
|
+ if (isGithub && typeof txt.meta.status !== undefined && txt.meta.status != 200) {
|
|
218
|
+ self.setRequestError(txt.meta.status, url);
|
|
219
|
+ }
|
|
220
|
+ else {
|
|
221
|
+ // self.log(txt, 1);
|
|
222
|
+ if (isGithub) {
|
|
223
|
+ rateLimit = {
|
|
224
|
+ quota: 1 * txt.meta['X-RateLimit-Remaining'],
|
|
225
|
+ timeLeft: Math.floor(txt.meta['X-RateLimit-Reset'] - Date.now()/1000),
|
|
226
|
+ };
|
|
227
|
+ }
|
|
228
|
+ loaded_items[fname] = function(){ self.fileLoaded(fname, isGithub ? atob(txt.data.content) : txt); };
|
|
229
|
+ success_count++;
|
|
230
|
+ }
|
124
|
231
|
},
|
125
|
232
|
complete: function() {
|
126
|
233
|
ajax_count++;
|
127
|
234
|
if (ajax_count >= 3) {
|
128
|
235
|
$.each(config_files, function(){ if (loaded_items[this]) loaded_items[this](); });
|
129
|
236
|
if (success_count < ajax_count)
|
130
|
|
- self.setMessage('Unable to load configurations. Use the upload field instead.', 'error');
|
|
237
|
+ self.setMessage('Unable to load configurations. Try the upload field.', 'error');
|
|
238
|
+ var r;
|
|
239
|
+ if (r = rateLimit) {
|
|
240
|
+ if (r.quota < 20) {
|
|
241
|
+ self.setMessage(
|
|
242
|
+ 'Approaching request limit (' +
|
|
243
|
+ r.quota + ' remaining.' +
|
|
244
|
+ ' Reset in ' + Math.floor(r.timeLeft/60) + ':' + (r.timeLeft%60+'').zeroPad(2) + ')'
|
|
245
|
+ );
|
|
246
|
+ }
|
|
247
|
+ }
|
131
|
248
|
}
|
132
|
249
|
}
|
133
|
250
|
});
|
134
|
251
|
});
|
135
|
252
|
},
|
136
|
253
|
|
|
254
|
+ createDownloadLink: function(adv) {
|
|
255
|
+ var $c = adv ? $config_adv : $config, txt = $c.text();
|
|
256
|
+ var filename = (adv ? config_adv_file : config_file);
|
|
257
|
+ $c.prevAll('.download:first')
|
|
258
|
+ .mouseover(function() {
|
|
259
|
+ var d = new Date(), fn = d.fileStamp(filename);
|
|
260
|
+ $(this).attr({ download:fn, href:'download:'+fn, title:'download:'+fn });
|
|
261
|
+ })
|
|
262
|
+ .click(function(){
|
|
263
|
+ var $button = $(this);
|
|
264
|
+ $(this).attr({ href:'data:text/plain;charset=utf-8,' + encodeURIComponent($c.text()) });
|
|
265
|
+ setTimeout(function(){
|
|
266
|
+ $button.attr({ href:$button.attr('title') });
|
|
267
|
+ }, 100);
|
|
268
|
+ return true;
|
|
269
|
+ })
|
|
270
|
+ .css({visibility:'visible'});
|
|
271
|
+ },
|
|
272
|
+
|
137
|
273
|
/**
|
138
|
274
|
* Init the boards array from a boards.h file
|
139
|
275
|
*/
|
|
@@ -239,6 +375,7 @@ var configuratorApp = (function(){
|
239
|
375
|
this.log(define_list[0], 2);
|
240
|
376
|
this.createFieldsForDefines(0);
|
241
|
377
|
this.refreshConfigForm();
|
|
378
|
+ this.createDownloadLink(false);
|
242
|
379
|
has_config = true;
|
243
|
380
|
}
|
244
|
381
|
else {
|
|
@@ -253,6 +390,7 @@ var configuratorApp = (function(){
|
253
|
390
|
define_list[1] = this.getDefinesFromText(txt);
|
254
|
391
|
this.log(define_list[1], 2);
|
255
|
392
|
this.refreshConfigForm();
|
|
393
|
+ this.createDownloadLink(true);
|
256
|
394
|
has_config_adv = true;
|
257
|
395
|
}
|
258
|
396
|
else {
|
|
@@ -289,7 +427,7 @@ var configuratorApp = (function(){
|
289
|
427
|
});
|
290
|
428
|
|
291
|
429
|
// Get all 'switchable' class items and add a checkbox
|
292
|
|
- $('#config_form .switchable').each(function(){
|
|
430
|
+ $form.find('.switchable').each(function(){
|
293
|
431
|
$(this).after(
|
294
|
432
|
$('<input>',{type:'checkbox',value:'1',class:'enabler'}).prop('checked',true)
|
295
|
433
|
.attr('id',this.id + '-switch')
|
|
@@ -651,6 +789,7 @@ var configuratorApp = (function(){
|
651
|
789
|
}
|
652
|
790
|
}
|
653
|
791
|
|
|
792
|
+ // Success?
|
654
|
793
|
if (info.type) {
|
655
|
794
|
// Get the end-of-line comment, if there is one
|
656
|
795
|
var tooltip = '';
|
|
@@ -668,11 +807,11 @@ var configuratorApp = (function(){
|
668
|
807
|
tooltip = '';
|
669
|
808
|
break;
|
670
|
809
|
}
|
671
|
|
- tooltip += ' ' + s[1] + "\n";
|
|
810
|
+ tooltip += ' ' + s[1] + '\n';
|
672
|
811
|
}
|
673
|
812
|
}
|
674
|
813
|
|
675
|
|
- findDef = new RegExp('^[ \\t]*'+name+'[ \\t]*', 'm');
|
|
814
|
+ findDef = new RegExp('^[ \\t]*'+name); // To strip the name from the start
|
676
|
815
|
$.extend(info, {
|
677
|
816
|
tooltip: '<strong>'+name+'</strong> '+tooltip.replace(findDef,'').trim().toHTML(),
|
678
|
817
|
lineNum: this.getLineNumberOfText(info.line, txt)
|
|
@@ -700,24 +839,22 @@ var configuratorApp = (function(){
|
700
|
839
|
setMessage: function(msg,type) {
|
701
|
840
|
if (msg) {
|
702
|
841
|
if (type === undefined) type = 'message';
|
703
|
|
- var $err = $('<p class="'+type+'">'+msg+'</p>'), err = $err[0];
|
704
|
|
- $('#message').prepend($err);
|
|
842
|
+ var $err = $('<p class="'+type+'">'+msg+'<span>x</span></p>').appendTo($msgbox), err = $err[0];
|
705
|
843
|
var baseColor = $err.css('color').replace(/rgba?\(([^),]+,[^),]+,[^),]+).*/, 'rgba($1,');
|
706
|
|
- var d = new Date();
|
707
|
844
|
err.pulse_offset = (pulse_offset += 200);
|
708
|
|
- err.startTime = d.getTime() + pulse_offset;
|
|
845
|
+ err.startTime = Date.now() + pulse_offset;
|
709
|
846
|
err.pulser = setInterval(function(){
|
710
|
|
- d = new Date();
|
711
|
|
- var pulse_time = d.getTime() + err.pulse_offset;
|
712
|
|
- $err.css({color:baseColor+(0.5+Math.sin(pulse_time/200)*0.4)+')'});
|
713
|
|
- if (pulse_time - err.startTime > 5000) {
|
|
847
|
+ var pulse_time = Date.now() + err.pulse_offset;
|
|
848
|
+ var opac = 0.5+Math.sin(pulse_time/200)*0.4;
|
|
849
|
+ $err.css({color:baseColor+(opac)+')'});
|
|
850
|
+ if (pulse_time - err.startTime > 2500 && opac > 0.899) {
|
714
|
851
|
clearInterval(err.pulser);
|
715
|
|
- $err.remove();
|
716
|
852
|
}
|
717
|
853
|
}, 50);
|
|
854
|
+ $err.click(function(e) { $(this).remove(); return false; }).css({cursor:'pointer'});
|
718
|
855
|
}
|
719
|
856
|
else {
|
720
|
|
- $('#message p.error, #message p.warning').each(function() {
|
|
857
|
+ $msgbox.find('p.error, p.warning').each(function() {
|
721
|
858
|
if (this.pulser !== undefined && this.pulser)
|
722
|
859
|
clearInterval(this.pulser);
|
723
|
860
|
$(this).remove();
|
|
@@ -725,6 +862,10 @@ var configuratorApp = (function(){
|
725
|
862
|
}
|
726
|
863
|
},
|
727
|
864
|
|
|
865
|
+ setRequestError: function(stat, path) {
|
|
866
|
+ self.setMessage('Error '+stat+' – ' + path.replace(/^(https:\/\/[^\/]+\/)?.+(\/[^\/]+)$/, '$1...$2'), 'error');
|
|
867
|
+ },
|
|
868
|
+
|
728
|
869
|
log: function(o,l) {
|
729
|
870
|
if (l === undefined) l = 0;
|
730
|
871
|
if (this.logging>=l*1) console.log(o);
|