Browse Source

Merge a9b4a281afee90552d0c641f3d3b633c148a701e into c1b14f115474d1469d474aecfd6584601d42e754

Andrew Briz 6 years ago
parent
commit
859610ed32
No account linked to committer's email

+ 1 - 0
.gitignore View File

7
 media/
7
 media/
8
 *.log
8
 *.log
9
 .jobs
9
 .jobs
10
+settings/backgrounds/

+ 1 - 0
.python-version View File

1
+2.7.13

+ 25 - 8
Dockerfile View File

1
 FROM ubuntu:16.04
1
 FROM ubuntu:16.04
2
 
2
 
3
 # Install dependencies
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
 # Non-privileged user
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
 USER audiogram
28
 USER audiogram
15
 WORKDIR /home/audiogram
29
 WORKDIR /home/audiogram
16
 
30
 
17
 # Clone repo
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
 WORKDIR /home/audiogram/audiogram
34
 WORKDIR /home/audiogram/audiogram
35
+#VOLUME /home/audiogram/audiogram
20
 
36
 
21
 # Install dependencies
37
 # Install dependencies
22
 RUN npm install
38
 RUN npm install
39
+CMD npm start

+ 11 - 12
INSTALL.md View File

111
 
111
 
112
 If you use [Docker](https://www.docker.com/products/docker), you can build an image from the included Dockerfile.
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
 ```sh
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
 ```sh
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
 ## AWS installation
135
 ## AWS installation

+ 1 - 1
audiogram/combine-frames.js View File

4
 
4
 
5
   // Raw ffmpeg command with standard mp4 setup
5
   // Raw ffmpeg command with standard mp4 setup
6
   // Some old versions of ffmpeg require -strict for the aac codec
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
   exec(cmd, cb);
9
   exec(cmd, cb);
10
 
10
 

+ 24 - 2
client/index.js View File

2
     $ = require("jquery"),
2
     $ = require("jquery"),
3
     preview = require("./preview.js"),
3
     preview = require("./preview.js"),
4
     video = require("./video.js"),
4
     video = require("./video.js"),
5
-    audio = require("./audio.js");
5
+    audio = require("./audio.js"),
6
+    themeEditor = require("./themeEditor.js");
6
 
7
 
7
 d3.json("/settings/themes.json", function(err, themes){
8
 d3.json("/settings/themes.json", function(err, themes){
8
 
9
 
27
     return;
28
     return;
28
   }
29
   }
29
 
30
 
31
+  if(themeEditor.isEditor())
32
+    themeEditor.initializeThemes(themes);
33
+
30
   for (var key in themes) {
34
   for (var key in themes) {
31
     themes[key] = $.extend({}, themes.default, themes[key]);
35
     themes[key] = $.extend({}, themes.default, themes[key]);
32
   }
36
   }
138
 
142
 
139
   // Populate dropdown menu
143
   // Populate dropdown menu
140
   d3.select("#input-theme")
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
     .selectAll("option")
150
     .selectAll("option")
143
     .data(themesWithImages)
151
     .data(themesWithImages)
144
     .enter()
152
     .enter()
182
 
190
 
183
   d3.select("#submit").on("click", submitted);
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
 function updateAudioFile() {
209
 function updateAudioFile() {

+ 8 - 4
client/preview.js View File

47
 // Resize video and preview canvas to maintain aspect ratio
47
 // Resize video and preview canvas to maintain aspect ratio
48
 function resize(width, height) {
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
       factor = Math.min(widthFactor, heightFactor);
56
       factor = Math.min(widthFactor, heightFactor);
53
 
57
 
54
   d3.select("canvas")
58
   d3.select("canvas")
69
 }
73
 }
70
 
74
 
71
 function redraw() {
75
 function redraw() {
72
-
73
   resize(theme.width, theme.height);
76
   resize(theme.width, theme.height);
74
 
77
 
75
   video.kill();
78
   video.kill();
111
   theme: _theme,
114
   theme: _theme,
112
   file: _file,
115
   file: _file,
113
   selection: _selection,
116
   selection: _selection,
114
-  loadAudio: loadAudio
117
+  loadAudio: loadAudio,
118
+  redraw: redraw
115
 };
119
 };

+ 501 - 0
client/themeEditor.js View File

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 View File

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 View File

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

+ 249 - 0
editor/css/editor.css View File

1
 div.container {
1
 div.container {
2
   width: 640px;
2
   width: 640px;
3
   margin-bottom: 2rem;
3
   margin-bottom: 2rem;
4
+  padding: 0 20px;
4
 }
5
 }
5
 
6
 
6
 h1 {
7
 h1 {
13
   color: #c00;
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
 /* Buttons/controls */
25
 /* Buttons/controls */
17
 button,
26
 button,
18
 .button {
27
 .button {
301
     -webkit-transform: scaleY(1.0);
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 View File

28
         <div class="row form-row" id="row-theme">
28
         <div class="row form-row" id="row-theme">
29
           <label for="input-theme">Theme</label>
29
           <label for="input-theme">Theme</label>
30
           <select id="input-theme" name="theme"></select>
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
         </div>
33
         </div>
32
         <div class="row form-row" id="row-caption">
34
         <div class="row form-row" id="row-caption">
33
           <label for="input-caption">
35
           <label for="input-caption">

+ 97 - 0
editor/themes.html View File

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 View File

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 View File

4
     path = require("path"),
4
     path = require("path"),
5
     multer = require("multer"),
5
     multer = require("multer"),
6
     uuid = require("uuid"),
6
     uuid = require("uuid"),
7
-    mkdirp = require("mkdirp");
7
+    mkdirp = require("mkdirp"),
8
+    bodyParser = require('body-parser');
8
 
9
 
9
 // Routes and middleware
10
 // Routes and middleware
10
 var logger = require("../lib/logger/"),
11
 var logger = require("../lib/logger/"),
11
     render = require("./render.js"),
12
     render = require("./render.js"),
12
     status = require("./status.js"),
13
     status = require("./status.js"),
13
     fonts = require("./fonts.js"),
14
     fonts = require("./fonts.js"),
15
+    themesAPI = require("./themesAPI.js"),
16
+    imageAPI = require("./imageAPI.js"),
14
     errorHandlers = require("./error.js");
17
     errorHandlers = require("./error.js");
15
 
18
 
16
 // Settings
19
 // Settings
76
 
79
 
77
 }, express.static(path.join(__dirname, "..", "settings")));
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
 // Serve editor files statically
89
 // Serve editor files statically
80
 app.use(express.static(path.join(__dirname, "..", "editor")));
90
 app.use(express.static(path.join(__dirname, "..", "editor")));
81
 
91
 

+ 89 - 0
server/themesAPI.js View File

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 View File

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
+}