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));
});
}
});