diff --git a/customize.dist/loading.js b/customize.dist/loading.js index e8f8a9453..765ec56f9 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -169,6 +169,28 @@ define([], function () { height: 100%; background: #5cb85c; } + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(1800deg); + } +} + +.cp-spinner { + display: inline-block; + box-sizing: border-box; + width: 80px; + height: 80px; + border: 11px solid lightgrey; + border-radius: 50%; + border-top-color: transparent; + animation: spin infinite 3s; + animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); +} + */}).toString().slice(14, -3); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var elem = document.createElement('div'); @@ -182,7 +204,7 @@ define([], function () { '', '
', '
', - '', + '', '
', '

', '
' diff --git a/customize.dist/src/less2/include/contextmenu.less b/customize.dist/src/less2/include/contextmenu.less index 8b1ef8c21..e4d1318ec 100644 --- a/customize.dist/src/less2/include/contextmenu.less +++ b/customize.dist/src/less2/include/contextmenu.less @@ -22,13 +22,16 @@ } } .dropdown-toggle { + transform: rotate(270deg); margin-left: 1rem; + float: right; } .dropdown-menu { top: -0.7rem; left: 100%; &.left { - left: -10rem; + left: 0%; + transform: translate(-100%); } } } diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 34d44daf5..a6e5d5128 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -230,9 +230,15 @@ define([ if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); } }; + var addThumbnail = function (err, thumb, $span, cb) { + var u8 = Nacl.util.decodeBase64(thumb.split(',')[1]); + var blob = new Blob([u8], { + type: 'image/png' + }); + var url = URL.createObjectURL(blob); var img = new Image(); - img.src = thumb.slice(0,5) === 'data:' ? thumb : 'data:image/png;base64,'+thumb; + img.src = url; $span.find('.cp-icon').hide(); $span.prepend(img); cb($(img)); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 207d00336..9aa313779 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -573,9 +573,9 @@ define([ onFriendShare.reg(saveValue); var getLinkValue = function (initValue) { var val = initValue || {}; - var edit = initValue ? val.edit : Util.isChecked($(link).find('#cp-share-editable-true')); - var embed = initValue ? val.embed : Util.isChecked($(link).find('#cp-share-embed')); - var present = initValue ? val.present : Util.isChecked($(link).find('#cp-share-present')); + var edit = val.edit !== undefined ? val.edit : Util.isChecked($(link).find('#cp-share-editable-true')); + var embed = val.embed !== undefined ? val.embed : Util.isChecked($(link).find('#cp-share-embed')); + var present = val.present !== undefined ? val.present : Util.isChecked($(link).find('#cp-share-present')); var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; var href = origin + pathname + '#' + hash; @@ -2104,6 +2104,9 @@ define([ }; UIElements.createNewPadModal = function (common) { + // if in drive, show new pad modal instead + if ($("body.cp-app-drive").length !== 0) { return void $(".cp-app-drive-element-row.cp-app-drive-new-ghost").click(); } + var $modal = UIElements.createModal({ id: 'cp-app-toolbar-creation-dialog', $body: $('body') @@ -2766,8 +2769,12 @@ define([ UIElements.displayCrowdfunding(common); modal.delete(); }); + var waitingForStoringCb = false; $(store).click(function () { + if (waitingForStoringCb) { return; } + waitingForStoringCb = true; common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) { + waitingForStoringCb = false; var error = err || (obj && obj.error); if (error) { if (error === 'E_OVER_LIMIT') { @@ -2854,11 +2861,27 @@ define([ 'aria-labelledBy': 'dropdownMenu', 'style': 'display:block;position:static;margin-bottom:5px;' }, [ - h('li', h('a.dropdown-item', { + h('li', h('a.cp-app-code-context-saveindrive.dropdown-item', { + 'tabindex': '-1', + 'data-icon': "fa-cloud-upload", + }, Messages.pad_mediatagImport)), + h('li', h('a.cp-app-code-context-download.dropdown-item', { 'tabindex': '-1', - }, Messages.pad_mediatagImport)) + 'data-icon': "fa-download", + }, Messages.download_mt_button)), ]) ]); + // create the icon for each contextmenu option + $(menu).find("li a.dropdown-item").each(function (i, el) { + var $icon = $(""); + if ($(el).attr('data-icon')) { + var font = $(el).attr('data-icon').indexOf('cptools') === 0 ? 'cptools' : 'fa'; + $icon.addClass(font).addClass($(el).attr('data-icon')); + } else { + $icon.text($(el).text()); + } + $(el).prepend($icon); + }); var m = createContextMenu(menu); mediatagContextMenu = m; @@ -2868,7 +2891,13 @@ define([ e.stopPropagation(); m.hide(); var $mt = $menu.data('mediatag'); - common.importMediaTag($mt); + if ($(this).hasClass("cp-app-code-context-saveindrive")) { + common.importMediaTag($mt); + } + else if ($(this).hasClass("cp-app-code-context-download")) { + var media = $mt[0]._mediaObject; + window.saveAs(media._blob.content, media.name); + } }); return m; diff --git a/www/common/common-util.js b/www/common/common-util.js index 9fd2305c3..03c9e321b 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -319,6 +319,12 @@ define([], function () { return window.innerHeight < 800 || window.innerWidth < 800; }; + Util.stripTags = function (text) { + var div = document.createElement("div"); + div.innerHTML = text; + return div.innerText; + }; + return Util; }); }(self)); diff --git a/www/common/cryptget.js b/www/common/cryptget.js index d08b4b297..9ca05164c 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -21,6 +21,11 @@ define([ S.leave(); } catch (e) { console.log(e); } } + if (S.session && S.session.stop) { + try { + S.session.stop(); + } catch (e) { console.error(e); } + } var abort = Util.find(S, ['session', 'realtime', 'abort']); if (typeof(abort) === 'function') { S.session.realtime.sync(); @@ -52,11 +57,12 @@ define([ Object.keys(b).forEach(function (k) { a[k] = b[k]; }); }; - var get = function (hash, cb, opt) { + var get = function (hash, cb, opt, progress) { if (typeof(cb) !== 'function') { throw new Error('Cryptget expects a callback'); } opt = opt || {}; + progress = progress || function () {}; var config = makeConfig(hash, opt); var Session = { cb: cb, hasNetwork: Boolean(opt.network) }; @@ -65,6 +71,7 @@ define([ var rt = Session.session = info.realtime; Session.network = info.network; Session.leave = info.leave; + progress(1); finish(Session, void 0, rt.getUserDoc()); }; @@ -72,6 +79,16 @@ define([ finish(Session, info.error); }; + // We use the new onMessage handler to compute the progress: + // we should receive 2 checkpoints max, so 100 messages max + // We're going to consider that 1 message = 1%, and we'll send 100% + // at the end + var i = 0; + config.onMessage = function () { + i++; + progress(Math.min(0.99, i/100)); + }; + overwrite(config, opt); Session.realtime = CPNetflux.start(config); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 0311387ee..a22f4d57e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -254,8 +254,12 @@ define([ common.clearOwnedChannel = function (channel, cb) { postMessage("CLEAR_OWNED_CHANNEL", channel, cb); }; - common.removeOwnedChannel = function (channel, cb) { - postMessage("REMOVE_OWNED_CHANNEL", channel, cb); + // "force" allows you to delete your drive ID + common.removeOwnedChannel = function (channel, cb, force) { + postMessage("REMOVE_OWNED_CHANNEL", { + channel: channel, + force: force + }, cb); }; common.getDeletedPads = function (data, cb) { @@ -950,7 +954,7 @@ define([ common.logoutFromAll(waitFor(function () { postMessage("DISCONNECT"); })); - })); + }), true); } }).nThen(function (waitFor) { if (!oldIsOwned) { diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 35633c4cd..3798c0609 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -15,6 +15,7 @@ define([ var DiffDOM = window.diffDOM; var renderer = new Marked.Renderer(); + var restrictedRenderer = new Marked.Renderer(); var Mermaid = { init: function () {} @@ -61,13 +62,18 @@ define([ return h('div.cp-md-toc', content).outerHTML; }; - DiffMd.render = function (md, sanitize) { + DiffMd.render = function (md, sanitize, restrictedMd) { + Marked.setOptions({ + renderer: restrictedMd ? restrictedRenderer : renderer, + }); var r = Marked(md, { sanitize: sanitize }); // Add Table of Content - r = r.replace(/
<\/div>/g, getTOC()); + if (!restrictedMd) { + r = r.replace(/
<\/div>/g, getTOC()); + } toc = []; return r; @@ -83,12 +89,7 @@ define([ return defaultCode.apply(renderer, arguments); } }; - - var stripTags = function (text) { - var div = document.createElement("div"); - div.innerHTML = text; - return div.innerText; - }; + restrictedRenderer.code = renderer.code; renderer.heading = function (text, level) { var i = 0; @@ -105,10 +106,13 @@ define([ toc.push({ level: level, id: id, - title: stripTags(text) + title: Util.stripTags(text) }); return "" + text + ""; }; + restrictedRenderer.heading = function (text) { + return text; + }; // Tasks list var checkedTaskItemPtn = /^\s*(

)?\[[xX]\](<\/p>)?\s*/; @@ -138,6 +142,13 @@ define([ var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : ''; return '' + text + '\n'; }; + restrictedRenderer.listitem = function (text) { + if (bogusCheckPtn.test(text)) { + text = text.replace(bogusCheckPtn, ''); + } + return '

  • ' + text + '
  • \n'; + }; + renderer.image = function (href, title, text) { if (href.slice(0,6) === '/file/') { // DEPRECATED @@ -162,12 +173,19 @@ define([ out += this.options.xhtml ? '/>' : '>'; return out; }; + restrictedRenderer.image = renderer.image; + var renderParagraph = function (p) { + return //i.test(p)? p + '\n': '

    ' + p + '

    \n'; + }; renderer.paragraph = function (p) { if (p === '[TOC]') { return '

    '; } - return //i.test(p)? p + '\n': '

    ' + p + '

    \n'; + return renderParagraph(p); + }; + restrictedRenderer.paragraph = function (p) { + return renderParagraph(p); }; var MutationObserver = window.MutationObserver; diff --git a/www/common/make-backup.js b/www/common/make-backup.js index b86f1f3b2..d9fb62d96 100644 --- a/www/common/make-backup.js +++ b/www/common/make-backup.js @@ -1,11 +1,13 @@ define([ '/common/cryptget.js', + '/file/file-crypto.js', '/common/common-hash.js', - '/common/sframe-common-file.js', + '/common/common-util.js', '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', '/bower_components/jszip/dist/jszip.min.js', -], function (Crypt, Hash, SFCFile, nThen, Saferphore, JsZip) { +], function (Crypt, FileCrypto, Hash, Util, nThen, Saferphore, JsZip) { + var saveAs = window.saveAs; var sanitize = function (str) { return str.replace(/[\\/?%*:|"<>]/gi, '_')/*.toLowerCase()*/; @@ -43,6 +45,88 @@ define([ }); }; + + var _downloadFile = function (ctx, fData, cb, updateProgress) { + var cancelled = false; + var cancel = function () { + cancelled = true; + }; + var parsed = Hash.parsePadUrl(fData.href || fData.roHref); + var hash = parsed.hash; + var name = fData.filename || fData.title; + var secret = Hash.getSecrets('file', hash, fData.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = secret.keys && secret.keys.cryptKey; + Util.fetch(src, function (err, u8) { + if (cancelled) { return; } + if (err) { return void cb('E404'); } + FileCrypto.decrypt(u8, key, function (err, res) { + if (cancelled) { return; } + if (err) { return void cb(err); } + if (!res.content) { return void cb('EEMPTY'); } + var dl = function () { + saveAs(res.content, name || res.metadata.name); + }; + cb(null, { + metadata: res.metadata, + content: res.content, + download: dl + }); + }, updateProgress && updateProgress.progress2); + }, updateProgress && updateProgress.progress); + return { + cancel: cancel + }; + + }; + + + var _downloadPad = function (ctx, pData, cb, updateProgress) { + var cancelled = false; + var cancel = function () { + cancelled = true; + }; + + var parsed = Hash.parsePadUrl(pData.href || pData.roHref); + var name = pData.filename || pData.title; + var opts = { + password: pData.password + }; + var done = false; + ctx.sframeChan.on("EV_CRYPTGET_PROGRESS", function (data) { + if (done || data.hash !== parsed.hash) { return; } + updateProgress.progress(data.progress); + if (data.progress === 1) { + done = true; + updateProgress.progress2(1); + } + }); + ctx.get({ + hash: parsed.hash, + opts: opts + }, function (err, val) { + if (cancelled) { return; } + if (err) { return; } + if (!val) { return; } + transform(ctx, parsed.type, val, function (res) { + if (cancelled) { return; } + if (!res.data) { return; } + var dl = function () { + saveAs(res.data, Util.fixFileName(name)); + }; + cb(null, { + metadata: res.metadata, + content: res.data, + download: dl + }); + }); + }); + return { + cancel: cancel + }; + + }; + // Add a file to the zip. We have to cryptget&transform it if it's a pad // or fetch&decrypt it if it's a file. var addFile = function (ctx, zip, fData, existingNames) { @@ -126,7 +210,7 @@ define([ // Files (mediatags...) var todoFile = function () { var it; - var dl = SFCFile.downloadFile(fData, function (err, res) { + var dl = _downloadFile(ctx, fData, function (err, res) { if (it) { clearInterval(it); } if (err) { return void error(err); } var opts = { @@ -189,6 +273,7 @@ define([ var ctx = { get: getPad, data: data.uo.drive, + folder: data.folder || ctx.data.root, sf: data.sf, zip: new JsZip(), errors: [], @@ -197,11 +282,12 @@ define([ max: 0, done: 0 }; + var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData; progress('reading', -1); nThen(function (waitFor) { ctx.waitFor = waitFor; var zipRoot = ctx.zip.folder('Root'); - makeFolder(ctx, ctx.data.root, zipRoot, ctx.data.filesData); + makeFolder(ctx, ctx.folder, zipRoot, filesData); progress('download', {}); }).nThen(function () { console.log(ctx.zip); @@ -222,7 +308,33 @@ define([ }; }; + + var _downloadFolder = function (ctx, data, cb, updateProgress) { + create(data, ctx.get, function (blob, errors) { + console.error(errors); // TODO show user errors + var dl = function () { + saveAs(blob, data.folderName); + }; + cb(null, {download: dl}); + }, function (state, progress) { + if (state === "reading") { + updateProgress.folderProgress(0); + } + if (state === "download") { + if (typeof progress.current !== "number") { return; } + updateProgress.folderProgress(progress.current / progress.max); + } + else if (state === "done") { + updateProgress.folderProgress(1); + } + }); + }; + + return { - create: create + create: create, + downloadFile: _downloadFile, + downloadPad: _downloadPad, + downloadFolder: _downloadFolder, }; }); diff --git a/www/common/media-tag.js b/www/common/media-tag.js index c820d9201..186fe4632 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -30,9 +30,22 @@ }; + var isplainTextFile = function (metadata) { + // does its type begins with "text/" + if (metadata.type.indexOf("text/") === 0) { return true; } + // no type and no file extension -> let's guess it's plain text + var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(metadata.name) || []; + if (!metadata.type && !parsedName[2]) { return true; } + // other exceptions + if (metadata.type === 'application/x-javascript') { return true; } + if (metadata.type === 'application/xml') { return true; } + return false; + }; + // Default config, can be overriden per media-tag call var config = { allowed: [ + 'text/plain', 'image/png', 'image/jpeg', 'image/jpg', @@ -53,6 +66,23 @@ text: "Download" }, Plugins: { + /** + * @param {object} metadataObject {name, metadatatype, owners} containing metadata of the file + * @param {strint} url Url of the blob object + * @param {Blob} content Blob object containing the data of the file + * @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins + * @param {function} cb Callback function: (err, pluginElement) => {} + */ + text: function (metadata, url, content, cfg, cb) { + var plainText = document.createElement('div'); + plainText.className = "plain-text-reader"; + var reader = new FileReader(); + reader.addEventListener('loadend', function (e) { + plainText.innerText = e.srcElement.result; + cb(void 0, plainText); + }); + reader.readAsText(content); + }, image: function (metadata, url, content, cfg, cb) { var img = document.createElement('img'); img.setAttribute('src', url); @@ -271,6 +301,9 @@ var blob = decrypted.content; var mediaType = getType(mediaObject, metadata, cfg); + if (isplainTextFile(metadata)) { + mediaType = "text"; + } if (mediaType === 'application') { mediaType = mediaObject.extension; diff --git a/www/common/messenger-ui.js b/www/common/messenger-ui.js index 682535d98..223f67bb3 100644 --- a/www/common/messenger-ui.js +++ b/www/common/messenger-ui.js @@ -151,7 +151,7 @@ define([ }); try { var $d = $(d); - DiffMd.apply(DiffMd.render(md || '', true), $d, common); + DiffMd.apply(DiffMd.render(md || '', true, true), $d, common); $d.addClass("cp-app-contacts-content"); // override link clicking, because we're in an iframe @@ -197,7 +197,7 @@ define([ var getChat = function (id) { return $messages.find(dataQuery(id)); }; - + var scrollChatToBottom = function () { var $messagebox = $('.cp-app-contacts-messages'); $messagebox.scrollTop($messagebox[0].scrollHeight); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 2b504d39a..02d516ff7 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -99,6 +99,7 @@ define(['json.sortify'], function (Sortify) { var addAuthor = function () { if (!meta.user || !meta.user.netfluxId || !priv || !priv.edPublic) { return; } var authors = metadataObj.authors || {}; + var old = Sortify(authors); if (!authors[priv.edPublic]) { authors[priv.edPublic] = { nId: [meta.user.netfluxId], @@ -110,9 +111,11 @@ define(['json.sortify'], function (Sortify) { authors[priv.edPublic].nId.push(meta.user.netfluxId); } } - metadataObj.authors = authors; - metadataLazyObj.authors = JSON.parse(JSON.stringify(authors)); - change(); + if (Sortify(authors) !== old) { + metadataObj.authors = authors; + metadataLazyObj.authors = JSON.parse(JSON.stringify(authors)); + change(); + } }; var netfluxId; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index ca7e4cd7c..e793cbcb3 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -34,7 +34,7 @@ define([ var sendDriveEvent = function () {}; var registerProxyEvents = function () {}; - var storeHash; + var storeHash, storeChannel; var store = window.CryptPad_AsyncStore = { modules: {} @@ -239,6 +239,20 @@ define([ Store.removeOwnedChannel = function (clientId, data, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + + // "data" used to be a string (channelID), now it can also be an object + // data.force tells us we can safely remove the drive ID + var channel = data; + var force = false; + if (data && typeof(data) === "object") { + channel = data.channel; + force = data.force; + } + + if (channel === storeChannel && !force) { + return void cb({error: 'User drive removal blocked!'}); + } + store.rpc.removeOwnedChannel(data, function (err) { cb({error:err}); }); @@ -573,7 +587,10 @@ define([ })); }).nThen(function (waitFor) { // Delete Drive - Store.removeOwnedChannel(clientId, secret.channel, waitFor()); + Store.removeOwnedChannel(clientId, { + channel: secret.channel, + force: true + }, waitFor()); }).nThen(function () { store.network.disconnect(); cb({ @@ -786,6 +803,7 @@ define([ var h = p.hashData; if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); } + if (p.type === "debug") { return void cb(); } var channelData = Store.channels && Store.channels[channel]; @@ -1090,7 +1108,6 @@ define([ var channels = Store.channels = store.channels = {}; Store.joinPad = function (clientId, data) { - console.log('joining', data.channel); var isNew = typeof channels[data.channel] === "undefined"; var channel = channels[data.channel] = channels[data.channel] || { queue: [], @@ -1915,6 +1932,7 @@ define([ } // No password for drive var secret = Hash.getSecrets('drive', hash); + storeChannel = secret.channel; var listmapConfig = { data: {}, websocketURL: NetConfig.getWebsocketURL(), diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index ca39717ad..f910f12b6 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -99,9 +99,17 @@ define([ // lines beginning with a hash are potentially valuable // works for markdown, python, bash, etc. var hash = /^#+(.*?)$/; + var hashAndLink = /^#+\s*\[(.*?)\]\(.*\)\s*$/; if (hash.test(line)) { + // test for link inside the title, and set text just to the name of the link + if (hashAndLink.test(line)) { + line.replace(hashAndLink, function (a, one) { + text = Util.stripTags(one); + }); + return true; + } line.replace(hash, function (a, one) { - text = one; + text = Util.stripTags(one); }); return true; } @@ -387,21 +395,32 @@ define([ exp.mkIndentSettings = function (metadataMgr) { var setIndentation = function (units, useTabs, fontSize, spellcheck) { if (typeof(units) !== 'number') { return; } + var doc = editor.getDoc(); editor.setOption('indentUnit', units); editor.setOption('tabSize', units); editor.setOption('indentWithTabs', useTabs); editor.setOption('spellcheck', spellcheck); - if (!useTabs) { - editor.setOption("extraKeys", { - Tab: function() { - editor.replaceSelection(Array(units + 1).join(" ")); + editor.setOption("extraKeys", { + Tab: function() { + if (doc.somethingSelected()) { + editor.execCommand("indentMore"); } - }); - } else { - editor.setOption("extraKeys", { - Tab: undefined, - }); - } + else { + if (!useTabs) { editor.execCommand("insertSoftTab"); } + else { editor.execCommand("insertTab"); } + } + }, + "Shift-Tab": function () { + editor.execCommand("indentLess"); + }, + "Backspace": function () { + var cursor = doc.getCursor(); + var line = doc.getLine(cursor.line); + if (line.substring(0, cursor.ch).trim() === "") { editor.execCommand("indentLess"); } + else { editor.execCommand("delCharBefore"); } + + }, + }); $('.CodeMirror').css('font-size', fontSize+'px'); }; diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 099176afd..32f031010 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -1,6 +1,7 @@ define([ 'jquery', '/file/file-crypto.js', + '/common/make-backup.js', '/common/common-thumbnail.js', '/common/common-interface.js', '/common/common-ui-elements.js', @@ -11,9 +12,8 @@ define([ '/bower_components/file-saver/FileSaver.min.js', '/bower_components/tweetnacl/nacl-fast.min.js', -], function ($, FileCrypto, Thumb, UI, UIElements, Util, Hash, h, Messages) { +], function ($, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages) { var Nacl = window.nacl; - var saveAs = window.saveAs; var module = {}; var blobToArrayBuffer = function (blob, cb) { @@ -446,124 +446,108 @@ define([ createUploader(config.dropArea, config.hoverArea, config.body); - File.downloadFile = function (fData, cb) { - var parsed = Hash.parsePadUrl(fData.href || fData.roHref); - var hash = parsed.hash; - var name = fData.filename || fData.title; - var secret = Hash.getSecrets('file', hash, fData.password); - var src = Hash.getBlobPathFromHex(secret.channel); - var key = secret.keys && secret.keys.cryptKey; - common.getFileSize(secret.channel, function (e, data) { - var todo = function (file) { - if (queue.inProgress) { return; } - queue.inProgress = true; - var id = file.id; - - var $row = $table.find('tr[id="'+id+'"]'); - var $pv = $row.find('.cp-fileupload-table-progress-value'); - var $pb = $row.find('.cp-fileupload-table-progress-container'); - var $pc = $row.find('.cp-fileupload-table-progress'); - var $link = $row.find('.cp-fileupload-table-link'); - - var done = function () { - $row.find('.cp-fileupload-table-cancel').text('-'); - queue.inProgress = false; - queue.next(); - }; + var updateProgressbar = function (file, data, downloadFunction, cb) { + if (queue.inProgress) { return; } + queue.inProgress = true; + var id = file.id; - var updateDLProgress = function (progressValue) { - var text = Math.round(progressValue*100) + '%'; - text += ' ('+ Messages.download_step1 +'...)'; - $pv.text(text); - $pb.css({ - width: progressValue * $pc.width()+'px' - }); - }; - var updateProgress = function (progressValue) { - var text = Math.round(progressValue*100) + '%'; - text += progressValue === 1 ? '' : ' ('+ Messages.download_step2 +'...)'; - $pv.text(text); - $pb.css({ - width: progressValue * $pc.width()+'px' - }); - }; + var $row = $table.find('tr[id="'+id+'"]'); + var $pv = $row.find('.cp-fileupload-table-progress-value'); + var $pb = $row.find('.cp-fileupload-table-progress-container'); + var $pc = $row.find('.cp-fileupload-table-progress'); + var $link = $row.find('.cp-fileupload-table-link'); - var dl = module.downloadFile(fData, function (err, obj) { - $link.prepend($('', {'class': 'fa fa-external-link'})) - .attr('href', '#') - .click(function (e) { - e.preventDefault(); - obj.download(); - }); - done(); - if (obj) { obj.download(); } - cb(err, obj); - }, { - src: src, - key: key, - name: name, - progress: updateDLProgress, - progress2: updateProgress, - }); + var done = function () { + $row.find('.cp-fileupload-table-cancel').text('-'); + queue.inProgress = false; + queue.next(); + }; - var $cancel = $('', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () { - dl.cancel(); - $cancel.remove(); - $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); - done(); - }); - $row.find('.cp-fileupload-table-cancel').html('').append($cancel); - }; + var updateDLProgress = function (progressValue) { + var text = Math.round(progressValue * 100) + '%'; + text += ' (' + Messages.download_step1 + '...)'; + $pv.text(text); + $pb.css({ + width: progressValue * $pc.width() + 'px' + }); + }; + var updateDecryptProgress = function (progressValue) { + var text = Math.round(progressValue*100) + '%'; + text += progressValue === 1 ? '' : ' (' + Messages.download_step2 + '...)'; + $pv.text(text); + $pb.css({ + width: progressValue * $pc.width()+'px' + }); + }; + var updateProgress = function (progressValue) { + var text = Math.round(progressValue*100) + '%'; + $pv.text(text); + $pb.css({ + width: progressValue * $pc.width()+'px' + }); + }; + + var ctx = { + get: common.getPad, + sframeChan: sframeChan, + }; + downloadFunction(ctx, data, function (err, obj) { + $link.prepend($('', {'class': 'fa fa-external-link'})) + .attr('href', '#') + .click(function (e) { + e.preventDefault(); + obj.download(); + }); + done(); + if (obj) { obj.download(); } + cb(err, obj); + }, { + progress: updateDLProgress, + progress2: updateDecryptProgress, + folderProgress: updateProgress, + }); + +// var $cancel = $('', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () { +// dl.cancel(); +// $cancel.remove(); +// $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); +// done(); +// }); +// $row.find('.cp-fileupload-table-cancel').html('').append($cancel); + $row.find('.cp-fileupload-table-cancel').html(''); + }; + + + File.downloadFile = function (fData, cb) { + var name = fData.filename || fData.title; + common.getFileSize(fData.channel, function (e, data) { queue.push({ - dl: todo, + dl: function (file) { updateProgressbar(file, fData, MakeBackup.downloadFile, cb); }, size: data, name: name }); }); }; - return File; - }; - - module.downloadFile = function (fData, cb, obj) { - var cancelled = false; - var cancel = function () { - cancelled = true; + File.downloadPad = function (pData, cb) { + queue.push({ + dl: function (file) { updateProgressbar(file, pData, MakeBackup.downloadPad, cb); }, + size: 0, + name: pData.title, + }); }; - var src, key, name; - if (obj && obj.src && obj.key && obj.name) { - src = obj.src; - key = obj.key; - name = obj.name; - } else { - var parsed = Hash.parsePadUrl(fData.href || fData.roHref); - var hash = parsed.hash; - name = fData.filename || fData.title; - var secret = Hash.getSecrets('file', hash, fData.password); - src = Hash.getBlobPathFromHex(secret.channel); - key = secret.keys && secret.keys.cryptKey; - } - Util.fetch(src, function (err, u8) { - if (cancelled) { return; } - if (err) { return void cb('E404'); } - FileCrypto.decrypt(u8, key, function (err, res) { - if (cancelled) { return; } - if (err) { return void cb(err); } - if (!res.content) { return void cb('EEMPTY'); } - var dl = function () { - saveAs(res.content, name || res.metadata.name); - }; - cb(null, { - metadata: res.metadata, - content: res.content, - download: dl - }); - }, obj && obj.progress2); - }, obj && obj.progress); - return { - cancel: cancel + + File.downloadFolder = function (data, cb) { + queue.push({ + dl: function (file) { updateProgressbar(file, data, MakeBackup.downloadFolder, cb); }, + size: 0, + name: data.folderName, + }); }; + + return File; }; + return module; }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 13229969d..104bdaa12 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -838,13 +838,6 @@ define([ Cryptpad.setLanguage(data, cb); }); - sframeChan.on('Q_CLEAR_OWNED_CHANNEL', function (channel, cb) { - Cryptpad.clearOwnedChannel(channel, cb); - }); - sframeChan.on('Q_REMOVE_OWNED_CHANNEL', function (channel, cb) { - Cryptpad.removeOwnedChannel(channel, cb); - }); - sframeChan.on('Q_GET_ALL_TAGS', function (data, cb) { Cryptpad.listAllTags(function (err, tags) { cb({ @@ -871,6 +864,9 @@ define([ Cryptpad.removeLoginBlock(data, cb); }); + // It seems we have performance issues when we open and close a lot of channels over + // the same network, maybe a memory leak. To fix this, we kill and create a new + // network every 30 cryptget calls (1 call = 1 channel) var cgNetwork; var whenCGReady = function (cb) { if (cgNetwork && cgNetwork !== true) { console.log(cgNetwork); return void cb(); } @@ -887,7 +883,12 @@ define([ error: err, data: val }); - }, data.opts); + }, data.opts, function (progress) { + sframeChan.event("EV_CRYPTGET_PROGRESS", { + hash: data.hash, + progress: progress, + }); + }); }; //return void todo(); if (i > 30) { diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index f93021bd3..82c71ace2 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -459,6 +459,14 @@ define([ }); }; */ + funcs.getPad = function (data, cb) { + ctx.sframeChan.query("Q_CRYPTGET", data, function (err, obj) { + if (err) { return void cb(err); } + if (obj.error) { return void cb(obj.error); } + cb(null, obj.data); + }, { timeout: 60000 }); + }; + funcs.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); }; funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); }; funcs.openUnsafeURL = function (url) { diff --git a/www/drive/app-drive.less b/www/drive/app-drive.less index c85cd3c88..d04abbd08 100644 --- a/www/drive/app-drive.less +++ b/www/drive/app-drive.less @@ -30,6 +30,7 @@ @drive_content-bg-ro: darken(@drive_content-bg, 10%); @drive_selected-bg: #888; + @drive_droppable-bg: #FE9A2E; /* PAGE */ @@ -107,7 +108,7 @@ .cp-app-drive-container { flex: 1; - overflow: auto; + overflow-x: auto; width: 100%; display: flex; flex-flow: row; @@ -121,6 +122,7 @@ #cp-app-drive-tree { resize: none; width: 100% !important; + min-width: unset; max-width: unset; max-height: unset; border-bottom: 1px solid @drive_mobile-tree-border-col; @@ -156,7 +158,7 @@ } .cp-app-drive-element-droppable { - background-color: #FE9A2E; + background-color: @drive_droppable-bg; color: #222; } @@ -239,7 +241,6 @@ max-height: 100%; .cp-app-drive-tree-categories-container { flex: 1; - max-width: 500px; overflow: auto; } img.cp-app-drive-icon { @@ -438,13 +439,13 @@ flex: 1; // Needed to avoid the folder's path to overflows // https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout - min-width: 0; + // min-width: 0; } #cp-app-drive-content { box-sizing: border-box; background: @drive_content-bg; color: @drive_content-fg; - overflow: auto; + overflow-y: auto; flex: 1; display: flex; flex-flow: column; @@ -939,6 +940,7 @@ overflow: hidden; text-overflow: ellipsis; transition: all 0.15s; + cursor: pointer; &:first-child { flex-shrink: 1; @@ -946,17 +948,20 @@ &.cp-app-drive-path-separator { color: #ccc; + cursor: default; } &.cp-app-drive-path-collapse { position: relative; } - &:hover { + &.cp-app-drive-element-droppable { + background-color: @drive_droppable-bg; + } + &:not(.cp-app-drive-element-droppable):hover { &:not(.cp-app-drive-path-separator) { background-color: darken(@colortheme_drive-bg, 15%); text-decoration: underline; - cursor: pointer; } & ~ .cp-app-drive-path-element { background-color: darken(@colortheme_drive-bg, 15%); diff --git a/www/drive/inner.js b/www/drive/inner.js index b3c6a71f0..456b3588d 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -17,8 +17,6 @@ define([ '/bower_components/chainpad-listmap/chainpad-listmap.js', '/customize/messages.js', - '/common/jscolor.js', - 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/drive/app-drive.less', @@ -43,7 +41,10 @@ define([ { var APP = window.APP = { editable: false, - mobile: function () { return $('body').width() <= 600; }, // Menu and content area are not inline-block anymore for mobiles + mobile: function () { + if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; } + else { return $('body').width() <= 600; } + }, isMac: navigator.platform === "MacIntel", }; @@ -299,6 +300,33 @@ define([ }); }; + + APP.selectedFiles = []; + + var isElementSelected = function ($element) { + var elementId = $element.data("path").slice(-1)[0]; + return APP.selectedFiles.indexOf(elementId) !== -1; + }; + var selectElement = function ($element) { + var elementId = $element.data("path").slice(-1)[0]; + if (APP.selectedFiles.indexOf(elementId) === -1) { + APP.selectedFiles.push(elementId); + } + $element.addClass("cp-app-drive-element-selected"); + }; + var unselectElement = function ($element) { + var elementId = $element.data("path").slice(-1)[0]; + var index = APP.selectedFiles.indexOf(elementId); + if (index !== -1) { + APP.selectedFiles.splice(index, 1); + } + $element.removeClass("cp-app-drive-element-selected"); + }; + var findSelectedElements = function () { + return $(".cp-app-drive-element-selected"); + }; + + var createContextMenu = function () { var menu = h('div.cp-contextmenu.dropdown.cp-unselectable', [ h('ul.dropdown-menu', { @@ -363,16 +391,34 @@ define([ 'data-icon': AppConfig.applicationsIcon.slide, 'data-type': 'slide' }, Messages.button_newslide)), - h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { - 'tabindex': '-1', - 'data-icon': AppConfig.applicationsIcon.poll, - 'data-type': 'poll' - }, Messages.button_newpoll)), - h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { - 'tabindex': '-1', - 'data-icon': AppConfig.applicationsIcon.whiteboard, - 'data-type': 'whiteboard' - }, Messages.button_newwhiteboard)), + h('li.dropdown-submenu', [ + h('a.cp-app-drive-context-newdocmenu.dropdown-item', { + 'tabindex': '-1', + 'data-icon': "fa-plus", + }, Messages.fm_morePads || "More pads"), //XXX + h("ul.dropdown-menu", [ + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.sheet, + 'data-type': 'sheet' + }, Messages.button_newsheet)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.whiteboard, + 'data-type': 'whiteboard' + }, Messages.button_newwhiteboard)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.kanban, + 'data-type': 'kanban' + }, Messages.button_newkanban)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.poll, + 'data-type': 'poll' + }, Messages.button_newpoll)), + ]), + ]), $separator.clone()[0], h('li', h('a.cp-app-drive-context-empty.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', @@ -419,6 +465,7 @@ define([ }, Messages.fc_prop)), ]) ]); + // add icons to the contextmenu options $(menu).find("li a.dropdown-item").each(function (i, el) { var $icon = $(""); if ($(el).attr('data-icon')) { @@ -429,21 +476,46 @@ define([ } $(el).prepend($icon); }); + // add events handlers for the contextmenu submenus $(menu).find(".dropdown-submenu").each(function (i, el) { var $el = $(el); var $a = $el.children().filter("a"); var $sub = $el.find(".dropdown-menu").first(); + var timeoutId; + var showSubmenu = function () { + clearTimeout(timeoutId); + $sub.toggleClass("left", $el.offset().left + $el.outerWidth() + $sub.outerWidth() > $(window).width()); + $el.siblings().find(".dropdown-menu").hide(); + $sub.show(); + }; + var hideSubmenu = function () { + $sub.hide(); + $sub.removeClass("left"); + }; + var mouseOutSubmenu = function () { + // don't hide immediately the submenu + timeoutId = setTimeout(hideSubmenu, 100); + }; // Add submenu expand icon $a.append(h("span.dropdown-toggle")); // Show / hide submenu $el.hover(function () { - setTimeout(function () { // wait for dom to update - $sub.toggleClass("left", $el.offset().left + $el.outerWidth() + $sub.outerWidth() > $(window).width()); - $sub.show(); - }); + showSubmenu(); }, function () { - $sub.hide(); - $sub.removeClass("left"); + mouseOutSubmenu(); + }); + // handle click event + $el.click(function (e) { + var targetItem = $(e.target).closest(".dropdown-item")[0]; // don't close contextmenu if open submenu + var elTarget = $el.children(".dropdown-item")[0]; + if (targetItem === elTarget) { e.stopPropagation(); } + if ($el.children().filter(".dropdown-menu:visible").length !== 0) { + $el.find(".dropdown-menu").hide(); + hideSubmenu(); + } + else { + showSubmenu(); + } }); }); return $(menu); @@ -569,7 +641,8 @@ define([ var sel = {}; var removeSelected = function (keepObj) { - $('.cp-app-drive-element-selected').removeClass("cp-app-drive-element-selected"); + APP.selectedFiles = []; + findSelectedElements().removeClass("cp-app-drive-element-selected"); var $container = $driveToolbar.find('#cp-app-drive-toolbar-contextbuttons'); if (!$container.length) { return; } $container.html(''); @@ -679,7 +752,9 @@ define([ delete sel.move; $content.find('.cp-app-drive-element-selected-tmp') .removeClass('cp-app-drive-element-selected-tmp') - .addClass('cp-app-drive-element-selected'); + .each(function (idx, element) { + selectElement($(element)); + }); e.stopPropagation(); }); @@ -714,7 +789,9 @@ define([ // Ctrl+A select all if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) { $content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)') - .addClass('cp-app-drive-element-selected'); + .each(function (idx, element) { + selectElement($(element)); + }); return; } @@ -727,7 +804,7 @@ define([ APP.onElementClick(ev, $(el)); }; - var $selection = $content.find('.cp-app-drive-element.cp-app-drive-element-selected'); + var $selection = findSelectedElements(); if ($selection.length === 0) { return void click($elements.first()[0]); } var lastIndex = typeof sel.endSelected === "number" ? sel.endSelected : @@ -846,12 +923,12 @@ define([ return; } removeInput(); - removeSelected(); var $name = $element.find('.cp-app-drive-element-name'); if (!$name.length) { $name = $element.find('> .cp-app-drive-element'); } $name.hide(); + var isFolder = $element.is(".cp-app-drive-element-folder:not(.cp-app-drive-element-sharedf)"); var el = manager.find(path); var name = manager.isFile(el) ? manager.getTitle(el) : path[path.length - 1]; if (manager.isSharedFolder(el)) { @@ -873,14 +950,21 @@ define([ var newName = $input.val(); if (JSON.stringify(path) === JSON.stringify(currentPath)) { manager.rename(path, $input.val(), function () { - renameFoldersOpened(path, newName); - path[path.length - 1] = newName; + if (isFolder) { + renameFoldersOpened(path, newName); + path[path.length - 1] = newName; + } APP.displayDirectory(path); }); } else { manager.rename(path, $input.val(), function () { - renameFoldersOpened(path, newName); + if (isFolder) { + renameFoldersOpened(path, newName); + unselectElement($element); + $element.data("path", $element.data("path").slice(0, -1).concat(newName)); + selectElement($element); + } refresh(); }); } @@ -901,7 +985,6 @@ define([ // We don't want to open the file/folder when clicking on the input $input.on('click dblclick', function (e) { - removeSelected(); e.stopPropagation(); }); // Remove the browser ability to drag text from the input to avoid @@ -1122,8 +1205,9 @@ define([ var getSelectedPaths = function ($element) { var paths = []; - if ($('.cp-app-drive-element-selected').length > 1) { - var $selected = $('.cp-app-drive-element-selected'); + if (!$element || $element.length === 0) { return paths; } + if (findSelectedElements().length > 1) { + var $selected = findSelectedElements(); $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { @@ -1152,7 +1236,7 @@ define([ } else { $driveToolbar.find('cp-app-drive-toolbar-emptytrash').hide(); } - var $li = $content.find('.cp-app-drive-element-selected'); + var $li = findSelectedElements(); if ($li.length === 0) { $li = findDataHolder($tree.find('.cp-app-drive-element-active')); } @@ -1219,6 +1303,7 @@ define([ if (pos+eh <= h && pos >= 0) { return; } $content.scrollTop(v); }; + // Add the "selected" class to the "li" corresponding to the clicked element var onElementClick = APP.onElementClick = function (e, $element) { // If "Ctrl" is pressed, do not remove the current selection @@ -1255,34 +1340,33 @@ define([ var $el; removeSelected(true); sel.oldSelection.forEach(function (el) { - if (!$(el).hasClass("cp-app-drive-element-selected")) { - $(el).addClass("cp-app-drive-element-selected"); + if (!isElementSelected($(el))) { + selectElement($(el)); } }); for (var i = Math.min(sel.startSelected, sel.endSelected); i <= Math.max(sel.startSelected, sel.endSelected); i++) { $el = $($elements.get(i)); - if (!$el.hasClass("cp-app-drive-element-selected")) { - $el.addClass("cp-app-drive-element-selected"); + if (!isElementSelected($el)) { + selectElement($el); } } } else { - if (!$element.hasClass("cp-app-drive-element-selected")) { - $element.addClass("cp-app-drive-element-selected"); + if (!isElementSelected($element)) { + selectElement($element); } else { - $element.removeClass("cp-app-drive-element-selected"); + unselectElement($element); } } updateContextButton(); }; - var displayMenu = function (e) { - var $menu = $contextMenu; + // show / hide dropdown separators + var hideSeparators = function ($menu) { var showSep = false; var $lastVisibleSep = null; - // show / hide drop-down divider - $menu.find(".dropdown-menu").children().each(function (i, el) { + $menu.children().each(function (i, el) { var $el = $(el); if ($el.is(".dropdown-divider")) { $el.css("display", showSep ? "list-item" : "none"); @@ -1294,18 +1378,36 @@ define([ } }); if (!showSep && $lastVisibleSep) { $lastVisibleSep.css("display", "none"); } // remove last divider if no options after + }; + + // prepare and display contextmenu + var displayMenu = function (e) { + var $menu = $contextMenu; // show / hide submenus $menu.find(".dropdown-submenu").each(function (i, el) { var $el = $(el); + $el.children(".dropdown-menu").css("display", "none"); $el.find("li").each(function (i, li) { if ($(li).css("display") !== "none") { - $(el).css("display", "block"); + $el.css("display", "block"); return; } }); }); + // show / hide separators + $menu.find(".dropdown-menu").each(function (i, menu) { + hideSeparators($(menu)); + }); + // show contextmenu at cursor position $menu.css({ display: "block" }); - if (APP.mobile()) { return; } + if (APP.mobile()) { + $menu.css({ + top: ($("#cp-app-drive-toolbar-context-mobile").offset().top + 32) + 'px', + right: '0px', + left: '' + }); + return; + } var h = $menu.outerHeight(); var w = $menu.outerWidth(); var wH = window.innerHeight; @@ -1358,6 +1460,17 @@ define([ } else { var $element = findDataHolder($(e.target)); + // if clicked from tree + var fromTree = $element.closest("#cp-app-drive-tree").length; + if (fromTree) { + removeSelected(); + } + + // if clicked on non selected element + if (!isElementSelected($element)) { + removeSelected(); + } + if (type === 'trash' && !$element.data('path')) { return; } if (!$element.length) { @@ -1366,8 +1479,8 @@ define([ return false; } - if (!$element.hasClass('cp-app-drive-element-selected')) { - onElementClick(undefined, $element); + if (!isElementSelected($element)) { + selectElement($element); } paths = getSelectedPaths($element); @@ -1437,6 +1550,7 @@ define([ if (!res) { return; } manager.delete(pathsList, function () { pathsList.forEach(removeFoldersOpened); + removeSelected(); refresh(); }); }, null, true); @@ -1447,7 +1561,7 @@ define([ var paths = []; var $element = findDataHolder($(ev.target)); if ($element.hasClass('cp-app-drive-element-selected')) { - var $selected = $('.cp-app-drive-element-selected'); + var $selected = findSelectedElements(); $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { @@ -1464,7 +1578,7 @@ define([ }); } else { removeSelected(); - $element.addClass('cp-app-drive-element-selected'); + selectElement($element); var val = manager.find(path); if (!val) { return; } // The element is not in the object paths = [{ @@ -1483,7 +1597,13 @@ define([ var findDropPath = function (target) { var $target = $(target); - var $el = findDataHolder($target); + var $el; + if ($target.is(".cp-app-drive-path-element")) { + $el = $target; + } + else { + $el = findDataHolder($target); + } var newPath = $el.data('path'); var dropEl = newPath && manager.find(newPath); if (newPath && manager.isSharedFolder(dropEl)) { @@ -1611,7 +1731,8 @@ define([ $owner.attr('title', Messages.fm_padIsOwnedOther); } }; - var addFileData = function (element, $span) { + var thumbsUrls = {}; + var addFileData = function (element, $element) { if (!manager.isFile(element)) { return; } var data = manager.getFileData(element); @@ -1620,7 +1741,7 @@ define([ var hrefData = Hash.parsePadUrl(href); if (hrefData.type) { - $span.addClass('cp-border-color-'+hrefData.type); + $element.addClass('cp-border-color-'+hrefData.type); } var $state = $('', {'class': 'cp-app-drive-element-state'}); @@ -1640,25 +1761,38 @@ define([ var $expire = $expirableIcon.clone().appendTo($state); $expire.attr('title', Messages._getKey('fm_expirablePad', [new Date(data.expire).toLocaleString()])); } - _addOwnership($span, $state, data); + _addOwnership($element, $state, data); var name = manager.getTitle(element); // The element with the class '.name' is underlined when the 'li' is hovered var $name = $('', {'class': 'cp-app-drive-element-name'}).text(name); - $span.append($name); - $span.append($state); - $span.attr('title', name); + $element.append($name); + $element.append($state); + $element.attr('title', name); + + // display the thumbnail + // if the thumbnail has already been displayed once, do not reload it, keep the same url + if (thumbsUrls[element]) { + var img = new Image(); + img.src = thumbsUrls[element]; + $element.find('.cp-icon').addClass('cp-app-drive-element-list'); + $element.prepend(img); + $(img).addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail'); + $(img).attr("draggable", false); + } + else { + common.displayThumbnail(href || data.roHref, data.channel, data.password, $element, function ($thumb) { + // Called only if the thumbnail exists + // Remove the .hide() added by displayThumnail() because it hides the icon in list mode too + $element.find('.cp-icon').removeAttr('style').addClass('cp-app-drive-element-list'); + $thumb.addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail'); + $thumb.attr("draggable", false); + thumbsUrls[element] = $thumb[0].src; + }); + } var type = Messages.type[hrefData.type] || hrefData.type; - common.displayThumbnail(href || data.roHref, data.channel, data.password, $span, function ($thumb) { - // Called only if the thumbnail exists - // Remove the .hide() added by displayThumnail() because it hides the icon in - // list mode too - $span.find('.cp-icon').removeAttr('style').addClass('cp-app-drive-element-list'); - $thumb.addClass('cp-app-drive-element-grid') - .addClass('cp-app-drive-element-thumbnail'); - }); var $type = $('', { 'class': 'cp-app-drive-element-type cp-app-drive-element-list' }).text(type); @@ -1668,7 +1802,7 @@ define([ var $cdate = $('', { 'class': 'cp-app-drive-element-ctime cp-app-drive-element-list' }).text(getDate(data.ctime)); - $span.append($type).append($adate).append($cdate); + $element.append($type).append($adate).append($cdate); }; var addFolderData = function (element, key, $span) { @@ -1744,12 +1878,9 @@ define([ draggable: true, 'class': 'cp-app-drive-element-row' }); - if (!isFolder && Array.isArray(APP.selectedFiles)) { - var idx = APP.selectedFiles.indexOf(element); - if (idx !== -1) { - $element.addClass('cp-app-drive-element-selected'); - APP.selectedFiles.splice(idx, 1); - } + $element.data('path', newPath); + if (isElementSelected($element)) { + selectElement($element); } $element.prepend($icon).dblclick(function () { if (isFolder) { @@ -1765,11 +1896,10 @@ define([ addFileData(element, $element); } $element.addClass(liClass); - $element.data('path', newPath); addDragAndDropHandlers($element, newPath, isFolder, !isTrash); $element.click(function(e) { e.stopPropagation(); - onElementClick(e, $element, newPath); + onElementClick(e, $element); }); if (!isTrash) { $element.contextmenu(openContextMenu('tree')); @@ -1920,6 +2050,8 @@ define([ } else if (idx > 0 && manager.isFile(el)) { name = getElementName(path); } + $span.data("path", path.slice(0, idx + 1)); + addDragAndDropHandlers($span, path.slice(0, idx), true, true); if (idx === 0) { name = p === SHARED_FOLDER ? name : getPrettyName(p); } else { @@ -2564,22 +2696,19 @@ define([ 'class': 'cp-app-drive-element cp-app-drive-element-file cp-app-drive-element-row' + roClass, draggable: draggable }); - if (Array.isArray(APP.selectedFiles)) { - var sidx = APP.selectedFiles.indexOf(id); - if (sidx !== -1) { - $element.addClass('cp-app-drive-element-selected'); - APP.selectedFiles.splice(sidx, 1); - } + + var path = [rootName, idx]; + $element.data('path', path); + if (isElementSelected($element)) { + selectElement($element); } $element.prepend($icon).dblclick(function () { openFile(id); }); addFileData(id, $element); - var path = [rootName, idx]; - $element.data('path', path); $element.click(function(e) { e.stopPropagation(); - onElementClick(e, $element, path); + onElementClick(e, $element); }); $element.contextmenu(openContextMenu('default')); $element.data('context', 'default'); @@ -2706,8 +2835,8 @@ define([ e.preventDefault(); if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; } else { parentPath.pop(); } - APP.selectedFiles = [r.id]; APP.displayDirectory(parentPath); + APP.selectedFiles = path.slice(-1); }).appendTo($openDir); } $('').text(Messages.fc_prop).click(function () { @@ -2798,7 +2927,7 @@ define([ $element.data('path', path); $element.click(function(e) { e.stopPropagation(); - onElementClick(e, $element, path); + onElementClick(e, $element); }); $element.contextmenu(openContextMenu('default')); $element.data('context', 'default'); @@ -3022,7 +3151,7 @@ define([ $context.click(function (e) { e.preventDefault(); e.stopPropagation(); - var $li = $content.find('.cp-app-drive-element-selected'); + var $li = findSelectedElements(); if ($li.length !== 1) { $li = findDataHolder($tree.find('.cp-app-drive-element-active')); } @@ -3032,11 +3161,6 @@ define([ return; } // Open the menu - $('.cp-contextmenu').css({ - top: ($context.offset().top + 32) + 'px', - right: '0px', - left: '' - }); $li.contextmenu(); }); } else { @@ -3104,7 +3228,7 @@ define([ } });*/ - var $sel = $content.find('.cp-app-drive-element-selected'); + var $sel = findSelectedElements(); if ($sel.length) { $sel[0].scrollIntoView(); } else { @@ -3116,6 +3240,9 @@ define([ if (history.isHistoryMode) { return void _displayDirectory(path, force); } + if (!manager.comparePath(currentPath, path)) { + removeSelected(); + } updateObject(sframeChan, proxy, function () { copyObjectValue(files, proxy.drive); updateSharedFolders(sframeChan, manager, files, folders, function () { @@ -3466,15 +3593,37 @@ define([ if (!res) { return; } manager.delete(pathsList, function () { pathsList.forEach(removeFoldersOpened); + removeSelected(); refresh(); }); }); }; + + + var downloadFolder = function (folderElement, folderName, sfId) { + var todo = function (data) { + data.folder = folderElement; + data.sharedFolderId = sfId; + data.folderName = Util.fixFileName(folderName) + '.zip'; + + APP.FM.downloadFolder(data, function (err, obj) { + console.log(err, obj); + console.log('DONE'); + }); + }; + todo({ + uo: proxy, + sf: folders, + }); + }; + + $contextMenu.on("click", "a", function(e) { e.stopPropagation(); var paths = $contextMenu.data('paths'); var pathsList = []; var type = $contextMenu.attr('data-menu-type'); + var $this = $(this); var el, data; if (paths.length === 0) { @@ -3483,11 +3632,11 @@ define([ return; } - if ($(this).hasClass("cp-app-drive-context-rename")) { + if ($this.hasClass("cp-app-drive-context-rename")) { if (paths.length !== 1) { return; } displayRenameInput(paths[0].element, paths[0].path); } - if ($(this).hasClass("cp-app-drive-context-color")) { + else if ($this.hasClass("cp-app-drive-context-color")) { var currentColor = getFolderColor(paths[0].path); pickFolderColor(paths[0].element, currentColor, function (color) { paths.forEach(function (p) { @@ -3496,24 +3645,24 @@ define([ refresh(); }); } - else if($(this).hasClass("cp-app-drive-context-delete")) { + else if($this.hasClass("cp-app-drive-context-delete")) { if (!APP.loggedIn) { return void deletePaths(paths); } paths.forEach(function (p) { pathsList.push(p.path); }); moveElements(pathsList, [TRASH], false, refresh); } - else if ($(this).hasClass('cp-app-drive-context-deleteowned')) { + else if ($this.hasClass('cp-app-drive-context-deleteowned')) { deleteOwnedPaths(paths); } - else if ($(this).hasClass('cp-app-drive-context-open')) { + else if ($this.hasClass('cp-app-drive-context-open')) { paths.forEach(function (p) { var $element = p.element; $element.click(); $element.dblclick(); }); } - else if ($(this).hasClass('cp-app-drive-context-openro')) { + else if ($this.hasClass('cp-app-drive-context-openro')) { paths.forEach(function (p) { var el = manager.find(p.path); if (paths[0].path[0] === SHARED_FOLDER && APP.newSharedFolder) { @@ -3531,10 +3680,10 @@ define([ openFile(null, href); }); } - else if ($(this).hasClass('cp-app-drive-context-expandall') || - $(this).hasClass('cp-app-drive-context-collapseall')) { + else if ($this.hasClass('cp-app-drive-context-expandall') || + $this.hasClass('cp-app-drive-context-collapseall')) { if (paths.length !== 1) { return; } - var opened = $(this).hasClass('cp-app-drive-context-expandall'); + var opened = $this.hasClass('cp-app-drive-context-expandall'); var openRecursive = function (path) { setFolderOpened(path, opened); var folderContent = manager.find(path); @@ -3557,16 +3706,49 @@ define([ openRecursive(paths[0].path); refresh(); } - else if ($(this).hasClass('cp-app-drive-context-download')) { + + else if ($this.hasClass('cp-app-drive-context-download')) { if (paths.length !== 1) { return; } - el = manager.find(paths[0].path); - data = manager.getFileData(el); - APP.FM.downloadFile(data, function (err, obj) { - console.log(err, obj); - console.log('DONE'); - }); + var path = paths[0]; + el = manager.find(path.path); + // folder + if (manager.isFolder(el)) { + // folder + var name, folderEl; + if (!manager.isSharedFolder(el)) { + name = path.path[path.path.length - 1]; + folderEl = el; + downloadFolder(folderEl, name); + } + // shared folder + else { + data = manager.getSharedFolderData(el); + name = data.title; + folderEl = manager.find(path.path.concat("root")); + downloadFolder(folderEl, name, el); + } + } + // file + else if (manager.isFile(el)) { + // imported file + if (path.element.is(".cp-border-color-file")) { + data = manager.getFileData(el); + APP.FM.downloadFile(data, function (err, obj) { + console.log(err, obj); + console.log('DONE'); + }); + } + // pad + else { + data = manager.getFileData(el); + APP.FM.downloadPad(data, function (err, obj) { + console.log(err, obj); + console.log('DONE'); + }); + } + } } - else if ($(this).hasClass('cp-app-drive-context-share')) { + else if ($this.hasClass('cp-app-drive-context-share')) { if (paths.length !== 1) { return; } el = manager.find(paths[0].path); var parsed, modal; @@ -3617,7 +3799,7 @@ define([ wide: Object.keys(friends).length !== 0 }); } - else if ($(this).hasClass('cp-app-drive-context-newfolder')) { + else if ($this.hasClass('cp-app-drive-context-newfolder')) { if (paths.length !== 1) { return; } var onFolderCreated = function (err, info) { if (err) { return void logError(err); } @@ -3630,21 +3812,21 @@ define([ } manager.addFolder(paths[0].path, null, onFolderCreated); } - else if ($(this).hasClass('cp-app-drive-context-newsharedfolder')) { + else if ($this.hasClass('cp-app-drive-context-newsharedfolder')) { if (paths.length !== 1) { return; } addSharedFolderModal(function (obj) { if (!obj) { return; } manager.addSharedFolder(paths[0].path, obj, refresh); }); } - else if ($(this).hasClass("cp-app-drive-context-newdoc")) { - var ntype = $(this).data('type') || 'pad'; + else if ($this.hasClass("cp-app-drive-context-newdoc")) { + var ntype = $this.data('type') || 'pad'; var path2 = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath; common.sessionStorage.put(Constants.newPadPathKey, path2, function () { common.openURL('/' + ntype + '/'); }); } - else if ($(this).hasClass("cp-app-drive-context-properties")) { + else if ($this.hasClass("cp-app-drive-context-properties")) { if (type === 'trash') { var pPath = paths[0].path; if (paths.length !== 1 || pPath.length !== 4) { return; } @@ -3664,7 +3846,7 @@ define([ UI.alert($prop[0], undefined, true); }); } - else if ($(this).hasClass("cp-app-drive-context-hashtag")) { + else if ($this.hasClass("cp-app-drive-context-hashtag")) { if (paths.length !== 1) { return; } el = manager.find(paths[0].path); data = manager.getFileData(el); @@ -3672,7 +3854,7 @@ define([ var href = data.href || data.roHref; common.updateTags(href); } - else if ($(this).hasClass("cp-app-drive-context-empty")) { + else if ($this.hasClass("cp-app-drive-context-empty")) { if (paths.length !== 1 || !paths[0].element || !manager.comparePath(paths[0].path, [TRASH])) { log(Messages.fm_forbidden); @@ -3683,13 +3865,13 @@ define([ manager.emptyTrash(refresh); }); } - else if ($(this).hasClass("cp-app-drive-context-remove")) { + else if ($this.hasClass("cp-app-drive-context-remove")) { return void deletePaths(paths); } - else if ($(this).hasClass("cp-app-drive-context-removesf")) { + else if ($this.hasClass("cp-app-drive-context-removesf")) { return void deletePaths(paths); } - else if ($(this).hasClass("cp-app-drive-context-restore")) { + else if ($this.hasClass("cp-app-drive-context-restore")) { if (paths.length !== 1) { return; } var restorePath = paths[0].path; var restoreName = paths[0].path[paths[0].path.length - 1]; @@ -3706,21 +3888,20 @@ define([ manager.restore(restorePath, refresh); }); } - else if ($(this).hasClass("cp-app-drive-context-openparent")) { + else if ($this.hasClass("cp-app-drive-context-openparent")) { if (paths.length !== 1) { return; } var parentPath = paths[0].path.slice(); if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; } else { parentPath.pop(); } - el = manager.find(paths[0].path); - APP.selectedFiles = [el]; APP.displayDirectory(parentPath); + APP.selectedFiles = paths[0].path.slice(-1); } APP.hideMenu(); }); - $content.on("keydown", function (e) { - if (e.which === 113) { - var paths = $contextMenu.data('paths'); + $(window).on("keydown", function (e) { + if (e.which === 113) { // if F2 key pressed + var paths = getSelectedPaths(findSelectedElements().first()); if (paths.length !== 1) { return; } displayRenameInput(paths[0].element, paths[0].path); } @@ -3732,10 +3913,9 @@ define([ e.preventDefault(); }); $appContainer.on('mouseup', function (e) { - //if (sel.down) { return; } if (e.which !== 1) { return ; } + if ($(e.target).is(".dropdown-submenu a, .dropdown-submenu a span")) { return; } // if we click on dropdown-submenu, don't close menu APP.hideMenu(e); - //removeSelected(e); }); $appContainer.on('click', function (e) { if (e.which !== 1) { return ; } @@ -3754,7 +3934,7 @@ define([ if (manager.isPathIn(currentPath, [FILES_DATA]) && APP.loggedIn) { return; // We can't remove elements directly from filesData } - var $selected = $('.cp-app-drive-element-selected'); + var $selected = findSelectedElements(); if (!$selected.length) { return; } var paths = []; var isTrash = manager.isPathIn(currentPath, [TRASH]); diff --git a/www/file/app-file.less b/www/file/app-file.less index 20428b382..4ac94ca33 100644 --- a/www/file/app-file.less +++ b/www/file/app-file.less @@ -52,6 +52,16 @@ max-width: 100%; max-height: ~"calc(100vh - 96px)"; } + .plain-text-reader { + align-self: flex-start; + width: 90vw; + height: 100%; + padding: 2em; + background-color: white; + overflow-y: auto; + word-wrap: break-word; + white-space: pre-wrap; + } } #cp-app-file-upload-form, #cp-app-file-download-form { diff --git a/www/settings/app-settings.less b/www/settings/app-settings.less index 0c21bfed0..185b22741 100644 --- a/www/settings/app-settings.less +++ b/www/settings/app-settings.less @@ -111,8 +111,14 @@ vertical-align: middle; margin-right: 5px; } - input[type="color"] { - width: 100px; + .cp-settings-cursor-color-picker { + display: inline-block; + vertical-align: middle; + height: 25px; + width: 70px; + margin-right: 10px; + cursor: pointer; + border: 1px solid black; } .cp-settings-language-selector { button.btn { diff --git a/www/settings/inner.js b/www/settings/inner.js index 37143a617..c084e9dcd 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -15,6 +15,7 @@ define([ '/common/make-backup.js', '/common/common-feedback.js', + '/common/jscolor.js', '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', @@ -1082,18 +1083,9 @@ define([ var exportDrive = function () { Feedback.send('FULL_DRIVE_EXPORT_START'); var todo = function (data, filename) { - var getPad = function (data, cb) { - sframeChan.query("Q_CRYPTGET", data, function (err, obj) { - if (err) { return void cb(err); } - if (obj.error) { return void cb(obj.error); } - cb(null, obj.data); - }, { timeout: 60000 }); - }; - var ui = createExportUI(); - var bu = Backup.create(data, getPad, function (blob, errors) { - console.log(blob); + var bu = Backup.create(data, common.getPad, function (blob, errors) { saveAs(blob, filename); sframeChan.event('EV_CRYPTGET_DISCONNECT'); ui.complete(function () { @@ -1191,13 +1183,13 @@ define([ var $inputBlock = $('
    ').appendTo($div); + var $colorPicker = $("
    ", { class: "cp-settings-cursor-color-picker"}); var $ok = $('', {'class': 'fa fa-check', title: Messages.saved}); var $spinner = $('', {'class': 'fa fa-spinner fa-pulse'}); - var $input = $('', { - type: 'color', - }).on('change', function () { - var val = $input.val(); + // when jscolor picker value change + var onchange = function (colorL) { + var val = "#" + colorL.toString(); if (!/^#[0-9a-fA-F]{6}$/.test(val)) { return; } $spinner.show(); $ok.hide(); @@ -1205,15 +1197,25 @@ define([ $spinner.hide(); $ok.show(); }); - }).appendTo($inputBlock); + }; - $ok.hide().appendTo($inputBlock); - $spinner.hide().appendTo($inputBlock); + // jscolor picker + var jscolorL = new window.jscolor($colorPicker[0],{showOnClick: false, onFineChange: onchange, valueElement:undefined}); + $colorPicker.click(function () { + jscolorL.show(); + }); + // set default color common.getAttribute(['general', 'cursor', 'color'], function (e, val) { if (e) { return void console.error(e); } - $input.val(val || ''); + val = val || "#000"; + jscolorL.fromString(val); }); + + $colorPicker.appendTo($inputBlock); + $ok.hide().appendTo($inputBlock); + $spinner.hide().appendTo($inputBlock); + return $div; }; diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index b0b382314..a90dd4187 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -257,7 +257,7 @@ define([ metadataMgr.onChange(function () { var md = metadataMgr.getMetadata(); if (md.palette) { - updateLocalPalette(md.palette); + updatePalette(md.palette); } });