diff --git a/common/loginpage/src/css/styles.less b/common/loginpage/src/css/styles.less index baba80d0d..a2cbc319d 100644 --- a/common/loginpage/src/css/styles.less +++ b/common/loginpage/src/css/styles.less @@ -386,9 +386,9 @@ li.menu-item { align-items: center; justify-content: space-between; padding-left: 16px; - padding-right: 56px; + padding-right: 88px; .rtl & { - padding-left: 56px; + padding-left: 88px; padding-right: 16px; } } @@ -706,7 +706,7 @@ li.menu-item { justify-content: flex-end; } - .col-more { + .col-more, .col-pin { position: absolute; top: 0; bottom: 0; @@ -752,6 +752,19 @@ li.menu-item { background-color: @background-normal-element-light; } + + &.unavail { + .col-more, .col-pin { + display: none; + } + } + + &:hover:not(.unavail) { + .col-more, .col-pin { + opacity: 1; + } + } + &:hover { .col-more { opacity: 1; @@ -766,6 +779,22 @@ li.menu-item { background-color: @highlight-button-pressed; } + + .col-pin { + right: 44px; + + .rtl & { + right: unset; + left: 44px; + } + } + + &.pinned .col-pin { + opacity: 1; + .icon { + opacity: 1; + } + } &.unavail { > :not(.col-more) { opacity: 0.5; @@ -1018,6 +1047,13 @@ li.menu-item { gap: 8px; .file-list-body { overflow-x: hidden; + .col-pin { + right: 16px; + .rtl & { + right: auto; + left: 16px; + } + } } .win_xp & { @@ -1116,7 +1152,7 @@ li.menu-item { #box-recovery { .file-list-body, .file-list-head { - .col-location, .col-date { + .col-location, .col-date, .col-pin { display: none; } } diff --git a/common/loginpage/src/locale.js b/common/loginpage/src/locale.js index c5d6554ca..5c2d2d28b 100644 --- a/common/loginpage/src/locale.js +++ b/common/loginpage/src/locale.js @@ -56,6 +56,8 @@ l10n.en = { menuRemoveModel: 'Remove from list', menuClear: 'Clear', menuLogout: 'Logout', + menuFilePin: 'Pin', + menuFileUnpin: 'Unpin', textMyComputer: 'My Computer', textThrough: 'through', linkForgotPass: 'Forgot password?', diff --git a/common/loginpage/src/model.js b/common/loginpage/src/model.js index aae0049dc..1691f1869 100644 --- a/common/loginpage/src/model.js +++ b/common/loginpage/src/model.js @@ -204,6 +204,16 @@ Model.prototype.set = function(key, value, opts) { this.events.changed.notify(args); }; +Model.prototype.setMany = function(args, opts) { + for (const [key, value] of Object.entries(args)) { + this[key] = value; + } + + if (!opts || opts.silent !== true) { + this.events.changed.notify(args); + } +}; + Model.prototype.get = function(key) { return this[key]; }; diff --git a/common/loginpage/src/panelfolders.js b/common/loginpage/src/panelfolders.js index a9965830f..9b0ee08df 100644 --- a/common/loginpage/src/panelfolders.js +++ b/common/loginpage/src/panelfolders.js @@ -83,6 +83,11 @@ utils.fn.extend(ControllerFolders.prototype, (function() { var _on_update = function(params) { var _dirs = utils.fn.parseRecent(params, 'folders'), $item; + _dirs.sort((a, b) => { + if (a.pinned && !b.pinned) return -1; + if (!a.pinned && b.pinned) return 1; + return 0; + }); const $listRecentDirs = this.view.$panel.find('.file-list-body'); @@ -92,12 +97,26 @@ if (!utils.getUrlProtocol(dir.full)) { $item = $(app.controller.recent.view.listitemtemplate(dir)); - $item.click({path: dir.full}, e=>{ + if (dir.pinned) { + $item.addClass('pinned'); + } + + $item.find('.col-pin .btn-quick').click(function (e) { + + const folderPath = dir.full; + const newPinState = utils.fn.pinnedFolders(folderPath, 'toggle'); + + $item.toggleClass('pinned', newPinState); + + _on_update.call(this, params); + }.bind(this)); + + $item.click({path: dir.full}, e=>{ openFile(OPEN_FILE_FOLDER, e.data.path); e.preventDefault(); return false; - }); + }); $listRecentDirs.append($item); } diff --git a/common/loginpage/src/panelrecent.js b/common/loginpage/src/panelrecent.js index b6cacc39e..9926a77d7 100644 --- a/common/loginpage/src/panelrecent.js +++ b/common/loginpage/src/panelrecent.js @@ -36,24 +36,26 @@ * panel 'recent' */ -+function(){ 'use strict' - var ControllerRecent = function(args={}) { ++function () { + 'use strict' + var ControllerRecent = function (args = {}) { args.caption = 'Recent files'; args.action = - this.action = "recents"; + this.action = "recents"; this.view = new ViewRecent(args); }; ControllerRecent.prototype = Object.create(baseController.prototype); ControllerRecent.prototype.constructor = ControllerRecent; const isSvgIcons = window.devicePixelRatio >= 2 || window.devicePixelRatio === 1; - var ViewRecent = function(args) { + var ViewRecent = function (args) { var _lang = utils.Lang; // args.id&&(args.id=`"id=${args.id}"`)||(args.id=''); // localStorage.removeItem('welcome'); + //language=HTML const helpLink = `${_lang.textHelpCenter}`; const welcomeBannerTemplate = !localStorage.getItem('welcome') ? ` @@ -70,11 +72,11 @@ - +
${welcomeBannerTemplate}
- +
@@ -114,7 +116,7 @@ ViewRecent.prototype = Object.create(baseView.prototype); ViewRecent.prototype.constructor = ViewRecent; utils.fn.extend(ViewRecent.prototype, { - render: function() { + render: function () { baseView.prototype.render.apply(this, arguments); if (!localStorage.getItem('welcome')) { @@ -125,7 +127,7 @@ this.$boxRecent = this.$panel.find('#box-recent'); this.$panelContainer = this.$panel.find('.recent-panel-container'); }, - listitemtemplate: function(info) { + listitemtemplate: function (info) { let id = !!info.uid ? (` id="${info.uid}"`) : ''; info.crypted === undefined && (info.crypted = false); const dotIndex = info.name.lastIndexOf('.'); @@ -144,7 +146,8 @@
- + ${info.crypted ? ` @@ -158,29 +161,47 @@

- + ${info.descr}
`; + //language=HTML + _tpl += ` +
+ +
`; + if (info.type !== 'folder') { - _tpl += `

${info.date}

`; - _tpl += `
- -
`; + //language=HTML + _tpl += ` +

${info.date}

`; + + //language=HTML + _tpl += ` +
+ +
`; } return _tpl + '
'; }, onscale: function (pasteSvg) { - let elm,icoName, parent, + let elm, icoName, parent, emptylist = $('[class*="text-emptylist"]', '#box-recent'); emptylist.toggleClass('text-emptylist text-emptylist-svg'); - if(pasteSvg && !emptylist.find('svg').length) + if (pasteSvg && !emptylist.find('svg').length) emptylist.prepend($('')); // todo: rewrite cicon rescale @@ -252,26 +273,26 @@ window.ControllerRecent = ControllerRecent; - String.prototype.hashCode = function() { + String.prototype.hashCode = function () { var hash = 0, i, chr; if (this.length === 0) return hash; for (i = this.length; !(--i < 0);) { - chr = this.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; - hash = hash & hash; // Convert to 32bit integer + chr = this.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash = hash & hash; // Convert to 32bit integer } return hash; }; - utils.fn.extend(ControllerRecent.prototype, (function() { + utils.fn.extend(ControllerRecent.prototype, (function () { let collectionRecents, collectionRecovers; let ppmenu; const ITEMS_LOAD_RANGE = 40; const _add_recent_block = function () { - if ( !this.rawRecents || !Object.keys(this.rawRecents).length ) return; + if (!this.rawRecents || !Object.keys(this.rawRecents).length) return; const _raw_block = this.rawRecents.slice(this.recentIndex, this.recentIndex + ITEMS_LOAD_RANGE); const _files = utils.fn.parseRecent(_raw_block); @@ -282,22 +303,22 @@ var model = new FileModel(item); model.set('hash', item.path.hashCode()); - if ( !!this.rawRecents ) { + if (!!this.rawRecents) { collectionRecents.add(model); _check_block[model.get('hash')] = item.path; } else return; } const _new_items_count = Object.keys(_check_block).length; - if ( _new_items_count ) { - if ( this.appready ) { + if (_new_items_count) { + if (this.appready) { sdk.execCommand('files:check', JSON.stringify(_check_block)); } Object.assign(this.check_list, _check_block); } - if ( _new_items_count == ITEMS_LOAD_RANGE ) { + if (_new_items_count === ITEMS_LOAD_RANGE) { setTimeout(e => { this.recentIndex += ITEMS_LOAD_RANGE; _add_recent_block.call(this); @@ -307,7 +328,6 @@ } // this.view.$boxRecent.css('display', collectionRecents.size() > 0 ? 'flex' : 'none'); - // requestAnimationFrame(() => this.view.updateListSize()); if (collectionRecents.size() > 0 || collectionRecovers.size() > 0) { this.dndZone.hide(); @@ -315,7 +335,7 @@ } }; - var _on_recents = function(params) { + var _on_recents = function (params) { this.rawRecents = undefined; setTimeout(e => { @@ -348,6 +368,12 @@ }; function addContextMenuEventListener(collection, model, view, actionList) { + $(`#${model.uid}-pin-btn`, view).click((e) => { + e.stopPropagation(); + const pinned = !model.pinned; + model.setMany({ pinned: pinned, pinid: pinned ? -model.fileid : model.fileid }); + }) + $(`#${model.uid}-more-btn`, view).click((e) => { e.stopPropagation(); @@ -356,6 +382,18 @@ if (m.uid != model.uid) Menu.closeAll(); } + ppmenu.actionlist = actionList; + if (actionList === 'recovery') { + ppmenu.hideItem('files:explore', true); + ppmenu.hideItem('files:pin', true); + ppmenu.hideItem('files:unpin', true); + } else { + ppmenu.hideItem('files:explore', (!model.islocal && !model.dir) || !model.exist); + ppmenu.hideItem(model.pinned ? 'files:pin' : 'files:unpin', true); + ppmenu.hideItem(model.pinned ? 'files:unpin' : 'files:pin', false); + } + + ppmenu.showUnderElem(e.currentTarget, model, $('body').hasClass('rtl') ? 'left' : 'right'); if (!Menu.opened) { ppmenu.actionlist = actionList; @@ -366,6 +404,24 @@ }) } + function handlePin(collection, model) { + let $el = $('#' + model.uid, collection.list); + if ($el.length) { + const f = collection.items.find((elem) => { + return model.pinid <= 0 ? elem.pinid < model.pinid : elem.pinid > model.pinid + }); + + if (f) { + const $item = $('#' + f.uid, collection.list); + $el.insertBefore($item); + } else if (!f && model.pinid > 0) { + $el.appendTo(collection.list); + } else { + $el.prependTo(collection.list); + } + } + } + function _init_collections() { let _cl_rcbox = this.view.$boxRecent, _cl_rvbox = this.view.$boxRecovery; @@ -380,9 +436,20 @@ }); collectionRecents.events.inserted.attach((collection, model) => { - let $item = this.view.listitemtemplate(model); + let $item = $(this.view.listitemtemplate(model)); - collection.list.append($item); + if (model.pinned) { + const $pinned = collection.list.children('.row.pinned'); + if ($pinned.length) { + $item.insertAfter($pinned.last()); + } else { + $item.prependTo(collection.list); + } + } else { + collection.list.append($item); + } + + $item[model.pinned ? 'addClass' : 'removeClass']('pinned'); addContextMenuEventListener(collection, model, this.view.$panel, 'recent'); @@ -392,21 +459,33 @@ collectionRecents.events.click.attach((collection, model) => { // var _portal = model.descr; // if ( !model.islocal && !app.controller.portals.isConnected(_portal) ) { - // app.controller.portals.authorizeOn(_portal, {type: 'fileid', id: model.fileid}); + // app.controller.portals.authorizeOn(_portal, {type: 'fileid', id: model.fileid}); // } else { - openFile(OPEN_FILE_RECENT, model); + openFile(OPEN_FILE_RECENT, model); // } }); - collectionRecents.events.contextmenu.attach(function(collection, model, e){ + collectionRecents.events.contextmenu.attach(function (collection, model, e) { ppmenu.actionlist = 'recent'; ppmenu.hideItem('files:explore', (!model.islocal && !model.dir) || !model.exist); + ppmenu.hideItem(model.pinned ? 'files:pin' : 'files:unpin', true); + ppmenu.hideItem(model.pinned ? 'files:unpin' : 'files:pin', false); ppmenu.show({left: e.clientX, top: e.clientY}, model); }); - collectionRecents.events.changed.attach(function(collection, model){ + collectionRecents.events.changed.attach(function (collection, model, property) { let $el = collection.list.find('#' + model.uid); - if ( $el ) $el[model.exist ? 'removeClass' : 'addClass']('unavail'); + if ($el) { + $el[model.exist ? 'removeClass' : 'addClass']('unavail'); + if (property['pinned'] !== undefined) { + sdk.setRecentFilePinned(model.get('fileid'), property['pinned']); + $el[model.pinned ? 'addClass' : 'removeClass']('pinned'); + } + + if (property.pinid != undefined) { + handlePin(collection, model); + } + } }); collectionRecents.empty(); @@ -417,16 +496,18 @@ view: _cl_rvbox, list: _cl_rvbox.find('.file-list-body') }); - collectionRecovers.events.inserted.attach((collection, model)=>{ - collection.list.append( this.view.listitemtemplate(model) ); + collectionRecovers.events.inserted.attach((collection, model) => { + collection.list.append(this.view.listitemtemplate(model)); addContextMenuEventListener(collection, model, this.view.$panel, 'recovery'); }); - collectionRecovers.events.click.attach((collection, model)=>{ + collectionRecovers.events.click.attach((collection, model) => { openFile(OPEN_FILE_RECOVERY, model); }); - collectionRecovers.events.contextmenu.attach((collection, model, e)=>{ + collectionRecovers.events.contextmenu.attach((collection, model, e) => { ppmenu.actionlist = 'recovery'; ppmenu.hideItem('files:explore', true); + ppmenu.hideItem('files:pin', true); + ppmenu.hideItem('files:unpin', true); ppmenu.show({left: e.clientX, top: e.clientY}, model); }); }; @@ -437,11 +518,13 @@ className: 'with-icons', bottomlimitoffset: 10, items: [ - { caption: utils.Lang.menuFileOpen, action: 'files:open' , icon: '#folder'}, - { caption: utils.Lang.menuFileExplore, action: 'files:explore', icon: '#gofolder' }, - { caption: utils.Lang.menuRemoveModel, action: 'files:forget', icon: '#remove' }, - { caption: '--' }, - { caption: utils.Lang.menuClear, action: 'files:clear', variant: 'negative' } + {caption: utils.Lang.menuFileOpen, action: 'files:open', icon: '#folder'}, + {caption: utils.Lang.menuFilePin, action: 'files:pin', icon: '#pin20'}, + {caption: utils.Lang.menuFileUnpin, action: 'files:unpin', icon: '#unpin20'}, + {caption: utils.Lang.menuFileExplore, action: 'files:explore', icon: '#gofolder'}, + {caption: utils.Lang.menuRemoveModel, action: 'files:forget', icon: '#remove'}, + {caption: '--'}, + {caption: utils.Lang.menuClear, action: 'files:clear', variant: 'negative'} ] }); @@ -454,6 +537,16 @@ menu.actionlist == 'recent' ? openFile(OPEN_FILE_RECENT, data) : openFile(OPEN_FILE_RECOVERY, data); + } else if (/\:pin/.test(action)) { + const targetModel = collectionRecents.find('uid', data.uid); + if (targetModel) { + targetModel.setMany({ pinned: true, pinid: -targetModel.fileid }); + } + } else if (/\:unpin/.test(action)) { + const targetModel = collectionRecents.find('uid', data.uid); + if (targetModel) { + targetModel.setMany({ pinned: false, pinid: targetModel.fileid }); + } } else if (/\:clear/.test(action)) { if (menu.actionlist === 'recent') { window.sdk.LocalFileRemoveAllRecents(); @@ -466,8 +559,7 @@ this.dndZone.show(); } } - } else - if (/\:forget/.test(action)) { + } else if (/\:forget/.test(action)) { $('#' + data.uid, this.view.$panel).addClass('lost'); const count = collectionRecovers.size() + collectionRecents.size(); @@ -480,10 +572,13 @@ if ( !(count > 1) ) { this.dndZone.show(); } - } else - if (/\:explore/.test(action)) { + } else if (/\:explore/.test(action)) { if (menu.actionlist == 'recent') { - sdk.execCommand('files:explore', JSON.stringify({path: data.path, id: data.fileid, hash: data.hash})); + sdk.execCommand('files:explore', JSON.stringify({ + path: data.path, + id: data.fileid, + hash: data.hash + })); } } }; @@ -493,7 +588,7 @@ console.log('on recents filter', e.target.value) const _filter = e.target.value; - if ( !_filter.length ) { + if (!_filter.length) { $('.table-files tr.hidden', this.view.$panel).removeClass('hidden') collectionRecents.items.forEach(model => model.set('hidden', false)); @@ -501,11 +596,10 @@ const re = new RegExp(_filter, "gi"); collectionRecents.items.forEach(model => { const _path = model.get('path'); - if ( !re.test(_path) ) { + if (!re.test(_path)) { $('#' + model.uid, this.view.$panel).addClass('hidden'); model.set('hidden', true); - } else - if ( model.get('hidden') ) { + } else if (model.get('hidden')) { $('#' + model.uid, this.view.$panel).removeClass('hidden'); model.set('hidden', false); } @@ -515,7 +609,7 @@ return { - init: function() { + init: function () { baseController.prototype.init.apply(this, arguments); this.view.render(); @@ -526,25 +620,23 @@ window.sdk.on('onupdaterecents', _on_recents.bind(this)); window.sdk.on('onupdaterecovers', _on_recovers.bind(this)); - window.sdk.on('on_native_message', (cmd, param)=>{ + window.sdk.on('on_native_message', (cmd, param) => { if (/files:checked/.test(cmd)) { let fobjs = JSON.parse(param); - if ( fobjs ) { + if (fobjs) { for (let obj in fobjs) { let value = JSON.parse(fobjs[obj]); let model = collectionRecents.find('hash', parseInt(obj)); - if ( model ) { + if (model) { model.get('exist') != value && model.set('exist', value); } } } - } else - if (/file\:skip/.test(cmd)) { + } else if (/file\:skip/.test(cmd)) { sdk.LocalFileRemoveRecent(parseInt(param)); - } else - if (/app\:ready/.test(cmd)) { - if ( Object.keys(this.check_list).length ) { - setTimeout(()=>{ + } else if (/app\:ready/.test(cmd)) { + if (Object.keys(this.check_list).length) { + setTimeout(() => { sdk.execCommand('files:check', JSON.stringify(this.check_list)); }, 100); } @@ -553,11 +645,9 @@ } }); - // $(window).resize(() => requestAnimationFrame(() => this.view.updateListSize())); - CommonEvents.on("icons:svg", this.view.onscale); - CommonEvents.on('portal:authorized', (data)=>{ - if ( data.type == 'fileid' ) { + CommonEvents.on('portal:authorized', (data) => { + if (data.type == 'fileid') { let fileid = data.id; // openFile(OPEN_FILE_RECENT, fileid); } @@ -632,10 +722,10 @@ return this; }, - getRecents: function() { + getRecents: function () { return collectionRecents; }, - getRecovers: function() { + getRecovers: function () { return collectionRecovers; } }; diff --git a/common/loginpage/src/panels.js b/common/loginpage/src/panels.js index a40a36067..e546e2be7 100644 --- a/common/loginpage/src/panels.js +++ b/common/loginpage/src/panels.js @@ -194,12 +194,14 @@ $(document).ready(function() { function onActionClick(e) { var $el = $(this); var action = $el.attr('action'); + const pinned = JSON.parse(localStorage.getItem('pinnedFolders') || '[]'); if (/^custom/.test(action)) return; if (action == 'open' && !app.controller.recent.getRecents().size() && - !app.controller.recent.getRecovers().size()) + !app.controller.recent.getRecovers().size() && + !pinned.length) { openFile(OPEN_FILE_FOLDER, ''); } else { diff --git a/common/loginpage/src/utils.js b/common/loginpage/src/utils.js index 1556453af..c7c87def2 100644 --- a/common/loginpage/src/utils.js +++ b/common/loginpage/src/utils.js @@ -385,6 +385,20 @@ utils.fn.extend = function(dest, src) { return dest; }; +utils.fn.pinnedFolders = function(path, action) { + const key = 'pinnedFolders'; + let pinned = JSON.parse(localStorage.getItem(key) || '[]'); + + if (action === 'check') return pinned.includes(path); + + const i = pinned.indexOf(path); + if (action === 'toggle') { + i === -1 ? pinned.push(path) : pinned.splice(i, 1); + } + + localStorage.setItem(key, JSON.stringify(pinned)); +}; + utils.fn.parseRecent = function(arr, out = 'files') { var _files_arr = [], _dirs_arr = []; @@ -401,6 +415,7 @@ utils.fn.parseRecent = function(arr, out = 'files') { _files_arr.push({ fileid: _f_.id, + pinid: !_f_.pin ? _f_.id : -_f_.id, type: _f_.type, format: utils.parseFileFormat(_f_.type), name: name, @@ -408,6 +423,7 @@ utils.fn.parseRecent = function(arr, out = 'files') { date: _f_.modifyed, path: $('
').html(fn).text(), cloud: _f_.cloud, + pinned: _f_.pin, }); _dirs_arr.indexOf(path) < 0 && _dirs_arr.push(path); @@ -416,6 +432,13 @@ utils.fn.parseRecent = function(arr, out = 'files') { if (out == 'files') return _files_arr; + const pinned = JSON.parse(localStorage.getItem('pinnedFolders') || '[]'); + for (const pinnedPath of pinned) { + if (!_dirs_arr.includes(pinnedPath)) { + _dirs_arr.push(pinnedPath); + } + } + var out_dirs_arr = []; for (let _d_ of _dirs_arr) { let name = (!_is_win ? /([^/]+)$/ : /([^\\/]+)$/).exec(_d_)[1], @@ -426,11 +449,17 @@ utils.fn.parseRecent = function(arr, out = 'files') { } else parent = _d_.slice(0, _d_.length - name.length - 1); + let pinned = utils.fn.pinnedFolders(_d_, 'check'); + let id = _d_.hashCode(); + out_dirs_arr.push({ type: 'folder', full: _d_, name: name, - descr: parent + descr: parent, + pinid: !pinned ? id : -id, + pinned: pinned, + uid: `folder-${id}` }); }