ソースを参照

Create, edit, and delete themes with a WSYIWYG editor.

localadmin 7 年 前
コミット
d1dbe51e17
共有11 個のファイルを変更した1194 個の追加7 個の削除を含む
  1. 1 0
      .gitignore
  2. 24 2
      client/index.js
  3. 8 4
      client/preview.js
  4. 501 0
      client/themeEditor.js
  5. 160 0
      client/themeOptions.js
  6. 249 0
      editor/css/editor.css
  7. 2 0
      editor/index.html
  8. 97 0
      editor/themes.html
  9. 52 0
      server/imageAPI.js
  10. 11 1
      server/index.js
  11. 89 0
      server/themesAPI.js

+ 1 - 0
.gitignore ファイルの表示

@@ -7,3 +7,4 @@ junk/
7 7
 media/
8 8
 *.log
9 9
 .jobs
10
+settings/backgrounds/

+ 24 - 2
client/index.js ファイルの表示

@@ -2,7 +2,8 @@ var d3 = require("d3"),
2 2
     $ = require("jquery"),
3 3
     preview = require("./preview.js"),
4 4
     video = require("./video.js"),
5
-    audio = require("./audio.js");
5
+    audio = require("./audio.js"),
6
+    themeEditor = require("./themeEditor.js");
6 7
 
7 8
 d3.json("/settings/themes.json", function(err, themes){
8 9
 
@@ -27,6 +28,9 @@ d3.json("/settings/themes.json", function(err, themes){
27 28
     return;
28 29
   }
29 30
 
31
+  if(themeEditor.isEditor())
32
+    themeEditor.initializeThemes(themes);
33
+
30 34
   for (var key in themes) {
31 35
     themes[key] = $.extend({}, themes.default, themes[key]);
32 36
   }
@@ -138,7 +142,11 @@ function initialize(err, themesWithImages) {
138 142
 
139 143
   // Populate dropdown menu
140 144
   d3.select("#input-theme")
141
-    .on("change", updateTheme)
145
+    .on("change.a", updateTheme)
146
+    .on("change.b", function(){
147
+      if(themeEditor.isActive())
148
+        themeEditor.populateThemeFields();
149
+    })
142 150
     .selectAll("option")
143 151
     .data(themesWithImages)
144 152
     .enter()
@@ -182,6 +190,20 @@ function initialize(err, themesWithImages) {
182 190
 
183 191
   d3.select("#submit").on("click", submitted);
184 192
 
193
+  d3.select("#theme-edit").on("click", function(){
194
+    var activeTheme = d3.select('#input-theme').property('value');
195
+    var url = window.location.origin + window.location.pathname + 'themes.html?t=' + activeTheme;
196
+    window.location = url;
197
+  });
198
+
199
+  d3.select("#new-theme").on("click", function(){
200
+    var url = window.location.origin + window.location.pathname + 'themes.html?t=default'
201
+    window.location = url;
202
+  });
203
+
204
+  if(themeEditor.isEditor())
205
+    themeEditor.initialize();
206
+
185 207
 }
186 208
 
187 209
 function updateAudioFile() {

+ 8 - 4
client/preview.js ファイルの表示

@@ -47,8 +47,12 @@ minimap.onBrush(function(extent){
47 47
 // Resize video and preview canvas to maintain aspect ratio
48 48
 function resize(width, height) {
49 49
 
50
-  var widthFactor = 640 / width,
51
-      heightFactor = 360 / height,
50
+  var containerWidth = document.querySelector('#preview').clientWidth,
51
+      aspectRatio = height/width;
52
+  containerWidth = containerWidth > 0 ? containerWidth : 640;
53
+
54
+  var widthFactor = containerWidth / width,
55
+      heightFactor = containerWidth * aspectRatio / height,
52 56
       factor = Math.min(widthFactor, heightFactor);
53 57
 
54 58
   d3.select("canvas")
@@ -69,7 +73,6 @@ function resize(width, height) {
69 73
 }
70 74
 
71 75
 function redraw() {
72
-
73 76
   resize(theme.width, theme.height);
74 77
 
75 78
   video.kill();
@@ -111,5 +114,6 @@ module.exports = {
111 114
   theme: _theme,
112 115
   file: _file,
113 116
   selection: _selection,
114
-  loadAudio: loadAudio
117
+  loadAudio: loadAudio,
118
+  redraw: redraw
115 119
 };

+ 501 - 0
client/themeEditor.js ファイルの表示

@@ -0,0 +1,501 @@
1
+var d3 = require('d3'),
2
+  $ = require('jquery'),
3
+  preview = require('./preview.js'),
4
+  options = require('./themeOptions.js');
5
+
6
+var editorIsActive = false;
7
+var themesJson = {};
8
+
9
+function updateTheme(options) {
10
+  if(options !== undefined){
11
+    if(options.backgroundImage !== '')
12
+      getImage(options);
13
+    preview.theme(options);
14
+  }
15
+  else{
16
+    preview.theme(d3.select(this.options[this.selectedIndex]).datum());
17
+  }
18
+}
19
+
20
+function getImage(theme) {
21
+  if (theme.backgroundImage) {
22
+    theme.backgroundImageFile = new Image();
23
+
24
+    theme.backgroundImageFile.onload = function(){
25
+      updateTheme(anonymousTheme());
26
+    };
27
+
28
+    theme.backgroundImageFile.src = '/settings/backgrounds/' + theme.backgroundImage;
29
+  }
30
+}
31
+
32
+function error(msg) {
33
+  if (msg.responseText) {
34
+    msg = msg.responseText;
35
+  }
36
+  if (typeof msg !== 'string') {
37
+    msg = JSON.stringify(msg);
38
+  }
39
+  if (!msg) {
40
+    msg = 'Unknown error';
41
+  }
42
+  d3.select('#loading-message').text('Loading...');
43
+  setClass('error', msg);
44
+}
45
+
46
+function setClass(cl, msg) {
47
+  d3.select('body').attr('class', cl || null);
48
+  d3.select('#error').text(msg || '');
49
+}
50
+
51
+function anonymousTheme(){
52
+  var output = {};
53
+  d3.selectAll('.options-attribute-input').each(function(d){
54
+    if(d.type == 'number'){
55
+      if(Number.isNaN(parseFloat(this.value)))
56
+        output[d.name] = null;
57
+      else
58
+        output[d.name] = parseFloat(this.value);
59
+    }
60
+    else{
61
+      output[d.name] = this.value;
62
+    }
63
+  });
64
+
65
+  return output;
66
+}
67
+
68
+function postTheme(type, data, cb){
69
+  var postData = {
70
+    type: type,
71
+    data: data
72
+  };
73
+
74
+  $.ajax({
75
+    url: '/api/themes',
76
+    type: 'POST',
77
+    data: JSON.stringify(postData),
78
+    contentType: 'application/json',
79
+    cache: false,
80
+    success: cb,
81
+    error: error
82
+  });
83
+}
84
+
85
+function saveChanges(){
86
+  var activeTheme = preview.theme();
87
+  var activeThemeName = d3.select('#input-theme').property('value');
88
+
89
+  function makeTheme(){
90
+    var file = {};
91
+
92
+    file.name = activeTheme.name;
93
+    file.currentName = activeThemeName;
94
+
95
+    d3.selectAll('.options-attribute-input').each(function(d){
96
+      if(!this.disabled){
97
+        if(this.getAttribute('type') == 'number')
98
+          file[d.name] = parseFloat(this.value);
99
+        else
100
+          file[d.name] = this.value;
101
+      }
102
+    });
103
+
104
+    return file;
105
+  }
106
+
107
+  function reloadPage(){
108
+    var url = window.location.origin + window.location.pathname + '?t=' + activeTheme.name;
109
+    window.location = url;
110
+  }
111
+
112
+  var themeNames = [];
113
+  d3.selectAll('#input-theme option').each(function(d){
114
+    themeNames.push(d.name);
115
+  });
116
+  themeNames.splice(themeNames.length-1, 1);
117
+
118
+  var postData = makeTheme();
119
+
120
+  if(activeTheme.name == 'default' || activeTheme.name == '0' || activeTheme.name == ''){
121
+    window.alert('Please give your theme a name.');
122
+  }
123
+  else if(themeNames.indexOf(activeTheme.name) != -1 && activeThemeName == 'default'){
124
+    window.alert('That theme name already exists. Please choose a different name or select it to edit it.');
125
+  }
126
+  else if(themeNames.indexOf(activeThemeName) != -1){
127
+    var msg = 'Are you sure you want to override the options for "';
128
+    msg += activeThemeName + '"? This action is permanent and cannot be undone.';
129
+    if(window.confirm(msg)){
130
+      postTheme('UPDATE', postData, reloadPage);
131
+    }
132
+  }
133
+  else{
134
+    postTheme('ADD', postData, reloadPage);
135
+  }
136
+}
137
+
138
+function deleteTheme(){
139
+  var activeThemeName = d3.select('#input-theme').property('value');
140
+
141
+  var msg = 'Are you sure you want to delete the theme "';
142
+  msg += activeThemeName + '"? This action is permanent and cannot be undone.';
143
+
144
+  if(window.confirm(msg)){
145
+    var postData = {name: activeThemeName};
146
+    postTheme('DELETE', postData, function(){
147
+      var url = window.location.origin + window.location.pathname;
148
+      window.location = url;
149
+    });
150
+  }
151
+}
152
+
153
+function refreshTheme(){
154
+  var theme = $.extend({name: preview.theme().name}, themesJson.default, themesJson[preview.theme().name]);
155
+  updateTheme(theme);
156
+  populateThemeFields();
157
+}
158
+
159
+function loadBkgndImages(cb){
160
+  $.ajax({
161
+    url: '/api/images',
162
+    type: 'GET',
163
+    cache: false,
164
+    success: function(data){
165
+      $('#attribute-backgroundImage').html('');
166
+
167
+      var bkgndImgSelect = d3.select('#attribute-backgroundImage');
168
+      for(var img of data){
169
+        bkgndImgSelect.append('option')
170
+          .attr('value', img)
171
+          .text(img);
172
+      }
173
+
174
+      if(cb !== undefined){
175
+        cb();
176
+      }
177
+
178
+    },
179
+    error: error
180
+  });
181
+}
182
+
183
+function uploadImage(){
184
+  // this = the file input calling the function
185
+  var img = this.files[0];
186
+
187
+  var confirmed = confirm('Are you sure you want to upload ' + img.name + '?');
188
+  if(confirmed){
189
+    var formData = new FormData();
190
+    formData.append('img', img);
191
+
192
+    setClass('loading');
193
+
194
+    $.ajax({
195
+      url: '/api/images',
196
+      type: 'POST',
197
+      data: formData,
198
+      contentType: false,
199
+      cache: false,
200
+      processData: false,
201
+      success: function(){
202
+        var setImg = function(){
203
+          var input = d3.select('#container-backgroundImage').select('.options-attribute-input');
204
+          input.property('value', img.name).attr('data-value', img.name);
205
+
206
+          updateTheme(anonymousTheme());
207
+
208
+          setClass(null);
209
+        };
210
+
211
+        loadBkgndImages(setImg);
212
+      },
213
+      error: error
214
+    });
215
+  }
216
+}
217
+
218
+function camelToTitle(string){
219
+  var conversion = string.replace( /([A-Z])/g, ' $1' );
220
+  var title = conversion.charAt(0).toUpperCase() + conversion.slice(1);
221
+  return title;
222
+}
223
+
224
+function queryParser(query){
225
+    query = query.substring(1);
226
+    let query_string = {};
227
+    const vars = query.split('&');
228
+    for (var i=0;i<vars.length;i++) {
229
+        var pair = vars[i].split('=');
230
+        if (typeof query_string[pair[0]] === 'undefined') {
231
+            query_string[pair[0]] = decodeURIComponent(pair[1]);
232
+        } else if (typeof query_string[pair[0]] === 'string') {
233
+            const arr = [ query_string[pair[0]],decodeURIComponent(pair[1]) ];
234
+            query_string[pair[0]] = arr;
235
+        } else {
236
+            query_string[pair[0]].push(decodeURIComponent(pair[1]));
237
+        }
238
+    }
239
+
240
+    if (Object.keys(query_string).includes(''))
241
+        return {};
242
+    else
243
+        return query_string;
244
+};
245
+
246
+
247
+function isEditor(){
248
+  return document.querySelector('#themeEditor') !== null;
249
+}
250
+
251
+function isActive(){
252
+  return editorIsActive;
253
+}
254
+
255
+function initializeThemes(themes){
256
+  for (var key in themes) {
257
+    themesJson[key] = $.extend({}, themes[key]);
258
+    if('foregroundColor' in themesJson[key]){
259
+      themesJson[key].captionColor = themesJson[key].foregroundColor;
260
+      themesJson[key].waveColor = themesJson[key].foregroundColor;
261
+    }
262
+  }
263
+}
264
+
265
+function initializePreview(){
266
+  var container = $('#preview');
267
+  var top = $(container).offset().top;
268
+  $(window).scroll(function() {
269
+    var y = $(this).scrollTop();
270
+    if (y >= top){
271
+      $(container).addClass('sticky');
272
+    }
273
+    else{
274
+      $(container).removeClass('sticky');
275
+    }
276
+  });
277
+
278
+  preview.redraw();
279
+}
280
+
281
+function toggleSection(d){
282
+  var name;
283
+
284
+  if(typeof(d) == 'object')
285
+    name = d.name;
286
+  else
287
+    name = d;
288
+
289
+  $('#section-options-'+ name).slideToggle(500);
290
+  $('#section-toggle-' + name).toggleClass('toggled');
291
+}
292
+
293
+function initialize(){
294
+  var container = d3.select('#options');
295
+
296
+  // Add Option Sections
297
+  var sections = container.selectAll('.section').data(options)
298
+    .enter()
299
+    .append('div')
300
+    .attr('class', 'section');
301
+
302
+  var headers = sections.append('div')
303
+    .attr('class', 'section-header')
304
+    .on('click', toggleSection);
305
+
306
+  // Add Section Toggles
307
+  headers.append('svg')
308
+    .attr('id', function(d){return 'section-toggle-' + d.name;})
309
+    .attr('class', 'section-toggle')
310
+    .attr('viewBox', '0 0 24 24')
311
+    .append('path')
312
+    .attr('d', function(){
313
+      var path = 'M12,13.7c-0.5,0-0.9-0.2-1.2-0.5L0.5,2.9c-0.7-0.7-0.7-1.8,0-2.4C0.8,0.2,1.3,0,1.7,0h20.6C23.3,0,' +
314
+          '24,0.8,24,1.7c0,0.4-0.2,0.9-0.5,1.2L13.2,13.2C12.9,13.6,12.5,13.7,12,13.7z';
315
+      return path;
316
+    });
317
+
318
+  // Add Section Titles
319
+  headers.append('h4')
320
+    .attr('class', 'section-title')
321
+    .text(function(d){return d.name;});
322
+
323
+  var attributesContainer = sections.append('div')
324
+    .attr('class', 'section-options')
325
+    .attr('id', function(d){return 'section-options-' + d.name;})
326
+    .style('display', 'none');
327
+
328
+  // Add Option Container
329
+  var attributes = attributesContainer.selectAll('.options-attribute')
330
+    .data(function(d){return d.options;})
331
+    .enter()
332
+    .append('div')
333
+    .attr('class','options-attribute')
334
+    .attr('id', function(d){return 'container-' + d.name;})
335
+    .attr('data-type', function(d){return d.type;});
336
+
337
+  // Add Enable Checkboxes
338
+  attributes.append('input')
339
+    .attr('id', function(d){return 'enable-' + d.name;})
340
+    .attr('class', 'options-checkbox options-attribute-child')
341
+    .attr('name', function(d){return 'enable-' + d.name;})
342
+    .attr('value', 'true')
343
+    .attr('type', 'checkbox')
344
+    .property('checked', true)
345
+    .on('click', function(d){
346
+      var input = d3.select('#attribute-' + d.name);
347
+      if(this.checked){
348
+        input.property('disabled', false);
349
+        input.property('value', input.attr('data-value'));
350
+        input.attr('value', input.attr('data-value'));
351
+        updateTheme(anonymousTheme());
352
+      }
353
+      else{
354
+        input.property('value', themesJson.default[d.name]);
355
+        input.attr('value', themesJson.default[d.name]);
356
+        input.property('disabled', true);
357
+        updateTheme(anonymousTheme());
358
+      }
359
+    });
360
+
361
+  // Add Option Labels
362
+  attributes.append('label')
363
+    .attr('for', function(d){return 'attribute-' + d.name;})
364
+    .attr('class', 'options-attribute-child')
365
+    .text(function(d){return camelToTitle(d.name);});
366
+
367
+  // Add Help Text Icons
368
+  attributes.append('i')
369
+    .attr('id', function(d){return 'help-' + d.name;})
370
+    .attr('class', 'attribute-help options-attribute-child fa fa-question')
371
+    .attr('title', function(d){return d.help;});
372
+
373
+  // Add Inputs For Text Fields
374
+  sections.selectAll('.options-attribute:not([data-type="select"])')
375
+    .append('input')
376
+    .attr('id', function(d){return 'attribute-' + d.name;})
377
+    .attr('class', 'options-attribute-input options-attribute-child input-text')
378
+    .attr('name', function(d){return d.name;})
379
+    .attr('type', function(d){return d.type;})
380
+    .on('input', function(){
381
+      this.setAttribute('data-value', this.value);
382
+      updateTheme(anonymousTheme());
383
+    });
384
+
385
+  // Add Inputs For Select Fields
386
+  sections.selectAll('.options-attribute[data-type="select"]')
387
+    .append('select')
388
+    .attr('id', function(d){return 'attribute-' + d.name;})
389
+    .attr('class', 'options-attribute-input options-attribute-child input-select')
390
+    .attr('name', function(d){return d.name;})
391
+    .attr('type', function(d){return d.type;})
392
+    .on('input', function(){updateTheme(anonymousTheme());})
393
+    .selectAll('options')
394
+    .data(function(d){return d.options;})
395
+    .enter()
396
+    .append('option')
397
+    .attr('value', function(d){return d;})
398
+    .text(function(d){return camelToTitle(d);});
399
+
400
+  // Add Section Notes
401
+  attributesContainer.append('p')
402
+    .attr('class', 'options-note note')
403
+    .text(function(d){return d.note;});
404
+
405
+  // Add "New..." option to theme select
406
+  d3.select('#input-theme')
407
+    .append('option')
408
+    .data([themesJson.default])
409
+    .attr('value', 'default')
410
+    .text('New...');
411
+
412
+  // Add clickHandler for Save Button
413
+  d3.select('#saveChanges')
414
+    .on('click', saveChanges);
415
+
416
+  // Add clickHandler for Delete Button
417
+  d3.select('#deleteTheme')
418
+    .on('click', deleteTheme);
419
+
420
+  // Add clickHandler for Refresh Button
421
+  d3.select('#refreshTheme')
422
+    .on('click', refreshTheme);
423
+
424
+  // Background Images Populate and Add Uploader
425
+  loadBkgndImages(populateThemeFields);
426
+  var bkgndImgContainer = d3.select('#container-backgroundImage');
427
+  bkgndImgContainer.append('label')
428
+    .attr('for', 'imgUploader')
429
+    .attr('id', 'imgUploader-label')
430
+    .attr('class', 'button')
431
+    .append('i')
432
+    .attr('class', 'fa fa-upload');
433
+  bkgndImgContainer.append('input')
434
+    .attr('type', 'file')
435
+    .attr('id', 'imgUploader')
436
+    .attr('name', 'imgUploader')
437
+    .on('change', uploadImage);
438
+
439
+  toggleSection('Metadata');
440
+
441
+  // Active url theme
442
+  var selectedTheme = queryParser(window.location.search);
443
+  if('t' in selectedTheme && selectedTheme.t in themesJson){
444
+    d3.select('#input-theme')
445
+      .property('value', selectedTheme.t)
446
+      .dispatch('change');
447
+  }
448
+
449
+  populateThemeFields();
450
+
451
+  initializePreview();
452
+  window.addEventListener('resize', function(){
453
+    preview.redraw();
454
+  });
455
+
456
+  d3.select('#toggle-more-instructions').on('click', function(){
457
+    $('#more-instructions').slideToggle(500);
458
+  })
459
+
460
+
461
+  // Toggle the editor container to loaded
462
+  editorIsActive = true;
463
+}
464
+
465
+function populateThemeFields(){
466
+  var activeTheme = preview.theme();
467
+  var themeJson = activeTheme.name !== undefined && activeTheme.name in themesJson ?
468
+    themesJson[activeTheme.name] :
469
+    themesJson.default;
470
+
471
+  d3.selectAll('.options-attribute').each(function(d){
472
+    var input = d3.select(this).select('.options-attribute-input');
473
+
474
+    var activeValue = d.name in activeTheme ? activeTheme[d.name] : '';
475
+    input.property('value', activeValue).attr('data-value', activeValue);
476
+
477
+    if(d.name == 'name'){
478
+      d3.select('#enable-'+d.name).property('checked', true);
479
+      input.property('disabled', false);
480
+    }
481
+    else if(activeTheme.name == undefined || !(d.name in themeJson)){
482
+      d3.select('#enable-'+d.name).property('checked', false);
483
+      input.property('disabled', true);
484
+    }
485
+    else{
486
+      d3.select('#enable-'+d.name).property('checked', true);
487
+      input.property('disabled', false);
488
+    }
489
+
490
+  });
491
+}
492
+
493
+window.themesJson = themesJson;
494
+
495
+module.exports = {
496
+  isEditor,
497
+  isActive,
498
+  initialize,
499
+  initializeThemes,
500
+  populateThemeFields
501
+};

+ 160 - 0
client/themeOptions.js ファイルの表示

@@ -0,0 +1,160 @@
1
+module.exports = [
2
+  {
3
+    'name': 'Metadata',
4
+    'options': [
5
+      {
6
+        'name': 'name',
7
+        'type': 'text',
8
+        'help': 'The name of the theme.'
9
+      }
10
+    ]
11
+  },
12
+  {
13
+    'name': 'Movie',
14
+    'options': [
15
+      {
16
+        'name': 'width',
17
+        'type': 'number',
18
+        'help': 'Desired video width in pixels.'
19
+      },
20
+      {
21
+        'name': 'height',
22
+        'type': 'number',
23
+        'help': 'Desired video height in pixels.'
24
+      },
25
+      {
26
+        'name': 'framesPerSecond',
27
+        'type': 'number',
28
+        'help': 'Desired video framerate.'
29
+      },
30
+      {
31
+        'name': 'samplesPerFrame',
32
+        'type': 'number',
33
+        'help': 'How many data points to use for the waveform. More points = a more detailed wave. (e.g. 128)'
34
+      },
35
+      {
36
+        'name': 'maxDuration',
37
+        'type': 'number',
38
+        'help': 'Maximum duration of an audiogram, in seconds (e.g. set this to 30 to enforce a 30-second time limit).'
39
+      }
40
+    ]
41
+  },
42
+  {
43
+    'name': 'Background',
44
+    'note': 'You can set both a Background Color and a Background Image, in which case the image will be drawn on top of the color.',
45
+    'options': [
46
+      {
47
+        'name': 'backgroundImage',
48
+        'type': 'select',
49
+        'help': 'What image to put in the background of every frame, it should be a file in settings/backgrounds/',
50
+        'options': []
51
+      },
52
+      {
53
+        'name': 'backgroundColor',
54
+        'type': 'color',
55
+        'help': 'A CSS color to fill the background of every frame (e.g. pink or #ff00ff).'
56
+      }
57
+    ]
58
+  },
59
+  {
60
+    'name': 'Caption',
61
+    'note': 'If both Caption Top and Caption Bottom are set, the caption will be roughly vertically centered between them, give or take a few pixels depending on the font.',
62
+    'options': [
63
+      {
64
+        'name': 'captionColor',
65
+        'type': 'color',
66
+        'help': 'A CSS color, what color the text should be (e.g. red or #ffcc00).'
67
+      },
68
+      {
69
+        'name': 'captionAlign',
70
+        'type': 'select',
71
+        'help': 'Text alignment of the caption.',
72
+        'options': [
73
+          'left',
74
+          'right',
75
+          'center'
76
+        ]
77
+      },
78
+      {
79
+        'name': 'captionFont',
80
+        'type': 'text',
81
+        'help': 'A full CSS font definition to use for the caption. ([weight] size \'family\').'
82
+      },
83
+      {
84
+        'name': 'captionLineHeight',
85
+        'type': 'number',
86
+        'help': 'How tall each caption line is in pixels. You\'ll want to adjust this for whatever font and font size you\'re using.'
87
+      },
88
+      {
89
+        'name': 'captionLineSpacing',
90
+        'type': 'number',
91
+        'help': 'How many extra pixels to put between caption lines. You\'ll want to adjust this for whatever font and font size you\'re using.'
92
+      },
93
+      {
94
+        'name': 'captionLeft',
95
+        'type': 'number',
96
+        'help': 'How many pixels from the left edge to place the caption'
97
+      },
98
+      {
99
+        'name': 'captionRight',
100
+        'type': 'number',
101
+        'help': 'How many pixels from the right edge to place the caption'
102
+      },
103
+      {
104
+        'name': 'captionBottom',
105
+        'type': 'number',
106
+        'help': 'How many pixels from the bottom edge to place the caption.'
107
+      },
108
+      {
109
+        'name': 'captionTop',
110
+        'type': 'number',
111
+        'help': 'How many pixels from the top edge to place the caption.'
112
+      }
113
+    ]
114
+  },
115
+  {
116
+    'name': 'Wave',
117
+    'options': [
118
+      {
119
+        'name': 'pattern',
120
+        'type': 'select',
121
+        'help': 'What waveform shape to draw.',
122
+        'options': [
123
+          'wave',
124
+          'bars',
125
+          'line',
126
+          'curve',
127
+          'roundBars',
128
+          'pixel',
129
+          'bricks',
130
+          'equalizer'
131
+        ]
132
+      },
133
+      {
134
+        'name': 'waveTop',
135
+        'type': 'number',
136
+        'help': 'How many pixels from the top edge to start the waveform.'
137
+      },
138
+      {
139
+        'name': 'waveBottom',
140
+        'type': 'number',
141
+        'help': 'How many pixels from the top edge to end the waveform.'
142
+      },
143
+      {
144
+        'name': 'waveLeft',
145
+        'type': 'number',
146
+        'help': 'How many pixels from the left edge to start the waveform.'
147
+      },
148
+      {
149
+        'name': 'waveRight',
150
+        'type': 'number',
151
+        'help': 'How many pixels from the right edge to start the waveform.'
152
+      },
153
+      {
154
+        'name': 'waveColor',
155
+        'type': 'color',
156
+        'help': 'A CSS color, what color the wave should be.'
157
+      }
158
+    ]
159
+  }
160
+];

+ 249 - 0
editor/css/editor.css ファイルの表示

@@ -1,6 +1,7 @@
1 1
 div.container {
2 2
   width: 640px;
3 3
   margin-bottom: 2rem;
4
+  padding: 0 20px;
4 5
 }
5 6
 
6 7
 h1 {
@@ -13,6 +14,14 @@ h1 {
13 14
   color: #c00;
14 15
 }
15 16
 
17
+code {
18
+  padding: 0.2em 0.4em;
19
+  margin: 0;
20
+  font-size: 85%;
21
+  background-color: rgba(27,31,35,0.05);
22
+  border-radius: 3px;
23
+}
24
+
16 25
 /* Buttons/controls */
17 26
 button,
18 27
 .button {
@@ -301,3 +310,243 @@ g.time line {
301 310
     -webkit-transform: scaleY(1.0);
302 311
   }
303 312
 }
313
+
314
+#theme-edit i.fa,
315
+#new-theme i.fa{
316
+  margin: 0;
317
+}
318
+
319
+#themeEditor{
320
+  width: 100%;
321
+  max-width: 900px;
322
+}
323
+
324
+#themeEditor #row-instructions p{
325
+  line-height: 1.3;
326
+  margin: 0 auto;
327
+  padding: 10.5px 0;
328
+}
329
+
330
+#themeEditor #row-instructions p:first-of-type{
331
+  padding-top: 0;
332
+}
333
+
334
+#themeEditor #row-instructions i.fa{
335
+  margin: 0 2px;
336
+}
337
+
338
+#themeEditor #row-instructions #toggle-more-instructions{
339
+  cursor: pointer;
340
+}
341
+
342
+#themeEditor #row-instructions #toggle-more-instructions:hover{
343
+  text-decoration: underline;
344
+}
345
+
346
+#themeEditor #row-instructions #more-instructions{
347
+  display: none;
348
+}
349
+
350
+#themeEditor #editor-tools{
351
+  display: -webkit-box;
352
+  display: -ms-flexbox;
353
+  display: flex;
354
+  width: 100%;
355
+}
356
+
357
+#themeEditor #editor-tools #options-container,
358
+#themeEditor #editor-tools #preview-container{
359
+  width: 50%;
360
+}
361
+
362
+#themeEditor #preview.sticky{
363
+  top: 6px;
364
+  position: fixed;
365
+}
366
+
367
+#themeEditor #editor-tools #options-container{
368
+  padding-right: 20px;
369
+}
370
+
371
+#themeEditor .section{
372
+  padding: 13px 0;
373
+  border-top: 1px solid #e0e0e0;
374
+}
375
+
376
+#themeEditor .section:first-child{
377
+  padding-top: 0;
378
+  border-top: none;
379
+}
380
+
381
+#themeEditor .section-header{
382
+  cursor: pointer;
383
+}
384
+
385
+#themeEditor .section-toggle{
386
+  float: right;
387
+  width: 15px;
388
+  height: 15px;
389
+  margin: 2px 0 0 1px;
390
+  background-color: transparent;
391
+  border: 0;
392
+  padding-top: 3px;
393
+}
394
+
395
+#themeEditor .section-toggle.toggled{
396
+    -webkit-transform: rotate(180deg);
397
+        -ms-transform: rotate(180deg);
398
+            transform: rotate(180deg);
399
+}
400
+
401
+#themeEditor .section-options{
402
+  overflow: hidden;
403
+}
404
+
405
+#themeEditor .options-attribute{
406
+  display: -webkit-box;
407
+  display: -ms-flexbox;
408
+  display: flex;
409
+  -webkit-box-orient: horizontal;
410
+  -webkit-box-direction: normal;
411
+      -ms-flex-direction: row;
412
+          flex-direction: row;
413
+  -webkit-box-align: center;
414
+      -ms-flex-align: center;
415
+          align-items: center;
416
+  margin-bottom: 15px;
417
+}
418
+
419
+#themeEditor .options-attribute:first-of-type{
420
+  margin-top: 10px;
421
+}
422
+
423
+#themeEditor .options-attribute:last-of-type{
424
+  margin-bottom: 10px;
425
+}
426
+
427
+#themeEditor .section:last-of-type .options-attribute:last-of-type{
428
+  margin-bottom: 0;
429
+}
430
+
431
+#themeEditor .options-attribute-child{
432
+  -webkit-box-flex: 0;
433
+      -ms-flex-positive: 0;
434
+          flex-grow: 0;
435
+  margin: 0 10px;
436
+}
437
+
438
+#themeEditor .options-attribute-child:first-child{
439
+  -webkit-box-flex: 0;
440
+      -ms-flex-positive: 0;
441
+          flex-grow: 0;
442
+  margin: 0 10px 0 0;
443
+}
444
+
445
+#themeEditor .options-attribute-child:last-child{
446
+  margin: 0 0 0 10px;
447
+}
448
+
449
+#themeEditor .options-attribute label{
450
+  text-align: right;
451
+  margin-right: 0;
452
+  min-width: 130px;
453
+}
454
+
455
+#themeEditor .options-attribute .attribute-help{
456
+  margin-left: 0px;
457
+  border: 0px;
458
+  border-radius: 50px;
459
+  background-color: white;
460
+  padding: 3px 0px 7px 1px;
461
+  -webkit-box-sizing: border-box;
462
+          box-sizing: border-box;
463
+  font-size: 10px;
464
+  cursor: help;
465
+}
466
+
467
+#themeEditor .options-attribute .options-attribute-input{
468
+  -webkit-box-flex: 1;
469
+      -ms-flex-positive: 1;
470
+          flex-grow: 1;
471
+  width: auto;
472
+  padding: 6px;
473
+  color: #666;
474
+  font-size: 16px;
475
+  border: 1px solid #ddd;
476
+  border-radius: 4px;
477
+  -webkit-box-shadow: 0 0 0 0;
478
+          box-shadow: 0 0 0 0;
479
+}
480
+
481
+#themeEditor .options-attribute .options-attribute-input[type="color"]{
482
+  padding: 0px;
483
+  border: 0px;
484
+  margin: 10px 6px;
485
+}
486
+
487
+#themeEditor .options-attribute .options-attribute-input[disabled=""]{
488
+  cursor: not-allowed;
489
+}
490
+
491
+#themeEditor .options-note{
492
+  margin-top: -7px;
493
+}
494
+
495
+#themeEditor .options-note:empty{
496
+  display: none;
497
+}
498
+
499
+#themeEditor #imgUploader-label{
500
+  min-width: 0;
501
+}
502
+
503
+#themeEditor #imgUploader-label.button:hover{
504
+  color: #333;
505
+  background-color: #e6e6e6;
506
+  border-color: #adadad;
507
+}
508
+
509
+#themeEditor #imgUploader-label.button:active{
510
+  -webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
511
+          box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
512
+}
513
+
514
+#themeEditor #imgUploader-label i{
515
+  margin-right: 0;
516
+}
517
+
518
+#themeEditor #imgUploader{
519
+  width: 0.1px;
520
+  height: 0.1px;
521
+  opacity: 0;
522
+  overflow: hidden;
523
+  position: absolute;
524
+  z-index: -1;
525
+}
526
+
527
+#themeEditor #themeButtons button:first-child{
528
+  margin-left: 0;
529
+}
530
+
531
+#themeEditor #themeButtons i.fa{
532
+  margin: 0;
533
+}
534
+
535
+@media (max-width: 760px) {
536
+  #themeEditor #editor-tools{
537
+    -webkit-box-orient: vertical;
538
+    -webkit-box-direction: normal;
539
+        -ms-flex-direction: column;
540
+            flex-direction: column;
541
+  }
542
+
543
+  #themeEditor #editor-tools #options-container,
544
+  #themeEditor #editor-tools #preview-container{
545
+    width: 100%;
546
+  }
547
+
548
+  #themeEditor #preview.sticky{
549
+    top: 0;
550
+    position: initial;
551
+  }
552
+}

+ 2 - 0
editor/index.html ファイルの表示

@@ -28,6 +28,8 @@
28 28
         <div class="row form-row" id="row-theme">
29 29
           <label for="input-theme">Theme</label>
30 30
           <select id="input-theme" name="theme"></select>
31
+          <button id="theme-edit" title="Edit Theme"><i class="fa fa-pencil-square-o"></i></button>
32
+          <button id="new-theme" title="New Theme"><i class="fa fa-plus"></i></button>
31 33
         </div>
32 34
         <div class="row form-row" id="row-caption">
33 35
           <label for="input-caption">

+ 97 - 0
editor/themes.html ファイルの表示

@@ -0,0 +1,97 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <title>Theme Editor</title>
5
+    <meta charset="utf-8" />
6
+    <meta name="viewport" content="width=device-width, initial-scale=1">
7
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
8
+    <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600" rel="stylesheet" type="text/css">
9
+    <link href="/css/base.css" rel="stylesheet" type="text/css">
10
+    <link href="/css/editor.css" rel="stylesheet" type="text/css">
11
+    <link href="/fonts/fonts.css" rel="stylesheet" type="text/css">
12
+  </head>
13
+  <body class="loading">
14
+    <div id="themeEditor" class="container" data-active="false">
15
+      <p id="back-to-audiogram"><a href="../"><i class="fa fa-arrow-left"></i>Back to Audiogram</a></p>
16
+      <h1>Theme Editor</h1>
17
+      <div id="loading">
18
+        <div id="loading-bars">
19
+          <div class="r1"></div><div class="r2"></div><div class="r3"></div><div class="r4"></div><div class="r5"></div>
20
+        </div>
21
+        <div id="loading-message">Loading...</div>
22
+      </div><!-- #loading -->
23
+      <div id="loaded">
24
+        <div id="error"></div>
25
+        <div class="row form-row" id="row-audio" style="display:none;">
26
+          <label for="input-audio">Audio</label>
27
+          <input id="input-audio" name="audio" type="file" />
28
+        </div>
29
+        <div class="row" id="row-instructions">
30
+          <p>Welcome to the Theme Editor. From here you can edit existing themes or make new ones. Start by selecting a
31
+            theme to edit from the dropdown menu or choosing <code>New…</code> to make a new one. If you need more help, click this
32
+            <i id="toggle-more-instructions" class="fa fa-question"></i> or hover over the many others throughout the
33
+            page for more detailed help.</p>
34
+            <div id="more-instructions">
35
+              <p>Themes work by overriding the default styles your site administrator has set up. To override an option,
36
+                toggle it on with its checkbox. If an option isn’t toggled on, you won’t be able to adjust it. If you’re not
37
+                 sure what an option does, hover over the <i class="fa fa-question"></i> icon to get a description of it.</p>
38
+              <p>When you’re finished, you can click the <i class="fa fa-floppy-o"></i> button to save the changes to ONLY
39
+                the theme you’re currently editing. Switching themes will revert all the changes you’ve made in this
40
+                session. Themes can also be renamed by choosing them from the dropdown menu, changing the name option, and
41
+                saving the theme. Finally, if you’re absolutely sure, you can delete a theme by clicking the
42
+                <i class="fa fa-trash-o"></i> button. This action is irreversible and the theme will be gone forever.</p>
43
+              <p>Your theme preview changes live as you toggle options on and off and change their values. Experiment with
44
+                different values to create new and interesting themes. If you want to start over, you can click the
45
+                <i class="fa fa-refresh"></i> button to reset your theme.</p>
46
+            </div>
47
+        </div>
48
+        <div class="row form-row" id="row-theme">
49
+          <label for="input-theme">Theme</label>
50
+          <select id="input-theme" name="theme"></select>
51
+        </div>
52
+        <div class="row form-row" id="row-caption">
53
+          <label for="input-caption">
54
+            <div class="note">Preview a caption here.</div>
55
+          </label>
56
+          <input id="input-caption" name="caption" type="text" autocomplete="off" placeholder="Add a caption"
57
+            value="Lorem ipsum dolor sit amet, consectetur."/>
58
+        </div>
59
+
60
+        <div id="editor-tools">
61
+          <div id="options-container">
62
+            <div id="options"></div>
63
+            <div id="themeButtons">
64
+              <button id="saveChanges" title="Save Changes"><i class="fa fa-floppy-o"></i></button>
65
+              <button id="deleteTheme" title="Delete Theme"><i class="fa fa-trash-o"></i></button>
66
+              <button id="refreshTheme" title="Reset Theme"><i class="fa fa-refresh"></i></button>
67
+            </div>
68
+          </div>
69
+          <div id="preview-container">
70
+            <div id="preview">
71
+              <div style="background-color: black;">
72
+                <div id="canvas">
73
+                  <canvas width="640" height="360"></canvas>
74
+                  <div id="preview-label">Preview</div>
75
+                </div>
76
+              </div>
77
+            </div>
78
+          </div>
79
+        </div>
80
+
81
+        <div id="audio">
82
+          <audio controls>
83
+            <source />
84
+          </audio>
85
+        </div>
86
+        <div id="video">
87
+          <video width="640" height="360" controls>
88
+            <source type="video/mp4" />
89
+          </video>
90
+        </div>
91
+      </div><!-- #loaded -->
92
+    </div><!-- .container -->
93
+    <script src="/js/bundle.js"></script>
94
+    <!-- Force load custom fonts -->
95
+    <script src="/fonts/fonts.js"></script>
96
+  </body>
97
+</html>

+ 52 - 0
server/imageAPI.js ファイルの表示

@@ -0,0 +1,52 @@
1
+var fs = require('fs'),
2
+  path = require('path'),
3
+  multer = require('multer');
4
+
5
+var Storage = multer.diskStorage({
6
+  destination: function(req, file, callback) {
7
+    callback(null, './settings/backgrounds');
8
+  },
9
+  filename: function(req, file, callback) {
10
+    callback(null, file.originalname);
11
+  }
12
+});
13
+
14
+var upload = multer({
15
+  storage: Storage,
16
+  fileFilter: function(req, file, cb) {
17
+
18
+    var filetypes = /jpeg|jpg|png|gif|tiff|webp/;
19
+    var mimetype = filetypes.test(file.mimetype);
20
+    var extname = filetypes.test(path.extname(file.originalname).toLowerCase());
21
+
22
+    if (mimetype && extname) {
23
+      return cb(null, true);
24
+    }
25
+    cb('Error: File upload only supports the following image filetypes: ' + filetypes + '.');
26
+  }
27
+}).array('img', 1);
28
+
29
+var post = function(req, res){
30
+  upload(req, res, function(err) {
31
+    if(err)
32
+      return res.status(500).send(err);
33
+    else
34
+        return res.status(200).send('');
35
+  });
36
+};
37
+
38
+var get = function(req, res){
39
+  fs.readdir(path.join(__dirname, '..', 'settings', 'backgrounds'), function(err, files){
40
+    if(err)
41
+      res.status(500).send('An error occured reading the background directory.');
42
+    else{
43
+      res.setHeader('Content-Type', 'application/json');
44
+      res.status(200).send(JSON.stringify(files));
45
+    }
46
+  });
47
+};
48
+
49
+module.exports = {
50
+  post,
51
+  get
52
+};

+ 11 - 1
server/index.js ファイルの表示

@@ -4,13 +4,16 @@ var express = require("express"),
4 4
     path = require("path"),
5 5
     multer = require("multer"),
6 6
     uuid = require("uuid"),
7
-    mkdirp = require("mkdirp");
7
+    mkdirp = require("mkdirp"),
8
+    bodyParser = require('body-parser');
8 9
 
9 10
 // Routes and middleware
10 11
 var logger = require("../lib/logger/"),
11 12
     render = require("./render.js"),
12 13
     status = require("./status.js"),
13 14
     fonts = require("./fonts.js"),
15
+    themesAPI = require("./themesAPI.js"),
16
+    imageAPI = require("./imageAPI.js"),
14 17
     errorHandlers = require("./error.js");
15 18
 
16 19
 // Settings
@@ -76,6 +79,13 @@ app.use("/settings/", function(req, res, next) {
76 79
 
77 80
 }, express.static(path.join(__dirname, "..", "settings")));
78 81
 
82
+// Ovewrite themes file
83
+app.post("/api/themes", bodyParser.json(), themesAPI);
84
+
85
+// Get & Upload Images
86
+app.get("/api/images", imageAPI.get);
87
+app.post("/api/images", bodyParser.json(), imageAPI.post);
88
+
79 89
 // Serve editor files statically
80 90
 app.use(express.static(path.join(__dirname, "..", "editor")));
81 91
 

+ 89 - 0
server/themesAPI.js ファイルの表示

@@ -0,0 +1,89 @@
1
+var fs = require('fs'),
2
+  path = require('path');
3
+
4
+var themesPath = path.join(__dirname, '..','settings','themes.json');
5
+
6
+function getThemes(cb){
7
+  fs.readFile(themesPath, 'utf8', function (err, data) {
8
+    if (err){
9
+      if(err.code == 'ENOENT'){
10
+        cb('');
11
+      }
12
+    }
13
+    else{
14
+      cb(data);
15
+    }
16
+  });
17
+}
18
+
19
+function writeThemeFile(data, res){
20
+  var file = JSON.stringify(data, null, '\t');
21
+  fs.writeFile(themesPath, file, function(err){
22
+    if(err)
23
+      res.status(500).send('There was a problem saving "themes.json".');
24
+    else
25
+      res.status(200).send('');
26
+  });
27
+}
28
+
29
+function addTheme(body, res){
30
+
31
+  function save(data){
32
+    var themeFile = JSON.parse(data);
33
+
34
+    // add the new addition to the JSON and remove the name and themeName attributes
35
+    themeFile[body.name] = body;
36
+    delete themeFile[body.name].currentName;
37
+    delete themeFile[body.name].name;
38
+
39
+    writeThemeFile(themeFile, res);
40
+  }
41
+
42
+  getThemes(save);
43
+}
44
+
45
+function updateTheme(body, res){
46
+
47
+  function save(data){
48
+    var themeFile = JSON.parse(data);
49
+
50
+    // remove the old theme options
51
+    delete themeFile[body.currentName];
52
+
53
+    // add the new addition to the JSON and remove the name and themeName attributes
54
+    themeFile[body.name] = body;
55
+    delete themeFile[body.name].currentName;
56
+    delete themeFile[body.name].name;
57
+
58
+    writeThemeFile(themeFile, res);
59
+  }
60
+
61
+  getThemes(save);
62
+}
63
+
64
+function deleteTheme(body, res){
65
+
66
+  function save(data){
67
+    var themeFile = JSON.parse(data);
68
+
69
+    // remove the deleted theme options
70
+    delete themeFile[body.name];
71
+
72
+    writeThemeFile(themeFile, res);
73
+  }
74
+
75
+  getThemes(save);
76
+}
77
+
78
+module.exports = function(req, res){
79
+  switch (req.body.type) {
80
+    case 'ADD':
81
+      addTheme(req.body.data, res);
82
+      break;
83
+    case 'UPDATE':
84
+      updateTheme(req.body.data, res);
85
+      break;
86
+    case 'DELETE':
87
+      deleteTheme(req.body.data, res);
88
+  }
89
+};