ソースを参照

Merge a9b4a281afee90552d0c641f3d3b633c148a701e into c1b14f115474d1469d474aecfd6584601d42e754

Andrew Briz 6 年 前
コミット
859610ed32
No account linked to committer's email
共有17 個のファイルを変更した1364 個の追加132 個の削除を含む
  1. 1 0
      .gitignore
  2. 1 0
      .python-version
  3. 25 8
      Dockerfile
  4. 11 12
      INSTALL.md
  5. 1 1
      audiogram/combine-frames.js
  6. 24 2
      client/index.js
  7. 8 4
      client/preview.js
  8. 501 0
      client/themeEditor.js
  9. 160 0
      client/themeOptions.js
  10. 7 0
      docker-compose.yml
  11. 249 0
      editor/css/editor.css
  12. 2 0
      editor/index.html
  13. 97 0
      editor/themes.html
  14. 52 0
      server/imageAPI.js
  15. 11 1
      server/index.js
  16. 89 0
      server/themesAPI.js
  17. 125 104
      settings/themes.json

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

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

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

@@ -0,0 +1 @@
1
+2.7.13

+ 25 - 8
Dockerfile ファイルの表示

@@ -1,22 +1,39 @@
1 1
 FROM ubuntu:16.04
2 2
 
3 3
 # Install dependencies
4
-RUN apt-get update --yes && apt-get upgrade --yes
5
-RUN apt-get install git nodejs npm \
6
-libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev libpng-dev build-essential g++ \
7
-ffmpeg \
8
-redis-server --yes
4
+RUN apt-get --yes update && \
5
+    apt-get --yes upgrade
6
+RUN apt-get --yes install git \
7
+                          nodejs \
8
+                          npm \
9
+                          libcairo2-dev \
10
+                          libjpeg8-dev \
11
+                          libpango1.0-dev \
12
+                          libgif-dev \
13
+                          libpng-dev \
14
+                          build-essential \
15
+                          g++ \
16
+                          ffmpeg \
17
+                          redis-server
9 18
 
10
-RUN ln -s `which nodejs` /usr/bin/node
19
+RUN update-alternatives --install /usr/bin/node node $(which nodejs) 50
11 20
 
12 21
 # Non-privileged user
13
-RUN useradd -m audiogram
22
+ARG UID=1000
23
+RUN useradd --create-home \
24
+            --no-log-init \
25
+            --shell /bin/false \
26
+            --uid $UID \
27
+            audiogram
14 28
 USER audiogram
15 29
 WORKDIR /home/audiogram
16 30
 
17 31
 # Clone repo
18
-RUN git clone https://github.com/nypublicradio/audiogram.git
32
+RUN : breakcache0
33
+RUN git clone https://github.com/brizandrew/audiogram.git
19 34
 WORKDIR /home/audiogram/audiogram
35
+#VOLUME /home/audiogram/audiogram
20 36
 
21 37
 # Install dependencies
22 38
 RUN npm install
39
+CMD npm start

+ 11 - 12
INSTALL.md ファイルの表示

@@ -111,26 +111,25 @@ Installing these dependencies on Windows is an uphill battle.  If you're running
111 111
 
112 112
 If you use [Docker](https://www.docker.com/products/docker), you can build an image from the included Dockerfile.
113 113
 
114
-In addition to installing Docker, you'll need to install Git.  You can do this by installing [GitHub Desktop](https://desktop.github.com/).
114
+In addition to installing Docker, you may need to install Git.  You can do this by installing [GitHub Desktop](https://desktop.github.com/).
115 115
 
116
-You can clone the repo and build an image, or build it directly from the repo:
117
-
118
-```sh
119
-git clone https://github.com/nypublicradio/audiogram.git
120
-cd audiogram
121
-docker build -t audiogram .
122
-```
116
+Some operating systems e.g. Ubuntu provide a permission group "docker. You can join this group like `sudo usermod -aG docker $USER` and then it's not necessary to prepend every docker command with `sudo`. It's necessary to log out of the desktop session for this permission group change to take effect.
123 117
 
124
-or
118
+You can clone the repo and build an image, or build it directly from the repo:
125 119
 
120
+An ephemeral container image can be built and run like
126 121
 ```sh
127
-docker build -t audiogram https://github.com/nypublicradio/audiogram.git
122
+docker build -t audiogram https://github.com/brizandrew/audiogram.git
123
+docker run -p 8888:8888 audiogram
128 124
 ```
129 125
 
130
-Now you can run Audiogram in a container using that image:
126
+or you can clone the source code to obtain the service file for docker compose and run these commands once in order to always have the container running in the background and available at http://localhost:8888.
131 127
 
132 128
 ```sh
133
-docker run -p 8888:8888 -t -i audiogram
129
+git clone https://github.com/brizandrew/audiogram.git
130
+cd audiogram
131
+docker build -t audiogram .
132
+docker-compose up
134 133
 ```
135 134
 
136 135
 ## AWS installation

+ 1 - 1
audiogram/combine-frames.js ファイルの表示

@@ -4,7 +4,7 @@ function combineFrames(options, cb) {
4 4
 
5 5
   // Raw ffmpeg command with standard mp4 setup
6 6
   // Some old versions of ffmpeg require -strict for the aac codec
7
-  var cmd = "ffmpeg -r " + options.framesPerSecond + " -i \"" + options.framePath + "\" -i \"" + options.audioPath + "\" -c:v libx264 -c:a aac -strict experimental -shortest -pix_fmt yuv420p \"" + options.videoPath + "\"";
7
+  var cmd = "ffmpeg -r " + options.framesPerSecond + " -i \"" + options.framePath + "\" -i \"" + options.audioPath + "\" -c:v libx264 -c:a aac -strict experimental -shortest -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' \"" + options.videoPath + "\"";
8 8
 
9 9
   exec(cmd, cb);
10 10
 

+ 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
+];

+ 7 - 0
docker-compose.yml ファイルの表示

@@ -0,0 +1,7 @@
1
+version: '2'
2
+services:
3
+  audiogram:
4
+    image: audiogram
5
+    ports:
6
+        - 8888:8888
7
+    restart: always

+ 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: 20%;
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: post,
51
+  get: 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
+};

+ 125 - 104
settings/themes.json ファイルの表示

@@ -1,105 +1,126 @@
1 1
 {
2
-  "default": {
3
-    "width": 1280,
4
-    "height": 720,
5
-    "framesPerSecond": 20,
6
-    "maxDuration": 300,
7
-    "samplesPerFrame": 128,
8
-    "pattern": "wave",
9
-    "waveTop": 150,
10
-    "waveBottom": 420,
11
-    "captionTop": 470,
12
-    "captionFont": "300 52px 'Source Sans Pro'",
13
-    "captionLineHeight": 52,
14
-    "captionLineSpacing": 7,
15
-    "captionLeft": 200,
16
-    "captionRight": 1080
17
-  },
18
-  "Basic": {
19
-    "backgroundColor": "#eee",
20
-    "foregroundColor": "#de1e3d"
21
-  },
22
-  "Neon": {
23
-    "backgroundColor": "#556270",
24
-    "foregroundColor": "#c7f464"
25
-  },
26
-  "Three colors": {
27
-    "backgroundColor": "#eee",
28
-    "captionColor": "#515151",
29
-    "waveColor": "#00b4ff"
30
-  },
31
-  "Background image with top right text": {
32
-    "captionAlign": "right",
33
-    "captionTop": 60,
34
-    "captionRight": 1220,
35
-    "captionLeft": 640,
36
-    "backgroundImage": "subway.jpg",
37
-    "foregroundColor": "#fc0",
38
-    "waveBottom": 660,
39
-    "waveTop": 320
40
-  },
41
-  "Bars": {
42
-    "pattern": "bars",
43
-    "foregroundColor": "#d84a4a"
44
-  },
45
-  "Rounded bars with bottom left text": {
46
-    "pattern": "roundBars",
47
-    "foregroundColor": "#d84a4a",
48
-    "captionAlign": "left",
49
-    "captionLeft": 60,
50
-    "captionRight": 640,
51
-    "captionBottom": 660,
52
-    "captionTop": null
53
-  },
54
-  "Line": {
55
-    "backgroundColor": "#edc951",
56
-    "foregroundColor": "#00a0b0",
57
-    "pattern": "line"
58
-  },
59
-  "Curve": {
60
-    "backgroundColor": "#00a0b0",
61
-    "foregroundColor": "#edc951",
62
-    "pattern": "curve"
63
-  },
64
-  "Bricks": {
65
-    "backgroundColor": "#8771aa",
66
-    "foregroundColor": "#fff",
67
-    "pattern": "bricks"
68
-  },
69
-  "Equalizer on the bottom": {
70
-    "backgroundColor": "#222",
71
-    "foregroundColor": "#fff",
72
-    "pattern": "equalizer",
73
-    "captionTop": 40,
74
-    "captionBottom": 380,
75
-    "waveTop": 420,
76
-    "waveBottom": 720
77
-  },
78
-  "Bold italic orange": {
79
-    "captionAlign": "right",
80
-    "captionTop": null,
81
-    "captionBottom": 660,
82
-    "captionRight": 1220,
83
-    "captionLeft": 640,
84
-    "backgroundColor": "#fd5a1e",
85
-    "waveColor": "#fff",
86
-    "captionFont": "bold italic 52px 'Source Sans Pro'"
87
-  },
88
-  "Pixelated": {
89
-    "backgroundColor": "#ecd078",
90
-    "foregroundColor": "#c02942",
91
-    "pattern": "pixel"
92
-  },
93
-  "Square with background image": {
94
-    "width": 640,
95
-    "height": 640,
96
-    "samplesPerFrame": 64,
97
-    "waveTop": 20,
98
-    "waveBottom": 300,
99
-    "captionTop": 340,
100
-    "captionLeft": 20,
101
-    "captionRight": 620,
102
-    "foregroundColor": "#0eb8ba",
103
-    "backgroundImage": "nyc.png"
104
-  }
105
-}
2
+	"default": {
3
+		"width": 1280,
4
+		"height": 720,
5
+		"framesPerSecond": 20,
6
+		"maxDuration": 300,
7
+		"samplesPerFrame": 128,
8
+		"backgroundColor": "#ffffff",
9
+		"backgroundImage": "",
10
+		"captionColor": "#000000",
11
+		"captionAlign": "left",
12
+		"captionFont": "300 52px 'Source Sans Pro'",
13
+		"captionLineHeight": 52,
14
+		"captionLineSpacing": 7,
15
+		"captionLeft": 60,
16
+		"captionRight": 640,
17
+		"captionTop": null,
18
+		"captionBottom": 660,
19
+		"pattern": "wave",
20
+		"waveTop": 0,
21
+		"waveBottom": 720,
22
+		"waveLeft": 0,
23
+		"waveRight": 1280,
24
+		"waveColor": "#000000"
25
+	},
26
+	"Basic": {
27
+		"backgroundColor": "#eee",
28
+		"captionColor": "#de1e3d",
29
+		"waveColor": "#de1e3d"
30
+	},
31
+	"Neon": {
32
+		"backgroundColor": "#556270",
33
+		"captionColor": "#c7f464",
34
+		"waveColor": "#c7f464"
35
+	},
36
+	"Background image with top right text": {
37
+		"captionAlign": "right",
38
+		"captionTop": 60,
39
+		"captionRight": 1220,
40
+		"captionLeft": 640,
41
+		"backgroundImage": "subway.jpg",
42
+		"captionColor": "#fc0fc0",
43
+		"waveColor": "#fc0fc0",
44
+		"waveBottom": 660,
45
+		"waveTop": 320
46
+	},
47
+	"Bars": {
48
+		"pattern": "bars",
49
+		"captionColor": "#d84a4a",
50
+		"waveColor": "#d84a4a"
51
+	},
52
+	"Rounded bars with bottom left text": {
53
+		"pattern": "roundBars",
54
+		"captionColor": "#d84a4a",
55
+		"waveColor": "#d84a4a",
56
+		"captionAlign": "left",
57
+		"captionLeft": 60,
58
+		"captionRight": 640,
59
+		"captionBottom": 660,
60
+		"captionTop": null
61
+	},
62
+	"Line": {
63
+		"backgroundColor": "#edc951",
64
+		"captionColor": "#00a0b0",
65
+		"waveColor": "#00a0b0",
66
+		"pattern": "line"
67
+	},
68
+	"Curve": {
69
+		"backgroundColor": "#00a0b0",
70
+		"captionColor": "#edc951",
71
+		"waveColor": "#edc951",
72
+		"pattern": "curve"
73
+	},
74
+	"Bricks": {
75
+		"backgroundColor": "#8771aa",
76
+		"captionColor": "#ffffff",
77
+		"waveColor": "#ffffff",
78
+		"pattern": "bricks"
79
+	},
80
+	"Equalizer on the bottom": {
81
+		"backgroundColor": "#222",
82
+		"captionColor": "#ffffff",
83
+		"waveColor": "#ffffff",
84
+		"pattern": "equalizer",
85
+		"captionTop": 40,
86
+		"captionBottom": 380,
87
+		"waveTop": 420,
88
+		"waveBottom": 720
89
+	},
90
+	"Bold italic orange": {
91
+		"captionAlign": "right",
92
+		"captionTop": null,
93
+		"captionBottom": 660,
94
+		"captionRight": 1220,
95
+		"captionLeft": 640,
96
+		"backgroundColor": "#fd5a1e",
97
+		"waveColor": "#fff",
98
+		"captionFont": "bold italic 52px 'Source Sans Pro'"
99
+	},
100
+	"Pixelated": {
101
+		"backgroundColor": "#ecd078",
102
+		"captionColor": "#c02942",
103
+		"waveColor": "#c02942",
104
+		"pattern": "pixel"
105
+	},
106
+	"Square with background image": {
107
+		"width": 640,
108
+		"height": 640,
109
+		"samplesPerFrame": 64,
110
+		"waveTop": 20,
111
+		"waveBottom": 300,
112
+		"captionTop": 340,
113
+		"captionLeft": 20,
114
+		"captionRight": 620,
115
+		"captionColor": "#0eb8ba",
116
+		"waveColor": "#0eb8ba",
117
+		"backgroundImage": "nyc.png"
118
+	},
119
+	"Three colors": {
120
+		"backgroundColor": "#eeeeee",
121
+		"captionColor": "#515151",
122
+		"waveTop": 100,
123
+		"waveBottom": 500,
124
+		"waveColor": "#53fe64"
125
+	}
126
+}