diff --git a/src/media/css/forms.styl b/src/media/css/forms.styl index 2c71b889..a81813b9 100644 --- a/src/media/css/forms.styl +++ b/src/media/css/forms.styl @@ -263,11 +263,11 @@ label { } ::-webkit-input-placeholder { - color: $gray; + color: $colorGrayLightest; } :-moz-placeholder { - color: $gray; + color: $colorGrayLightest; text-overflow: ellipsis; // Firefox 18 and below } @@ -336,7 +336,7 @@ form { margin-top: 15px; } textarea[name=description].focused + .toggle-preview-container { - display: initial; + display: inherit; } .toggle-preview-container { background-color: rgba(0,255,0,0.25); @@ -386,7 +386,8 @@ form { color: #999; } .grid > .grid-flex-grow-two { - padding-right: 50px; + margin-right: 50px; + position: relative; } input.large ~ .req:after { bottom: 10px; @@ -423,7 +424,9 @@ form { } .l { - font-size: 10px; + color: $colorGray; + font-size: 12px; + margin-bottom: 5px; } [placeholder] { ~ .l { diff --git a/src/media/css/friends.styl b/src/media/css/friends.styl index a2bd8f51..45463007 100644 --- a/src/media/css/friends.styl +++ b/src/media/css/friends.styl @@ -1,12 +1,5 @@ @import 'lib'; -[data-page-type~="leaf"] .page { - h1 { - color: $flamin-hot-cheetos-orange; - line-height: 1; - } -} - [data-page-type~="profile"] .page { h1 .avatar { margin-right: 10px; diff --git a/src/media/css/media.styl b/src/media/css/media.styl index 9594293d..91ebbb93 100644 --- a/src/media/css/media.styl +++ b/src/media/css/media.styl @@ -1,44 +1,239 @@ @import 'lib'; -.media-preview { - cursor: pointer; - display: block; +.media { + .form-help { + margin-bottom: 5px; + margin-right: 50px; + } + .req:after { + bottom: auto; + top: 5px; + } + .media-left, + .media-right { + padding-right: 80px; + } + .media-size { + border: 1px solid $colorGrayLightest; + border-radius: 2px; + color: $colorGrayLighter; + display: inline-block; + font-size: 10px; + margin-top: 15px; + padding: 1px 3px; + .inner { + color: $colorGrayLightest; + } + } + input { + font-size: 12px; + } + input[type=file] { + display: none; + } + .media-item { + margin-bottom: 10px; + &.media-item-template { + display: none; + &:first-child { + display: block; + } + &:not(:first-child) + .add-button { + display: block; + } + } + &.add-item { + display: inherit; + } + } + .add-button { + display: none; + i { + margin-right: 3px; + } + } } -.media-preview { - display: none; - margin-top: 5px; - max-height: 150px; - max-width: 200px; +.media-item { + .media-preview-container { + background-color: $mediaPreviewBackgroundColor; + border: $mediaPreviewBorder; + border-radius: 2px; + cursor: pointer; + display: block; + margin: 5px 0; + max-width: 100%; + position: relative; + + img, iframe { + height: 100%; + position: relative; + width: 100%; + z-index: 1; + } + .media-preview { + display: none; + } + &:before { + background: url('../img/game-submission/upload-blue.png') 50% 0 no-repeat; + content: ""; + display: block; + position: absolute; + width: 100%; + } + &:after { + color: $colorLightBlue; + content: attr(data-message); + display: block; + position: absolute; + text-align: center; + } + } + &.processed { + .media-preview-container { + background: #fff url('../img/game-submission/crosshatch.png'); + border: 1px solid $colorGray; + .media-preview { + display: inherit; + } + .media-delete { + background-color: rgba(217,21,24,0.75); + border: 1px solid #d91518; + border-radius: 5px; + bottom: 5px; + color: #fff; + cursor: pointer; + display: none; + height: 30px; + position: absolute; + width: 30px; + right: 5px; + text-align: center; + z-index: 2; + &:before { + content: "\00d7"; /* Cross symbol */ + font-size: 24px; + font-weight: 600; + line-height: 30px; + } + } + &:hover { + .media-delete { + display: inherit; + } + } + &:before { + background-image: none; + } + &:after { + content: ""; + } + } + .media-input { + display: none; + } + } } -.icons .media-preview { - max-height: 128px; - max-width: 128px; -} +.icons { + .icons-dimensions { + border: 1px solid $colorGrayLighter; + border-radius: 2px; + color: $colorGrayLighter; + display: inline-block; + font-size: 10px + padding: 1px 3px; + } -.videos .media-preview { - max-height: 150px; - max-width: 200px; + .media-preview-container { + height: 128px; + width: 128px; + &:before { + background-size: 50px; + height: 50px; + top: 25px; + } + &:after { + bottom: 17px; + font-size: 12px; + margin-left: 20%; + width: 60%; + } + &.icons-64 { + height: 64px; + width: 64px; + &:before { + background-size: 25px; + top: 20px; + } + &:after { + content: ""; + } + } + } } -.media-size { - color: $colorGrayLight; - font-size: 10px; - margin-top: 15px; +.screenshots { + .media-preview-container { + height: 210px; + &:before { + background-size: 80px; + height: 80px; + top: 50px; + } + &:after { + bottom: 35px; + margin-left: 32%; + width: 36%; + } + &.screenshots-16-9 { + height: 157px; + &:before { + top: 20px; + } + &:after { + bottom: 15px; + } + } + } } -input[type=url].media { - margin-bottom: 5px; +.icons, +.screenshots { + .media-preview-container.dragenter { + background-color: #efefef; + border-color: $colorGray; + &:before { + background-image: url('../img/game-submission/upload.png'); + } + &:after { + color: $colorGray; + } + } } -.media { - .form-help { - margin-bottom: 5px; - margin-right: 50px; - } - .req:after { - bottom: auto; - top: 5px; +.videos { + .media-preview-container { + cursor: default; + height: 210px; + &:before { + background: url('../img/game-submission/preview-blue.png') 53% 0 no-repeat; + background-size: 80px; + height: 80px; + top: 60px; + } + &:after { + bottom: 55px; + content: attr(data-message); + width: 100%; + } + .media-iframe { + height: 100%; + width: 100%; + } } } + +input[type=url].media { + margin-bottom: 5px; +} diff --git a/src/media/css/modal.styl b/src/media/css/modal.styl index 1e0a638d..4a65eb6f 100644 --- a/src/media/css/modal.styl +++ b/src/media/css/modal.styl @@ -78,7 +78,7 @@ body.overlayed { } .form-close { color: $colorGrayDark; - display: initial; + display: inherit; font-size: 24px; position: absolute; right: 10px; diff --git a/src/media/css/site.styl b/src/media/css/site.styl index cc93aacf..efdd3518 100644 --- a/src/media/css/site.styl +++ b/src/media/css/site.styl @@ -61,7 +61,7 @@ body { } main { - margin: 0 auto; + margin: 0 auto 40px; width: 1200px; } @@ -101,7 +101,7 @@ main { margin-bottom: 6px; padding: 40px 35px; h1 { - color: $colorGray; + color: $colorBlack; } h2 { color: $colorGrayDark; diff --git a/src/media/css/variables.styl b/src/media/css/variables.styl index a1565f5a..722eecfa 100644 --- a/src/media/css/variables.styl +++ b/src/media/css/variables.styl @@ -54,10 +54,13 @@ $colorGrayDark = #555; $colorGray = #777; $colorGrayLight = #999; $colorGrayUltraLight = #bbb; +$colorGrayLighter = #9d9d9d; +$colorGrayLightest = #d4d4d4; $colorWhite = #fff; $colorRed = #da3e5a; $colorGreen = #18a011; -$colorLightGreen = #7CCD2F; +$colorLightGreen = #7ccd2f; +$colorLightBlue = #98bada; // ButtonColor $buttonColorTeal = #40dfc2; @@ -80,6 +83,7 @@ $mainBackgroundColor = #f7f7f7; $mainFooterColor = rgba(45,57,74,0.9); $mainFooterHoverColor = rgba(255,255,255,.1); $cloakBackgroundColor = rgba(0,0,0,.9); +$mediaPreviewBackgroundColor = #eff; // Legacy colors $flamin-hot-cheetos-orange = #ff9500; @@ -100,6 +104,7 @@ $light-gray = #ccc $cellBorder = 1px solid #eee; $cellBorderDark = 1px solid #ddd; $fieldsetBorder = 1px solid #ddd; +$mediaPreviewBorder = 1px solid #3892e3; // Modal $modalBackgroundColor = #d8d8d8; diff --git a/src/media/img/game-submission/crosshatch.png b/src/media/img/game-submission/crosshatch.png new file mode 100644 index 00000000..7ab053bb Binary files /dev/null and b/src/media/img/game-submission/crosshatch.png differ diff --git a/src/media/img/game-submission/preview-blue.png b/src/media/img/game-submission/preview-blue.png new file mode 100644 index 00000000..f738c841 Binary files /dev/null and b/src/media/img/game-submission/preview-blue.png differ diff --git a/src/media/img/game-submission/preview.png b/src/media/img/game-submission/preview.png new file mode 100644 index 00000000..c002970e Binary files /dev/null and b/src/media/img/game-submission/preview.png differ diff --git a/src/media/img/game-submission/upload-blue.png b/src/media/img/game-submission/upload-blue.png new file mode 100644 index 00000000..68e37998 Binary files /dev/null and b/src/media/img/game-submission/upload-blue.png differ diff --git a/src/media/img/game-submission/upload.png b/src/media/img/game-submission/upload.png new file mode 100644 index 00000000..54a34a86 Binary files /dev/null and b/src/media/img/game-submission/upload.png differ diff --git a/src/media/js/media-input.js b/src/media/js/media-input.js index 8b664bd5..06d0d444 100644 --- a/src/media/js/media-input.js +++ b/src/media/js/media-input.js @@ -1,6 +1,6 @@ define('media-input', - ['jquery', 'l10n', 'promise', 'z'], - function($, l10n, promise, z) { + ['jquery', 'l10n', 'promise', 'video-utils', 'z'], + function($, l10n, promise, video_utils, z) { var gettext = l10n.gettext; var Promise = promise; @@ -10,10 +10,6 @@ define('media-input', apiKey: '18dd09ec2d40f716', apiVersion: 3, theme: 'dark', - // TODO: We should probably enforce a minimum size. - // TODO: `maxSize` will have to be different for screenshots. - maxSize: 128, - // TODO: `cropPresets` will have to be different for screenshots. cropPresets: [ ['Square', '1:1'] ], @@ -21,12 +17,30 @@ define('media-input', var img = document.getElementById(imageID); // Replace the contents of the preview box + dimensions. - preview(img.dataset.type, newURL); + var $img = $(img); + preview($img.closest('.media-preview-container'), newURL); + // At any one time, there should only be one image with the 'aviary-image' id + $img.removeAttr('id'); + + var $mediaItem = $img.closest('.media-item'); + + if ($mediaItem.data('type') !== 'icons') { + var $clone = $mediaItem.clone(); + $mediaItem.before($clone); + cleanUpTemplate($mediaItem); + $mediaItem = $clone; + } // This is the URL that gets POST'd to the API. - // TODO: As there will be multiple fields for screenshots, - // we need to update the correct hidden `input` field. - $(img).siblings('.media-processed-url').val(newURL); + var $processedInput = $mediaItem.children('.media-input-processed-url'); + $processedInput.val(newURL); + + img.onload = function() { + $processedInput.attr('data-height', img.naturalHeight); + $processedInput.attr('data-width', img.naturalWidth); + }; + + $mediaItem.removeClass('add-item').addClass('processed'); // Close the editor. featherEditor.close(); @@ -36,105 +50,199 @@ define('media-input', } }); - function launchEditor(id, src) { + function cleanUpTemplate($mediaItem) { + // To reset all input fields and img src in a media-item-template + $mediaItem.removeClass('add-item').addClass('media-item-template'); + $mediaItem.children('.media-preview').removeAttr('src'); + $mediaItem.children('.media-input').val(''); + } + + function launchEditor(id, src, type) { + + switch (type) { + case 'icons': + var message = gettext('Crop your icon to a square'); + var ratio = '1:1'; + var size = 128; + break; + case 'screenshots-4-3': + var message = gettext('Crop your icon to a 4:3 rectangle'); + var ratio = '4:3'; + var size = 1024; + break; + case 'screenshots-16-9': + var message = gettext('Crop your icon to a 16:9 rectangle'); + var ratio = '16:9'; + var size = 1280; + break; + } + featherEditor.launch({ - forceCropPreset: [gettext('Crop your icon to a square'), '1:1'], + forceCropPreset: [message, ratio], forceCropMessage: ' ', + maxSize: size, image: id, url: src }); return false; } - function preview(type, src, launch) { - var $filePreview = $('.media-preview[data-type="' + type + '"]'); - $filePreview.show(); - - var img = $filePreview[0]; + function preview($obj, src, launch) { + var img = $obj.children('.media-preview')[0]; img.src = src; - img.onload = function() { - $filePreview.siblings('.media-size').html( - this.width + 'px × ' + this.height + 'px').show(); - }; - + if (launch) { + var type = $obj.data('type'); + img.id = 'aviary-img'; // If we've exited the editor, for example, // we don't we want to relaunch the editor. - launchEditor(img.id, img.src); + launchEditor(img.id, img.src, type); } } - function createInput($section) { - $section.append($('', { - 'class': 'media', - 'data-type': $section.data('type'), - 'type': 'url', - 'placeholder': $section.data('placeholder'), - 'pattern': 'https?://.*' - })); - } - function getFileDataURI(input, callback) { return new Promise(function(resolve, reject) { - if (input.files && input.files[0]) { - // This is so we can get the data URI of the image uploaded. - var reader = new FileReader(); - reader.onload = function (e) { - resolve(e.target.result); - }; - reader.onerror = function (err) { - reject(err.getMessage()); - }; - reader.readAsDataURL(input.files[0]); + var files = input.files; + if (files && files[0]) { + // Loop through the files and render image files. + for (var i = 0, f; f = files[i]; i++) { + // This is so we can get the data URI of the image uploaded. + var reader = new FileReader(); + reader.onload = function (e) { + resolve(e.target.result); + // preview(input.dataset.type, e.target.result, false); + }; + reader.onerror = function (err) { + reject(err.getMessage()); + }; + reader.readAsDataURL(input.files[i]); + } } }); } + function loadVideo($input, type) { + if ($input.val()) { + var $mediaPreview = $input.siblings('.media-preview-container'); + $mediaPreview.find('.media-iframe') + .html(video_utils.createVideoFromId($input.val(), type)); + $mediaPreview.parent('.media-item').addClass('processed'); + } + } + z.page.on('loaded', function() { - $('.fallback').each(function() { + $('.videos input[type=hidden]').each(function() { + // Load iframes for existing videos var $this = $(this); - createInput($this); - }); - }).on('input', 'input[type=url].media', function(e) { - var $input = $(e.target); - var $allInputs = $input.parent().children('input[type=url]'); - var $emptyInputs = $allInputs.filter(function() { - return !$(this).val(); + loadVideo($this, $this.data('video-type')); }); - // TODO: Have a better check for the icons input field - if ($input.val() && $emptyInputs.length === 0 && $input.data('type') !== "icons") { - createInput($input.parent()); - } else { - // So that at any point in time, there will be exactly - // ONE empty input field for user to enter more URLs. - $emptyInputs.slice(1).remove(); - } - }).on('keypress', 'input[type=url].media', function(e) { + + }).on('keypress', 'input[type=url].media-input', function(e) { var $this = $(this); - if (this.checkValidity() && e.keyCode === 13) { - // After it's been blurred, the editor will get launched. - return this.blur(); - } - }).on('blur', 'input[type=url].media', function(e) { + // After it's been blurred, the editor will get launched. + return this.blur(); + } + + }).on('blur', 'input[type=url].media-input', function(e) { var $this = $(this); + if ($this.data('type') === 'videos') { + return; + } // Launch editor only when input is blurred. - preview($this.data('type'), $this.val(), true); - - }).on('click', '.media-preview', function(e) { - // Clicking on the image preview should open the image - // for re-processing. - launchEditor('icon-preview', - $(this).siblings('.media-processed-url').val()); - }).on('blur change', 'input[type=file].media', function(e) { - // TODO: Allow images to be dragged and dropped to the file input. + if ($this.val() !== 'http://') { + // Launch on non-empty inputs. + preview($this.siblings('.media-preview-container'), $this.val(), true); + } + + }).on('click', '.media-preview-container', function(e) { + // Open the file upload dialog when user clicks on dropzone. + // Only happens if there's no image inside the preview container. + var $this = $(this); + if ($this.closest('.media-item').hasClass('processed')) { + var src = $this.siblings('.media-input-processed-url').val(); + var $img = $this.children('.media-preview'); + $img[0].id = 'aviary-img'; + launchEditor($img[0].id, src, $this.data('type')); + } else { + $this.children('input[type=file]')[0].click(); + } + + }).on('drop', '.media-preview-container', function(e) { + e.preventDefault(); + var $this = $(this); + $this.toggleClass('dragenter', false); + if (!$this.closest('.media-item').hasClass('processed')) { + e = e.originalEvent; + getFileDataURI(e.dataTransfer).then(function(data) { + preview($this, data, true); + }).catch(function(err) { + return console.error(err); + }); + } + + }).on('blur change', 'input[type=file].media-input', function(e) { var input = this; input.blur(); - + var $this = $(this); getFileDataURI(input).then(function (data) { - preview(input.dataset.type, data, true); + preview($this.closest('.media-preview-container'), data, true); }).catch(function (err) { return console.error(err); }); + + }).on('click', '.media-delete', function(e) { + e.stopPropagation(); + var $this = $(this); + $this.siblings('input[type=file].media-input').val(''); + var $mediaList = $this.closest('.media-list'); + if ($mediaList.children('.media-item').length === 1) { + var $mediaPreviewContainer = $this.parent(); + $mediaPreviewContainer.closest('.media-item').removeClass('processed'); + $mediaPreviewContainer.siblings('.media-input').val(''); + $mediaPreviewContainer.siblings('.media-input-processed-url').val(''); + $mediaPreviewContainer.children('.media-iframe').html(''); + $mediaPreviewContainer.siblings('input[type=hidden]'). + val('').attr('data-video-type', '').attr('data-video-thumbnail', ''); + } else { + $this.closest('.media-item').remove(); + } + + }).on('blur', '.videos input[type=url]', function() { + // Videos section + var $this = $(this); + var $hidden = $this.siblings('input[type=hidden]'); + if ($this.val() && $this.val() !== 'http://') { + var videoObject = video_utils.parseVideo($this.val()); + $hidden.val(videoObject.id).attr('data-video-type', videoObject.type); + video_utils.getVideoThumbnailFromId(videoObject.id, videoObject.type, function(thumbnail) { + $hidden.attr('data-video-thumbnail', thumbnail); + }); + loadVideo($hidden, videoObject.type); + } else { + // Clear the loaded iframe. + $this.siblings('.media-preview-container').children('.media-iframe').html(''); + $hidden.val('').attr('data-video-type', '').attr('data-video-thumbnail', ''); + } + + }).on('dragover dragenter', function(e) { + e.preventDefault(); + var $target = $(e.target); + if ($target.hasClass('media-preview-container')) { + e.originalEvent.dataTransfer.dropEffect = 'copy'; + $target.toggleClass('dragenter', true); + } else { + e.originalEvent.dataTransfer.dropEffect = 'none'; + } + return false; + + }).on('dragleave dragend', '.media-preview-container', function(e) { + $(this).toggleClass('dragenter', false); + return false; + + }).on('click', '.add-button', function(e) { + $(this).siblings('.media-item-template').removeClass('media-item-template').addClass('add-item'); + }); + }); diff --git a/src/media/js/settings.js b/src/media/js/settings.js index e151ae55..c124624c 100644 --- a/src/media/js/settings.js +++ b/src/media/js/settings.js @@ -36,6 +36,17 @@ define('settings', ['l10n', 'settings_local', 'underscore'], function(l10n, sett 'multiplayer': gettext('Multiplayer') }, + video_utils_urls: { + 'youtube': { + 'iframe': '//www.youtube.com/embed/', + 'thumbnail': '//img.youtube.com/vi//hqdefault.jpg' + }, + 'vimeo': { + 'iframe': '//player.vimeo.com/video/', + 'thumbnail': 'http://vimeo.com/api/v2/video/.json' + } + }, + // Error template paths. Used by builder.js. fragment_error_template: 'errors/fragment.html', pagination_error_template: 'errors/pagination.html', diff --git a/src/media/js/video-utils.js b/src/media/js/video-utils.js index 2af23f1d..07e19267 100644 --- a/src/media/js/video-utils.js +++ b/src/media/js/video-utils.js @@ -1,6 +1,6 @@ define('video-utils', - ['l10n', 'jquery'], - function(l10n, $) { + ['l10n', 'jquery', 'requests', 'settings'], + function(l10n, $, requests, settings) { function parseVideo(url) { // - Supported YouTube URL formats: @@ -31,26 +31,29 @@ define('video-utils', function createVideoFromUrl(url, width, height) { var videoObj = parseVideo(url); return createVideoFromId(videoObj.id, videoObj.type, width, height); - } function createVideoFromId(id, type, width, height) { var $iframe = $('