diff --git a/.gitignore b/.gitignore index 3829b4ca..6cc15f26 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ junk/ media/ *.log .jobs +settings/backgrounds/ diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..ecc17b8e --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +2.7.13 diff --git a/Dockerfile b/Dockerfile index b3200ac3..795a5a88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,39 @@ FROM ubuntu:16.04 # Install dependencies -RUN apt-get update --yes && apt-get upgrade --yes -RUN apt-get install git nodejs npm \ -libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev libpng-dev build-essential g++ \ -ffmpeg \ -redis-server --yes +RUN apt-get --yes update && \ + apt-get --yes upgrade +RUN apt-get --yes install git \ + nodejs \ + npm \ + libcairo2-dev \ + libjpeg8-dev \ + libpango1.0-dev \ + libgif-dev \ + libpng-dev \ + build-essential \ + g++ \ + ffmpeg \ + redis-server -RUN ln -s `which nodejs` /usr/bin/node +RUN update-alternatives --install /usr/bin/node node $(which nodejs) 50 # Non-privileged user -RUN useradd -m audiogram +ARG UID=1000 +RUN useradd --create-home \ + --no-log-init \ + --shell /bin/false \ + --uid $UID \ + audiogram USER audiogram WORKDIR /home/audiogram # Clone repo -RUN git clone https://github.com/nypublicradio/audiogram.git +RUN : breakcache0 +RUN git clone https://github.com/brizandrew/audiogram.git WORKDIR /home/audiogram/audiogram +#VOLUME /home/audiogram/audiogram # Install dependencies RUN npm install +CMD npm start diff --git a/INSTALL.md b/INSTALL.md index aed8af62..0af5d09f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -111,26 +111,25 @@ Installing these dependencies on Windows is an uphill battle. If you're running If you use [Docker](https://www.docker.com/products/docker), you can build an image from the included Dockerfile. -In addition to installing Docker, you'll need to install Git. You can do this by installing [GitHub Desktop](https://desktop.github.com/). +In addition to installing Docker, you may need to install Git. You can do this by installing [GitHub Desktop](https://desktop.github.com/). -You can clone the repo and build an image, or build it directly from the repo: - -```sh -git clone https://github.com/nypublicradio/audiogram.git -cd audiogram -docker build -t audiogram . -``` +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. -or +You can clone the repo and build an image, or build it directly from the repo: +An ephemeral container image can be built and run like ```sh -docker build -t audiogram https://github.com/nypublicradio/audiogram.git +docker build -t audiogram https://github.com/brizandrew/audiogram.git +docker run -p 8888:8888 audiogram ``` -Now you can run Audiogram in a container using that image: +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. ```sh -docker run -p 8888:8888 -t -i audiogram +git clone https://github.com/brizandrew/audiogram.git +cd audiogram +docker build -t audiogram . +docker-compose up ``` ## AWS installation diff --git a/audiogram/combine-frames.js b/audiogram/combine-frames.js index 087a5b7c..f0ab2c22 100644 --- a/audiogram/combine-frames.js +++ b/audiogram/combine-frames.js @@ -4,7 +4,7 @@ function combineFrames(options, cb) { // Raw ffmpeg command with standard mp4 setup // Some old versions of ffmpeg require -strict for the aac codec - 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 + "\""; + 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 + "\""; exec(cmd, cb); diff --git a/client/index.js b/client/index.js index e65343d6..f4ae9237 100644 --- a/client/index.js +++ b/client/index.js @@ -2,7 +2,8 @@ var d3 = require("d3"), $ = require("jquery"), preview = require("./preview.js"), video = require("./video.js"), - audio = require("./audio.js"); + audio = require("./audio.js"), + themeEditor = require("./themeEditor.js"); d3.json("/settings/themes.json", function(err, themes){ @@ -27,6 +28,9 @@ d3.json("/settings/themes.json", function(err, themes){ return; } + if(themeEditor.isEditor()) + themeEditor.initializeThemes(themes); + for (var key in themes) { themes[key] = $.extend({}, themes.default, themes[key]); } @@ -138,7 +142,11 @@ function initialize(err, themesWithImages) { // Populate dropdown menu d3.select("#input-theme") - .on("change", updateTheme) + .on("change.a", updateTheme) + .on("change.b", function(){ + if(themeEditor.isActive()) + themeEditor.populateThemeFields(); + }) .selectAll("option") .data(themesWithImages) .enter() @@ -182,6 +190,20 @@ function initialize(err, themesWithImages) { d3.select("#submit").on("click", submitted); + d3.select("#theme-edit").on("click", function(){ + var activeTheme = d3.select('#input-theme').property('value'); + var url = window.location.origin + window.location.pathname + 'themes.html?t=' + activeTheme; + window.location = url; + }); + + d3.select("#new-theme").on("click", function(){ + var url = window.location.origin + window.location.pathname + 'themes.html?t=default' + window.location = url; + }); + + if(themeEditor.isEditor()) + themeEditor.initialize(); + } function updateAudioFile() { diff --git a/client/preview.js b/client/preview.js index 65129d46..578733c3 100644 --- a/client/preview.js +++ b/client/preview.js @@ -47,8 +47,12 @@ minimap.onBrush(function(extent){ // Resize video and preview canvas to maintain aspect ratio function resize(width, height) { - var widthFactor = 640 / width, - heightFactor = 360 / height, + var containerWidth = document.querySelector('#preview').clientWidth, + aspectRatio = height/width; + containerWidth = containerWidth > 0 ? containerWidth : 640; + + var widthFactor = containerWidth / width, + heightFactor = containerWidth * aspectRatio / height, factor = Math.min(widthFactor, heightFactor); d3.select("canvas") @@ -69,7 +73,6 @@ function resize(width, height) { } function redraw() { - resize(theme.width, theme.height); video.kill(); @@ -111,5 +114,6 @@ module.exports = { theme: _theme, file: _file, selection: _selection, - loadAudio: loadAudio + loadAudio: loadAudio, + redraw: redraw }; diff --git a/client/themeEditor.js b/client/themeEditor.js new file mode 100644 index 00000000..936374d0 --- /dev/null +++ b/client/themeEditor.js @@ -0,0 +1,501 @@ +var d3 = require('d3'), + $ = require('jquery'), + preview = require('./preview.js'), + options = require('./themeOptions.js'); + +var editorIsActive = false; +var themesJson = {}; + +function updateTheme(options) { + if(options !== undefined){ + if(options.backgroundImage !== '') + getImage(options); + preview.theme(options); + } + else{ + preview.theme(d3.select(this.options[this.selectedIndex]).datum()); + } +} + +function getImage(theme) { + if (theme.backgroundImage) { + theme.backgroundImageFile = new Image(); + + theme.backgroundImageFile.onload = function(){ + updateTheme(anonymousTheme()); + }; + + theme.backgroundImageFile.src = '/settings/backgrounds/' + theme.backgroundImage; + } +} + +function error(msg) { + if (msg.responseText) { + msg = msg.responseText; + } + if (typeof msg !== 'string') { + msg = JSON.stringify(msg); + } + if (!msg) { + msg = 'Unknown error'; + } + d3.select('#loading-message').text('Loading...'); + setClass('error', msg); +} + +function setClass(cl, msg) { + d3.select('body').attr('class', cl || null); + d3.select('#error').text(msg || ''); +} + +function anonymousTheme(){ + var output = {}; + d3.selectAll('.options-attribute-input').each(function(d){ + if(d.type == 'number'){ + if(Number.isNaN(parseFloat(this.value))) + output[d.name] = null; + else + output[d.name] = parseFloat(this.value); + } + else{ + output[d.name] = this.value; + } + }); + + return output; +} + +function postTheme(type, data, cb){ + var postData = { + type: type, + data: data + }; + + $.ajax({ + url: '/api/themes', + type: 'POST', + data: JSON.stringify(postData), + contentType: 'application/json', + cache: false, + success: cb, + error: error + }); +} + +function saveChanges(){ + var activeTheme = preview.theme(); + var activeThemeName = d3.select('#input-theme').property('value'); + + function makeTheme(){ + var file = {}; + + file.name = activeTheme.name; + file.currentName = activeThemeName; + + d3.selectAll('.options-attribute-input').each(function(d){ + if(!this.disabled){ + if(this.getAttribute('type') == 'number') + file[d.name] = parseFloat(this.value); + else + file[d.name] = this.value; + } + }); + + return file; + } + + function reloadPage(){ + var url = window.location.origin + window.location.pathname + '?t=' + activeTheme.name; + window.location = url; + } + + var themeNames = []; + d3.selectAll('#input-theme option').each(function(d){ + themeNames.push(d.name); + }); + themeNames.splice(themeNames.length-1, 1); + + var postData = makeTheme(); + + if(activeTheme.name == 'default' || activeTheme.name == '0' || activeTheme.name == ''){ + window.alert('Please give your theme a name.'); + } + else if(themeNames.indexOf(activeTheme.name) != -1 && activeThemeName == 'default'){ + window.alert('That theme name already exists. Please choose a different name or select it to edit it.'); + } + else if(themeNames.indexOf(activeThemeName) != -1){ + var msg = 'Are you sure you want to override the options for "'; + msg += activeThemeName + '"? This action is permanent and cannot be undone.'; + if(window.confirm(msg)){ + postTheme('UPDATE', postData, reloadPage); + } + } + else{ + postTheme('ADD', postData, reloadPage); + } +} + +function deleteTheme(){ + var activeThemeName = d3.select('#input-theme').property('value'); + + var msg = 'Are you sure you want to delete the theme "'; + msg += activeThemeName + '"? This action is permanent and cannot be undone.'; + + if(window.confirm(msg)){ + var postData = {name: activeThemeName}; + postTheme('DELETE', postData, function(){ + var url = window.location.origin + window.location.pathname; + window.location = url; + }); + } +} + +function refreshTheme(){ + var theme = $.extend({name: preview.theme().name}, themesJson.default, themesJson[preview.theme().name]); + updateTheme(theme); + populateThemeFields(); +} + +function loadBkgndImages(cb){ + $.ajax({ + url: '/api/images', + type: 'GET', + cache: false, + success: function(data){ + $('#attribute-backgroundImage').html(''); + + var bkgndImgSelect = d3.select('#attribute-backgroundImage'); + for(var img of data){ + bkgndImgSelect.append('option') + .attr('value', img) + .text(img); + } + + if(cb !== undefined){ + cb(); + } + + }, + error: error + }); +} + +function uploadImage(){ + // this = the file input calling the function + var img = this.files[0]; + + var confirmed = confirm('Are you sure you want to upload ' + img.name + '?'); + if(confirmed){ + var formData = new FormData(); + formData.append('img', img); + + setClass('loading'); + + $.ajax({ + url: '/api/images', + type: 'POST', + data: formData, + contentType: false, + cache: false, + processData: false, + success: function(){ + var setImg = function(){ + var input = d3.select('#container-backgroundImage').select('.options-attribute-input'); + input.property('value', img.name).attr('data-value', img.name); + + updateTheme(anonymousTheme()); + + setClass(null); + }; + + loadBkgndImages(setImg); + }, + error: error + }); + } +} + +function camelToTitle(string){ + var conversion = string.replace( /([A-Z])/g, ' $1' ); + var title = conversion.charAt(0).toUpperCase() + conversion.slice(1); + return title; +} + +function queryParser(query){ + query = query.substring(1); + let query_string = {}; + const vars = query.split('&'); + for (var i=0;i= top){ + $(container).addClass('sticky'); + } + else{ + $(container).removeClass('sticky'); + } + }); + + preview.redraw(); +} + +function toggleSection(d){ + var name; + + if(typeof(d) == 'object') + name = d.name; + else + name = d; + + $('#section-options-'+ name).slideToggle(500); + $('#section-toggle-' + name).toggleClass('toggled'); +} + +function initialize(){ + var container = d3.select('#options'); + + // Add Option Sections + var sections = container.selectAll('.section').data(options) + .enter() + .append('div') + .attr('class', 'section'); + + var headers = sections.append('div') + .attr('class', 'section-header') + .on('click', toggleSection); + + // Add Section Toggles + headers.append('svg') + .attr('id', function(d){return 'section-toggle-' + d.name;}) + .attr('class', 'section-toggle') + .attr('viewBox', '0 0 24 24') + .append('path') + .attr('d', function(){ + 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,' + + '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'; + return path; + }); + + // Add Section Titles + headers.append('h4') + .attr('class', 'section-title') + .text(function(d){return d.name;}); + + var attributesContainer = sections.append('div') + .attr('class', 'section-options') + .attr('id', function(d){return 'section-options-' + d.name;}) + .style('display', 'none'); + + // Add Option Container + var attributes = attributesContainer.selectAll('.options-attribute') + .data(function(d){return d.options;}) + .enter() + .append('div') + .attr('class','options-attribute') + .attr('id', function(d){return 'container-' + d.name;}) + .attr('data-type', function(d){return d.type;}); + + // Add Enable Checkboxes + attributes.append('input') + .attr('id', function(d){return 'enable-' + d.name;}) + .attr('class', 'options-checkbox options-attribute-child') + .attr('name', function(d){return 'enable-' + d.name;}) + .attr('value', 'true') + .attr('type', 'checkbox') + .property('checked', true) + .on('click', function(d){ + var input = d3.select('#attribute-' + d.name); + if(this.checked){ + input.property('disabled', false); + input.property('value', input.attr('data-value')); + input.attr('value', input.attr('data-value')); + updateTheme(anonymousTheme()); + } + else{ + input.property('value', themesJson.default[d.name]); + input.attr('value', themesJson.default[d.name]); + input.property('disabled', true); + updateTheme(anonymousTheme()); + } + }); + + // Add Option Labels + attributes.append('label') + .attr('for', function(d){return 'attribute-' + d.name;}) + .attr('class', 'options-attribute-child') + .text(function(d){return camelToTitle(d.name);}); + + // Add Help Text Icons + attributes.append('i') + .attr('id', function(d){return 'help-' + d.name;}) + .attr('class', 'attribute-help options-attribute-child fa fa-question') + .attr('title', function(d){return d.help;}); + + // Add Inputs For Text Fields + sections.selectAll('.options-attribute:not([data-type="select"])') + .append('input') + .attr('id', function(d){return 'attribute-' + d.name;}) + .attr('class', 'options-attribute-input options-attribute-child input-text') + .attr('name', function(d){return d.name;}) + .attr('type', function(d){return d.type;}) + .on('input', function(){ + this.setAttribute('data-value', this.value); + updateTheme(anonymousTheme()); + }); + + // Add Inputs For Select Fields + sections.selectAll('.options-attribute[data-type="select"]') + .append('select') + .attr('id', function(d){return 'attribute-' + d.name;}) + .attr('class', 'options-attribute-input options-attribute-child input-select') + .attr('name', function(d){return d.name;}) + .attr('type', function(d){return d.type;}) + .on('input', function(){updateTheme(anonymousTheme());}) + .selectAll('options') + .data(function(d){return d.options;}) + .enter() + .append('option') + .attr('value', function(d){return d;}) + .text(function(d){return camelToTitle(d);}); + + // Add Section Notes + attributesContainer.append('p') + .attr('class', 'options-note note') + .text(function(d){return d.note;}); + + // Add "New..." option to theme select + d3.select('#input-theme') + .append('option') + .data([themesJson.default]) + .attr('value', 'default') + .text('New...'); + + // Add clickHandler for Save Button + d3.select('#saveChanges') + .on('click', saveChanges); + + // Add clickHandler for Delete Button + d3.select('#deleteTheme') + .on('click', deleteTheme); + + // Add clickHandler for Refresh Button + d3.select('#refreshTheme') + .on('click', refreshTheme); + + // Background Images Populate and Add Uploader + loadBkgndImages(populateThemeFields); + var bkgndImgContainer = d3.select('#container-backgroundImage'); + bkgndImgContainer.append('label') + .attr('for', 'imgUploader') + .attr('id', 'imgUploader-label') + .attr('class', 'button') + .append('i') + .attr('class', 'fa fa-upload'); + bkgndImgContainer.append('input') + .attr('type', 'file') + .attr('id', 'imgUploader') + .attr('name', 'imgUploader') + .on('change', uploadImage); + + toggleSection('Metadata'); + + // Active url theme + var selectedTheme = queryParser(window.location.search); + if('t' in selectedTheme && selectedTheme.t in themesJson){ + d3.select('#input-theme') + .property('value', selectedTheme.t) + .dispatch('change'); + } + + populateThemeFields(); + + initializePreview(); + window.addEventListener('resize', function(){ + preview.redraw(); + }); + + d3.select('#toggle-more-instructions').on('click', function(){ + $('#more-instructions').slideToggle(500); + }) + + + // Toggle the editor container to loaded + editorIsActive = true; +} + +function populateThemeFields(){ + var activeTheme = preview.theme(); + var themeJson = activeTheme.name !== undefined && activeTheme.name in themesJson ? + themesJson[activeTheme.name] : + themesJson.default; + + d3.selectAll('.options-attribute').each(function(d){ + var input = d3.select(this).select('.options-attribute-input'); + + var activeValue = d.name in activeTheme ? activeTheme[d.name] : ''; + input.property('value', activeValue).attr('data-value', activeValue); + + if(d.name == 'name'){ + d3.select('#enable-'+d.name).property('checked', true); + input.property('disabled', false); + } + else if(activeTheme.name == undefined || !(d.name in themeJson)){ + d3.select('#enable-'+d.name).property('checked', false); + input.property('disabled', true); + } + else{ + d3.select('#enable-'+d.name).property('checked', true); + input.property('disabled', false); + } + + }); +} + +window.themesJson = themesJson; + +module.exports = { + isEditor, + isActive, + initialize, + initializeThemes, + populateThemeFields +}; diff --git a/client/themeOptions.js b/client/themeOptions.js new file mode 100644 index 00000000..19975e68 --- /dev/null +++ b/client/themeOptions.js @@ -0,0 +1,160 @@ +module.exports = [ + { + 'name': 'Metadata', + 'options': [ + { + 'name': 'name', + 'type': 'text', + 'help': 'The name of the theme.' + } + ] + }, + { + 'name': 'Movie', + 'options': [ + { + 'name': 'width', + 'type': 'number', + 'help': 'Desired video width in pixels.' + }, + { + 'name': 'height', + 'type': 'number', + 'help': 'Desired video height in pixels.' + }, + { + 'name': 'framesPerSecond', + 'type': 'number', + 'help': 'Desired video framerate.' + }, + { + 'name': 'samplesPerFrame', + 'type': 'number', + 'help': 'How many data points to use for the waveform. More points = a more detailed wave. (e.g. 128)' + }, + { + 'name': 'maxDuration', + 'type': 'number', + 'help': 'Maximum duration of an audiogram, in seconds (e.g. set this to 30 to enforce a 30-second time limit).' + } + ] + }, + { + 'name': 'Background', + '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.', + 'options': [ + { + 'name': 'backgroundImage', + 'type': 'select', + 'help': 'What image to put in the background of every frame, it should be a file in settings/backgrounds/', + 'options': [] + }, + { + 'name': 'backgroundColor', + 'type': 'color', + 'help': 'A CSS color to fill the background of every frame (e.g. pink or #ff00ff).' + } + ] + }, + { + 'name': 'Caption', + '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.', + 'options': [ + { + 'name': 'captionColor', + 'type': 'color', + 'help': 'A CSS color, what color the text should be (e.g. red or #ffcc00).' + }, + { + 'name': 'captionAlign', + 'type': 'select', + 'help': 'Text alignment of the caption.', + 'options': [ + 'left', + 'right', + 'center' + ] + }, + { + 'name': 'captionFont', + 'type': 'text', + 'help': 'A full CSS font definition to use for the caption. ([weight] size \'family\').' + }, + { + 'name': 'captionLineHeight', + 'type': 'number', + 'help': 'How tall each caption line is in pixels. You\'ll want to adjust this for whatever font and font size you\'re using.' + }, + { + 'name': 'captionLineSpacing', + 'type': 'number', + '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.' + }, + { + 'name': 'captionLeft', + 'type': 'number', + 'help': 'How many pixels from the left edge to place the caption' + }, + { + 'name': 'captionRight', + 'type': 'number', + 'help': 'How many pixels from the right edge to place the caption' + }, + { + 'name': 'captionBottom', + 'type': 'number', + 'help': 'How many pixels from the bottom edge to place the caption.' + }, + { + 'name': 'captionTop', + 'type': 'number', + 'help': 'How many pixels from the top edge to place the caption.' + } + ] + }, + { + 'name': 'Wave', + 'options': [ + { + 'name': 'pattern', + 'type': 'select', + 'help': 'What waveform shape to draw.', + 'options': [ + 'wave', + 'bars', + 'line', + 'curve', + 'roundBars', + 'pixel', + 'bricks', + 'equalizer' + ] + }, + { + 'name': 'waveTop', + 'type': 'number', + 'help': 'How many pixels from the top edge to start the waveform.' + }, + { + 'name': 'waveBottom', + 'type': 'number', + 'help': 'How many pixels from the top edge to end the waveform.' + }, + { + 'name': 'waveLeft', + 'type': 'number', + 'help': 'How many pixels from the left edge to start the waveform.' + }, + { + 'name': 'waveRight', + 'type': 'number', + 'help': 'How many pixels from the right edge to start the waveform.' + }, + { + 'name': 'waveColor', + 'type': 'color', + 'help': 'A CSS color, what color the wave should be.' + } + ] + } +]; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..20bd1599 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +version: '2' +services: + audiogram: + image: audiogram + ports: + - 8888:8888 + restart: always diff --git a/editor/css/editor.css b/editor/css/editor.css index 4712374e..1ece8e5c 100644 --- a/editor/css/editor.css +++ b/editor/css/editor.css @@ -1,6 +1,7 @@ div.container { width: 640px; margin-bottom: 2rem; + padding: 0 20px; } h1 { @@ -13,6 +14,14 @@ h1 { color: #c00; } +code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; +} + /* Buttons/controls */ button, .button { @@ -301,3 +310,243 @@ g.time line { -webkit-transform: scaleY(1.0); } } + +#theme-edit i.fa, +#new-theme i.fa{ + margin: 0; +} + +#themeEditor{ + width: 100%; + max-width: 900px; +} + +#themeEditor #row-instructions p{ + line-height: 1.3; + margin: 0 auto; + padding: 10.5px 0; +} + +#themeEditor #row-instructions p:first-of-type{ + padding-top: 0; +} + +#themeEditor #row-instructions i.fa{ + margin: 0 2px; +} + +#themeEditor #row-instructions #toggle-more-instructions{ + cursor: pointer; +} + +#themeEditor #row-instructions #toggle-more-instructions:hover{ + text-decoration: underline; +} + +#themeEditor #row-instructions #more-instructions{ + display: none; +} + +#themeEditor #editor-tools{ + display: -webkit-box; + display: -ms-flexbox; + display: flex; + width: 100%; +} + +#themeEditor #editor-tools #options-container, +#themeEditor #editor-tools #preview-container{ + width: 50%; +} + +#themeEditor #preview.sticky{ + top: 6px; + position: fixed; +} + +#themeEditor #editor-tools #options-container{ + padding-right: 20px; +} + +#themeEditor .section{ + padding: 13px 0; + border-top: 1px solid #e0e0e0; +} + +#themeEditor .section:first-child{ + padding-top: 0; + border-top: none; +} + +#themeEditor .section-header{ + cursor: pointer; +} + +#themeEditor .section-toggle{ + float: right; + width: 15px; + height: 15px; + margin: 2px 0 0 1px; + background-color: transparent; + border: 0; + padding-top: 3px; +} + +#themeEditor .section-toggle.toggled{ + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +#themeEditor .section-options{ + overflow: hidden; +} + +#themeEditor .options-attribute{ + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin-bottom: 15px; +} + +#themeEditor .options-attribute:first-of-type{ + margin-top: 10px; +} + +#themeEditor .options-attribute:last-of-type{ + margin-bottom: 10px; +} + +#themeEditor .section:last-of-type .options-attribute:last-of-type{ + margin-bottom: 0; +} + +#themeEditor .options-attribute-child{ + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + margin: 0 10px; +} + +#themeEditor .options-attribute-child:first-child{ + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + margin: 0 10px 0 0; +} + +#themeEditor .options-attribute-child:last-child{ + margin: 0 0 0 10px; +} + +#themeEditor .options-attribute label{ + text-align: right; + margin-right: 0; + min-width: 130px; +} + +#themeEditor .options-attribute .attribute-help{ + margin-left: 0px; + border: 0px; + border-radius: 50px; + background-color: white; + padding: 3px 0px 7px 1px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + font-size: 10px; + cursor: help; +} + +#themeEditor .options-attribute .options-attribute-input{ + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + width: 20%; + padding: 6px; + color: #666; + font-size: 16px; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-box-shadow: 0 0 0 0; + box-shadow: 0 0 0 0; +} + +#themeEditor .options-attribute .options-attribute-input[type="color"]{ + padding: 0px; + border: 0px; + margin: 10px 6px; +} + +#themeEditor .options-attribute .options-attribute-input[disabled=""]{ + cursor: not-allowed; +} + +#themeEditor .options-note{ + margin-top: -7px; +} + +#themeEditor .options-note:empty{ + display: none; +} + +#themeEditor #imgUploader-label{ + min-width: 0; +} + +#themeEditor #imgUploader-label.button:hover{ + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +#themeEditor #imgUploader-label.button:active{ + -webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.125); + box-shadow: inset 0 3px 5px rgba(0,0,0,.125); +} + +#themeEditor #imgUploader-label i{ + margin-right: 0; +} + +#themeEditor #imgUploader{ + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; +} + +#themeEditor #themeButtons button:first-child{ + margin-left: 0; +} + +#themeEditor #themeButtons i.fa{ + margin: 0; +} + +@media (max-width: 760px) { + #themeEditor #editor-tools{ + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + } + + #themeEditor #editor-tools #options-container, + #themeEditor #editor-tools #preview-container{ + width: 100%; + } + + #themeEditor #preview.sticky{ + top: 0; + position: initial; + } +} diff --git a/editor/index.html b/editor/index.html index 5106c462..c54c672e 100644 --- a/editor/index.html +++ b/editor/index.html @@ -28,6 +28,8 @@

Audiogram

+ +