diff --git a/customize.dist/ckeditor-contents.css b/customize.dist/ckeditor-contents.css index 000162c00..a7939839d 100644 --- a/customize.dist/ckeditor-contents.css +++ b/customize.dist/ckeditor-contents.css @@ -213,3 +213,33 @@ media-tag * { width: 100%; height: 100%; } +media-tag button.btn { + background-color: #fff; + box-sizing: border-box; + outline: 0; + display: inline-flex; + align-items: center; + padding: 0 6px; + min-height: 36px; + line-height: 22px; + white-space: nowrap; + text-align: center; + text-transform: uppercase; + font-size: 14px; + text-decoration: none; + cursor: pointer; + border-radius: 0; + transition: none; + color: #3F4141; + border: 1px solid #3F4141; +} +media-tag button.btn:hover, media-tag button.btn:active, media-tag button.btn:focus { + background-color: #ccc; +} +media-tag button b { + margin-left: 5px; +} +media-tag button .fa { + display: inline; + margin-right: 5px; +} diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 737509130..65eedf263 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -128,6 +128,14 @@ font-weight: bold; } + &.btn-default { + border-color: @cryptpad_text_col; + color: @cryptpad_text_col; + &:hover, &:active, &:focus { + background-color: #ccc; + } + } + &.danger, &.btn-danger { background-color: @colortheme_alertify-red; border-color: @colortheme_alertify-red-border; diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 23eb056b0..d7fe13f43 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -94,6 +94,15 @@ height: 80vh; max-height: 90vh; } + button.btn-default { + display: inline-flex; + .fa { + margin-right: 5px; + } + b { + margin-left: 5px; + } + } } media-tag:empty { width: 100px; diff --git a/server.js b/server.js index eb2787475..3869af509 100644 --- a/server.js +++ b/server.js @@ -136,6 +136,20 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) { }); }()); +app.use('/blob', function (req, res, next) { + if (req.method === 'HEAD') { + Express.static(Path.join(__dirname, (config.blobPath || './blob')), { + setHeaders: function (res, path, stat) { + res.set('Access-Control-Allow-Origin', '*'); + res.set('Access-Control-Allow-Headers', 'Content-Length'); + res.set('Access-Control-Expose-Headers', 'Content-Length'); + } + })(req, res, next); + return; + } + next(); +}); + app.use(function (req, res, next) { if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) { res.setHeader('Access-Control-Allow-Origin', '*'); diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 734551983..fd3fcafb3 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -668,7 +668,7 @@ define([ } return; } - MediaTag(el); + var mediaObject = MediaTag(el); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { @@ -676,7 +676,7 @@ define([ .map(function (el) { return el.outerHTML; }) .join(''); mediaMap[mutation.target.getAttribute('src')] = list_values; - observer.disconnect(); + if (mediaObject.complete) { observer.disconnect(); } } }); $mt.off('click dblclick preview'); diff --git a/www/common/media-tag.js b/www/common/media-tag.js index 1ba9a1f53..4318ca8f6 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -63,7 +63,8 @@ ], pdf: {}, download: { - text: "Download" + text: "Save", + textDl: "Load attachment" }, Plugins: { /** @@ -114,8 +115,8 @@ }, download: function (metadata, url, content, cfg, cb) { var btn = document.createElement('button'); - btn.setAttribute('class', 'btn btn-success'); - btn.innerHTML = cfg.download.text + '
' + + btn.setAttribute('class', 'btn btn-default'); + btn.innerHTML = '' + cfg.download.text + '
' + (metadata.name ? '' + fixHTML(metadata.name) + '' : ''); btn.addEventListener('click', function () { saveFile(content, url, metadata.name); @@ -125,6 +126,92 @@ } }; + var makeProgressBar = function (cfg, mediaObject) { + // XXX CSP: we'll need to add style in cryptpad's less + var style = (function(){/* +.mediatag-progress-container { + position: relative; + border: 1px solid #0087FF; + background: white; + height: 25px; + display: inline-flex; + width: 200px; + align-items: center; + justify-content: center; + box-sizing: border-box; + vertical-align: top; +} +.mediatag-progress-bar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + background: #0087FF; + width: 0%; +} +.mediatag-progress-text { + height: 25px; + margin-left: 5px; + line-height: 25px; + vertical-align: top; + width: auto; + display: inline-block; + color: #3F4141; + font-weight: bold; +} +*/}).toString().slice(14, -3); + var container = document.createElement('div'); + container.classList.add('mediatag-progress-container'); + var bar = document.createElement('div'); + bar.classList.add('mediatag-progress-bar'); + container.appendChild(bar); + + var text = document.createElement('span'); + text.classList.add('mediatag-progress-text'); + text.innerText = '0%'; + + mediaObject.on('progress', function (obj) { + var percent = obj.progress; + text.innerText = (Math.round(percent*10))/10+'%'; + bar.setAttribute('style', 'width:'+percent+'%;'); + }); + + mediaObject.tag.innerHTML = ''; + mediaObject.tag.appendChild(container); + mediaObject.tag.appendChild(text); + }; + var makeDownloadButton = function (cfg, mediaObject, size, cb) { + var btn = document.createElement('button'); + btn.setAttribute('class', 'btn btn-default'); + btn.innerHTML = '' + + cfg.download.textDl + ' (' + size + 'MB)'; + btn.addEventListener('click', function () { + makeProgressBar(cfg, mediaObject); + cb(); + }); + mediaObject.tag.innerHTML = ''; + mediaObject.tag.appendChild(btn); + }; + + var getFileSize = function (src, _cb) { + var cb = function (e, res) { + _cb(e, res); + cb = function () {}; + }; + // XXX Cache + var xhr = new XMLHttpRequest(); + xhr.open("HEAD", src); + xhr.onerror = function () { return void cb("XHR_ERROR"); }; + xhr.onreadystatechange = function() { + if (this.readyState === this.DONE) { + cb(null, Number(xhr.getResponseHeader("Content-Length"))); + } + }; + xhr.onload = function () { + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + }; + xhr.send(); + }; // Download a blob from href var download = function (src, _cb, progressCb) { @@ -433,6 +520,7 @@ // End media-tag rendering: display the tag and emit the event var end = function (decrypted) { + mediaObject.complete = true; process(mediaObject, decrypted, cfg, function (err) { if (err) { return void emit('error', err); } mediaObject._blob = decrypted; @@ -441,36 +529,54 @@ }; // If we have the blob in our cache, don't download & decrypt it again, just display + // XXX Store in the cache the pending mediaobject: make sure we don't download and decrypt twice the same element at the same time if (cache[uid]) { end(cache[uid]); return mediaObject; } - // Download the encrypted blob - download(src, function (err, u8Encrypted) { + var dl = function () { + // Download the encrypted blob + download(src, function (err, u8Encrypted) { + if (err) { + if (err === "XHR_ERROR 404") { + mediaObject.tag.innerHTML = ''; + } + return void emit('error', err); + } + // Decrypt the blob + decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { + if (errDecryption) { + return void emit('error', errDecryption); + } + // Cache and display the decrypted blob + cache[uid] = u8Decrypted; + end(u8Decrypted); + }, function (progress) { + emit('progress', { + progress: 50+0.5*progress + }); + }); + }, function (progress) { + emit('progress', { + progress: 0.5*progress + }); + }); + }; + + if (cfg.force) { dl(); return mediaObject; } + + var maxSize = 5 * 1024 * 1024; + getFileSize(src, function (err, size) { if (err) { if (err === "XHR_ERROR 404") { mediaObject.tag.innerHTML = ''; } return void emit('error', err); } - // Decrypt the blob - decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { - if (errDecryption) { - return void emit('error', errDecryption); - } - // Cache and display the decrypted blob - cache[uid] = u8Decrypted; - end(u8Decrypted); - }, function (progress) { - emit('progress', { - progress: 50+0.5*progress - }); - }); - }, function (progress) { - emit('progress', { - progress: 0.5*progress - }); + if (!size || size < maxSize) { return void dl(); } + var sizeMb = Math.round(10 * size / 1024 / 1024) / 10; + makeDownloadButton(cfg, mediaObject, sizeMb, dl); }); return mediaObject; diff --git a/www/file/inner.js b/www/file/inner.js index 8b3f12682..c29657672 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -168,7 +168,9 @@ define([ var rightsideDisplayed = false; - MediaTag($mt[0]).on('complete', function (decrypted) { + MediaTag($mt[0], { + force: true // Download starts automatically + }).on('complete', function (decrypted) { $dlview.show(); $dlform.hide(); var $dlButton = $dlview.find('media-tag button'); diff --git a/www/pad/inner.js b/www/pad/inner.js index 235b4fafa..5139197e0 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -462,7 +462,7 @@ define([ setTimeout(function() { // Just in case var tags = dom.querySelectorAll('media-tag:empty'); Array.prototype.slice.call(tags).forEach(function(el) { - MediaTag(el); + var mediaObject = MediaTag(el); $(el).on('keydown', function(e) { if ([8, 46].indexOf(e.which) !== -1) { $(el).remove(); @@ -474,6 +474,7 @@ define([ if (mutation.type === 'childList') { var list_values = [].slice.call(el.children); mediaTagMap[el.getAttribute('src')] = list_values; + if (mediaObject.complete) { observer.disconnect(); } } }); }); @@ -492,7 +493,7 @@ define([ var src = tag.getAttribute('src'); if (mediaTagMap[src]) { mediaTagMap[src].forEach(function(n) { - tag.appendChild(n.cloneNode()); + tag.appendChild(n.cloneNode(true)); }); } });