From a5bb66f4a723bce5c5fbe919a48cd5133204ef62 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 18 Nov 2014 18:53:45 +0100 Subject: Added favorites feature to the files app --- apps/files/js/app.js | 2 + apps/files/js/favoritesfilelist.js | 99 +++++++++++++++++++++++++++ apps/files/js/favoritesplugin.js | 116 +++++++++++++++++++++++++++++++ apps/files/js/tagsplugin.js | 135 +++++++++++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 apps/files/js/favoritesfilelist.js create mode 100644 apps/files/js/favoritesplugin.js create mode 100644 apps/files/js/tagsplugin.js (limited to 'apps/files/js') diff --git a/apps/files/js/app.js b/apps/files/js/app.js index ee5330485e7..adb1893bb0e 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -80,6 +80,8 @@ // refer to the one of the "files" view window.FileList = this.fileList; + OC.Plugins.attach('OCA.Files.App', this); + this._setupEvents(); // trigger URL change event handlers this._onPopState(urlParams); diff --git a/apps/files/js/favoritesfilelist.js b/apps/files/js/favoritesfilelist.js new file mode 100644 index 00000000000..0d555ce609d --- /dev/null +++ b/apps/files/js/favoritesfilelist.js @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2014 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +// HACK: this piece needs to be loaded AFTER the files app (for unit tests) +$(document).ready(function() { + (function(OCA) { + /** + * @class OCA.Files.FavoritesFileList + * @augments OCA.Files.FavoritesFileList + * + * @classdesc Favorites file list. + * Displays the list of files marked as favorites + * + * @param $el container element with existing markup for the #controls + * and a table + * @param [options] map of options, see other parameters + */ + var FavoritesFileList = function($el, options) { + this.initialize($el, options); + }; + FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype, + /** @lends OCA.Files.FavoritesFileList.prototype */ { + id: 'favorites', + appName: 'Favorites', + + _clientSideSort: true, + _allowSelection: false, + + /** + * @private + */ + initialize: function($el, options) { + OCA.Files.FileList.prototype.initialize.apply(this, arguments); + if (this.initialized) { + return; + } + OC.Plugins.attach('OCA.Files.FavoritesFileList', this); + }, + + updateEmptyContent: function() { + var dir = this.getCurrentDirectory(); + if (dir === '/') { + // root has special permissions + this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty); + this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty); + } + else { + OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); + } + }, + + getDirectoryPermissions: function() { + return OC.PERMISSION_READ | OC.PERMISSION_DELETE; + }, + + updateStorageStatistics: function() { + // no op because it doesn't have + // storage info like free space / used space + }, + + reload: function() { + var tagName = OC.TAG_FAVORITE; + this.showMask(); + if (this._reloadCall) { + this._reloadCall.abort(); + } + this._reloadCall = $.ajax({ + url: OC.generateUrl('/apps/files/api/v1/tags/{tagName}/files', {tagName: tagName}), + type: 'GET', + dataType: 'json' + }); + var callBack = this.reloadCallback.bind(this); + return this._reloadCall.then(callBack, callBack); + }, + + reloadCallback: function(result) { + delete this._reloadCall; + this.hideMask(); + + if (result.files) { + this.setFiles(result.files.sort(this._sortComparator)); + } + else { + // TODO: error handling + } + } + }); + + OCA.Files.FavoritesFileList = FavoritesFileList; + })(OCA); +}); + diff --git a/apps/files/js/favoritesplugin.js b/apps/files/js/favoritesplugin.js new file mode 100644 index 00000000000..417a32ef804 --- /dev/null +++ b/apps/files/js/favoritesplugin.js @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2014 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OCA) { + /** + * @namespace OCA.Files.FavoritesPlugin + * + * Registers the favorites file list from the files app sidebar. + */ + OCA.Files.FavoritesPlugin = { + name: 'Favorites', + + /** + * @type OCA.Files.FavoritesFileList + */ + favoritesFileList: null, + + attach: function() { + var self = this; + $('#app-content-favorites').on('show.plugin-favorites', function(e) { + self.showFileList($(e.target)); + }); + $('#app-content-favorites').on('hide.plugin-favorites', function() { + self.hideFileList(); + }); + }, + + detach: function() { + if (this.favoritesFileList) { + this.favoritesFileList.destroy(); + OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated); + OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated); + $('#app-content-favorites').off('.plugin-favorites'); + this.favoritesFileList = null; + } + }, + + showFileList: function($el) { + if (!this.favoritesFileList) { + this.favoritesFileList = this._createFavoritesFileList($el); + } + return this.favoritesFileList; + }, + + hideFileList: function() { + if (this.favoritesFileList) { + this.favoritesFileList.$fileList.empty(); + } + }, + + /** + * Creates the favorites file list. + * + * @param $el container for the file list + * @return {OCA.Files.FavoritesFileList} file list + */ + _createFavoritesFileList: function($el) { + var fileActions = this._createFileActions(); + // register favorite list for sidebar section + return new OCA.Files.FavoritesFileList( + $el, { + fileActions: fileActions, + scrollContainer: $('#app-content') + } + ); + }, + + _createFileActions: function() { + // inherit file actions from the files app + var fileActions = new OCA.Files.FileActions(); + // note: not merging the legacy actions because legacy apps are not + // compatible with the sharing overview and need to be adapted first + fileActions.registerDefaultActions(); + fileActions.merge(OCA.Files.fileActions); + + if (!this._globalActionsInitialized) { + // in case actions are registered later + this._onActionsUpdated = _.bind(this._onActionsUpdated, this); + OCA.Files.fileActions.on('setDefault.plugin-favorites', this._onActionsUpdated); + OCA.Files.fileActions.on('registerAction.plugin-favorites', this._onActionsUpdated); + this._globalActionsInitialized = true; + } + + // when the user clicks on a folder, redirect to the corresponding + // folder in the files app instead of opening it directly + fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { + OCA.Files.App.setActiveView('files', {silent: true}); + OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true); + }); + fileActions.setDefault('dir', 'Open'); + return fileActions; + }, + + _onActionsUpdated: function(ev) { + if (ev.action) { + this.favoritesFileList.fileActions.registerAction(ev.action); + } else if (ev.defaultAction) { + this.favoritesFileList.fileActions.setDefault( + ev.defaultAction.mime, + ev.defaultAction.name + ); + } + } + }; + +})(OCA); + +OC.Plugins.register('OCA.Files.App', OCA.Files.FavoritesPlugin); + diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js new file mode 100644 index 00000000000..77b3167ab5e --- /dev/null +++ b/apps/files/js/tagsplugin.js @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2014 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +(function(OCA) { + + OCA.Files = OCA.Files || {}; + + /** + * @namespace OCA.Files.TagsPlugin + * + * Extends the file actions and file list to include a favorite action icon + * and addition "data-tags" and "data-favorite" attributes. + */ + OCA.Files.TagsPlugin = { + name: 'Tags', + + allowedLists: [ + 'files', + 'favorites' + ], + + _extendFileActions: function(fileActions) { + var self = this; + // register "star" action + fileActions.registerAction({ + name: 'favorite', + displayName: 'Favorite', + mime: 'all', + permissions: OC.PERMISSION_READ, + render: function(actionSpec, isDefault, context) { + // TODO: use proper icon + var $file = context.$file; + var isFavorite = $file.data('favorite') === true; + var starState = isFavorite ? '★' : '☆'; + var $icon = $( + '' + + starState + '' + ); + $file.find('td:first>.favorite').prepend($icon); + return $icon; + }, + actionHandler: function(fileName, context) { + var $actionEl = context.$file.find('.action-favorite'); + var $file = context.$file; + var dir = context.dir || context.fileList.getCurrentDirectory(); + var tags = $file.attr('data-tags'); + if (_.isUndefined(tags)) { + tags = ''; + } + tags = tags.split('|'); + tags = _.without(tags, ''); + var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0; + if (isFavorite) { + // remove tag from list + tags = _.without(tags, OC.TAG_FAVORITE); + } else { + tags.push(OC.TAG_FAVORITE); + } + if ($actionEl.hasClass('icon-loading')) { + // do nothing + return; + } + $actionEl.addClass('icon-loading permanent'); + self.applyFileTags( + dir + '/' + fileName, + tags + ).then(function() { + // TODO: read from result + $actionEl.removeClass('icon-loading'); + $actionEl.html(isFavorite ? '☆' : '★'); + $actionEl.toggleClass('permanent', !isFavorite); + $file.attr('data-tags', tags.join('|')); + $file.attr('data-favorite', !isFavorite); + }); + } + }); + }, + + _extendFileList: function(fileList) { + // extend row prototype + fileList.$fileList.addClass('has-favorites'); + var oldCreateRow = fileList._createRow; + fileList._createRow = function(fileData) { + var $tr = oldCreateRow.apply(this, arguments); + if (fileData.tags) { + $tr.attr('data-tags', fileData.tags.join('|')); + if (fileData.tags.indexOf(OC.TAG_FAVORITE) >= 0) { + $tr.attr('data-favorite', true); + } + } + $tr.find('td:first').prepend('
'); + return $tr; + }; + }, + + attach: function(fileList) { + if (this.allowedLists.indexOf(fileList.id) < 0) { + return; + } + this._extendFileActions(fileList.fileActions); + this._extendFileList(fileList); + }, + + /** + * Replaces the given files' tags with the specified ones. + * + * @param {String} fileName path to the file or folder to tag + * @param {Array.} tagNames array of tag names + */ + applyFileTags: function(fileName, tagNames) { + var encodedPath = OC.encodePath(fileName); + while (encodedPath[0] === '/') { + encodedPath = encodedPath.substr(1); + } + return $.ajax({ + url: OC.generateUrl('/apps/files/api/v1/files/') + encodedPath, + contentType: 'application/json', + data: JSON.stringify({ + tags: tagNames || [] + }), + dataType: 'json', + type: 'POST' + }); + } + }; +})(OCA); + +OC.Plugins.register('OCA.Files.FileList', OCA.Files.TagsPlugin); + -- cgit v1.2.3 From dfe922b72aad25ad478365ade6deecb063de0f59 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 12 Dec 2014 17:06:57 +0100 Subject: Correctly replace favorites icon when re-rendering file actions --- apps/files/js/tagsplugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/files/js') diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 77b3167ab5e..f75c7d49f77 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -42,7 +42,7 @@ '' + starState + '' ); - $file.find('td:first>.favorite').prepend($icon); + $file.find('td:first>.favorite').replaceWith($icon); return $icon; }, actionHandler: function(fileName, context) { -- cgit v1.2.3 From 1d22cd5b229753bf35a78bc342a6b6e644971aed Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 15 Dec 2014 12:43:16 +0100 Subject: Use star icon for favorites --- apps/files/index.php | 2 ++ apps/files/js/tagsplugin.js | 52 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 10 deletions(-) (limited to 'apps/files/js') diff --git a/apps/files/index.php b/apps/files/index.php index 86cf2e04a56..6f5989820b9 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -43,6 +43,8 @@ OCP\Util::addscript('files', 'filelist'); \OCP\Util::addScript('files', 'tagsplugin'); \OCP\Util::addScript('files', 'favoritesplugin'); +\OC_Util::addVendorScript('core', 'handlebars/handlebars'); + OCP\App::setActiveNavigationEntry('files_index'); $l = \OC::$server->getL10N('files'); diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index f75c7d49f77..cfde8bc6304 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -7,8 +7,44 @@ * See the COPYING-README file. * */ + +/* global Handlebars */ + (function(OCA) { + var TEMPLATE_FAVORITE_ACTION = + '' + + '{{altText}}' + + ''; + + /** + * Returns the path to the star image + * + * @param {boolean} state true if starred, false otherwise + * @return {string} path to star image + */ + function getStarImage(state) { + return OC.imagePath('core', state ? 'actions/starred' : 'actions/star'); + } + + /** + * Render the star icon with the given state + * + * @param {boolean} state true if starred, false otherwise + * @return {Object} jQuery object + */ + function renderStar(state) { + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE_FAVORITE_ACTION); + } + return this._template({ + isFavorite: state, + altText: state ? t('core', 'Favorited') : t('core', 'Favorite'), + imgFile: getStarImage(state) + }); + } + OCA.Files = OCA.Files || {}; /** @@ -34,14 +70,9 @@ mime: 'all', permissions: OC.PERMISSION_READ, render: function(actionSpec, isDefault, context) { - // TODO: use proper icon var $file = context.$file; var isFavorite = $file.data('favorite') === true; - var starState = isFavorite ? '★' : '☆'; - var $icon = $( - '' + - starState + '' - ); + var $icon = $(renderStar(isFavorite)); $file.find('td:first>.favorite').replaceWith($icon); return $icon; }, @@ -70,11 +101,12 @@ self.applyFileTags( dir + '/' + fileName, tags - ).then(function() { - // TODO: read from result + ).then(function(result) { + // read latest state from result + var isFavorite = (result.tags.indexOf(OC.TAG_FAVORITE) >= 0); $actionEl.removeClass('icon-loading'); - $actionEl.html(isFavorite ? '☆' : '★'); - $actionEl.toggleClass('permanent', !isFavorite); + $actionEl.find('img').attr('src', getStarImage(isFavorite)); + $actionEl.toggleClass('permanent', isFavorite); $file.attr('data-tags', tags.join('|')); $file.attr('data-favorite', !isFavorite); }); -- cgit v1.2.3 From 33eb4483b297d12b7cb76e5a3df25f2d641a96cb Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 15 Dec 2014 15:18:29 +0100 Subject: Fixed has-favorites CSS --- apps/files/css/files.css | 2 +- apps/files/js/tagsplugin.js | 2 +- apps/files/tests/js/tagspluginspec.js | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) (limited to 'apps/files/js') diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 7d482ccbb7a..3829759a14e 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -291,7 +291,7 @@ table td.filename .nametext { max-width: 800px; height: 100%; } -#fileList.has-favorites td.filename a.name { +.has-favorites #fileList td.filename a.name { left: 50px; margin-right: 50px; } diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index cfde8bc6304..b7bbb6daf54 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -116,7 +116,7 @@ _extendFileList: function(fileList) { // extend row prototype - fileList.$fileList.addClass('has-favorites'); + fileList.$el.addClass('has-favorites'); var oldCreateRow = fileList._createRow; fileList._createRow = function(fileData) { var $tr = oldCreateRow.apply(this, arguments); diff --git a/apps/files/tests/js/tagspluginspec.js b/apps/files/tests/js/tagspluginspec.js index 424017a9dc6..66240575a5c 100644 --- a/apps/files/tests/js/tagspluginspec.js +++ b/apps/files/tests/js/tagspluginspec.js @@ -71,6 +71,9 @@ describe('OCA.Files.TagsPlugin tests', function() { expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', OC.TAG_FAVORITE]); expect($tr.attr('data-favorite')).toEqual('true'); }); + it('adds has-favorites class on table', function() { + expect(fileList.$el.hasClass('has-favorites')).toEqual(true); + }); }); describe('Applying tags', function() { it('sends request to server and updates icon', function() { -- cgit v1.2.3 From 976baed5f9dec891fedc4756fa272dd5a0f4eae2 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 15 Dec 2014 15:50:57 +0100 Subject: Remove spinner when toggling file favorite --- apps/files/js/tagsplugin.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'apps/files/js') diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index b7bbb6daf54..550026e3b48 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -45,6 +45,17 @@ }); } + /** + * Toggle star icon on action element + * + * @param {Object} action element + * @param {boolean} state true if starred, false otherwise + */ + function toggleStar($actionEl, state) { + $actionEl.find('img').attr('src', getStarImage(state)); + $actionEl.toggleClass('permanent', state); + } + OCA.Files = OCA.Files || {}; /** @@ -93,20 +104,14 @@ } else { tags.push(OC.TAG_FAVORITE); } - if ($actionEl.hasClass('icon-loading')) { - // do nothing - return; - } - $actionEl.addClass('icon-loading permanent'); + toggleStar($actionEl, !isFavorite); + self.applyFileTags( dir + '/' + fileName, tags ).then(function(result) { // read latest state from result - var isFavorite = (result.tags.indexOf(OC.TAG_FAVORITE) >= 0); - $actionEl.removeClass('icon-loading'); - $actionEl.find('img').attr('src', getStarImage(isFavorite)); - $actionEl.toggleClass('permanent', isFavorite); + toggleStar($actionEl, (result.tags.indexOf(OC.TAG_FAVORITE) >= 0)); $file.attr('data-tags', tags.join('|')); $file.attr('data-favorite', !isFavorite); }); -- cgit v1.2.3 From 207d77e5cdf6386dd22d87a5851adae38ad9f77f Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 15 Dec 2014 17:20:41 +0100 Subject: Fixed small code style issues --- apps/files/appinfo/application.php | 6 +++--- apps/files/controller/apicontroller.php | 5 +++++ apps/files/index.php | 10 +++++----- apps/files/js/tagsplugin.js | 4 ++-- apps/files/simplelist.php | 3 +-- 5 files changed, 16 insertions(+), 12 deletions(-) (limited to 'apps/files/js') diff --git a/apps/files/appinfo/application.php b/apps/files/appinfo/application.php index fcf974a701b..13ff60daf89 100644 --- a/apps/files/appinfo/application.php +++ b/apps/files/appinfo/application.php @@ -31,9 +31,9 @@ class Application extends App { ); }); - /** - * Core - */ + /** + * Core + */ $container->registerService('L10N', function(IContainer $c) { return $c->query('ServerContainer')->getL10N($c->query('AppName')); }); diff --git a/apps/files/controller/apicontroller.php b/apps/files/controller/apicontroller.php index 1990971438b..902731a0492 100644 --- a/apps/files/controller/apicontroller.php +++ b/apps/files/controller/apicontroller.php @@ -19,6 +19,11 @@ use OCA\Files\Service\TagService; class ApiController extends Controller { + /** + * @var TagService $tagService + */ + private $tagService; + public function __construct($appName, IRequest $request, TagService $tagService){ parent::__construct($appName, $request); $this->tagService = $tagService; diff --git a/apps/files/index.php b/apps/files/index.php index 6f5989820b9..02076226c1a 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -90,11 +90,11 @@ function sortNavigationItems($item1, $item2) { \OCA\Files\App::getNavigationManager()->add( array( - "id" => 'favorites', - "appname" => 'files', - "script" => 'simplelist.php', - "order" => 50, - "name" => $l->t('Favorites') + 'id' => 'favorites', + 'appname' => 'files', + 'script' => 'simplelist.php', + 'order' => 50, + 'name' => $l->t('Favorites') ) ); diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 550026e3b48..a6757431ffa 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -12,7 +12,7 @@ (function(OCA) { - var TEMPLATE_FAVORITE_ACTION = + var TEMPLATE_FAVORITE_ACTION = '' + '{{altText}}' + @@ -30,7 +30,7 @@ /** * Render the star icon with the given state - * + * * @param {boolean} state true if starred, false otherwise * @return {Object} jQuery object */ diff --git a/apps/files/simplelist.php b/apps/files/simplelist.php index e3fdf6cd1de..53e56b4ed32 100644 --- a/apps/files/simplelist.php +++ b/apps/files/simplelist.php @@ -21,8 +21,7 @@ * */ -// Check if we are a user -OCP\User::checkLoggedIn(); +// TODO: move to handlebars // renders the controls and table headers template $tmpl = new OCP\Template('files', 'simplelist', ''); -- cgit v1.2.3