diff options
-rw-r--r-- | apps/files/appinfo/routes.php | 60 | ||||
-rw-r--r-- | apps/files/js/app.js | 8 | ||||
-rw-r--r-- | apps/files/js/navigation.js | 257 | ||||
-rw-r--r-- | apps/files/js/tagsplugin.js | 134 | ||||
-rw-r--r-- | apps/files/lib/AppInfo/Application.php | 4 | ||||
-rw-r--r-- | apps/files/lib/Controller/ApiController.php | 175 | ||||
-rw-r--r-- | apps/files/lib/Controller/ViewController.php | 83 | ||||
-rw-r--r-- | apps/files/templates/appnavigation.php | 116 | ||||
-rw-r--r-- | apps/files/tests/Controller/ViewControllerTest.php | 22 | ||||
-rw-r--r-- | apps/files/tests/js/appSpec.js | 24 | ||||
-rw-r--r-- | core/css/apps.scss | 17 |
11 files changed, 781 insertions, 119 deletions
diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 0d1449ff355..a9d8ba0a1b9 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -76,6 +76,66 @@ $application->registerRoutes( 'url' => '/ajax/getstoragestats.php', 'verb' => 'GET', ], + [ + 'name' => 'API#showQuickAccess', + 'url' => '/api/v1/quickaccess/set/showList', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getShowQuickAccess', + 'url' => '/api/v1/quickaccess/get/showList', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getShowQuickaccessSettings', + 'url' => '/api/v1/quickaccess/showsettings', + 'verb' => 'GET', + ], + [ + 'name' => 'API#setShowQuickaccessSettings', + 'url' => '/api/v1/quickaccess/set/showsettings', + 'verb' => 'GET', + ], + [ + 'name' => 'API#setSortingStrategy', + 'url' => '/api/v1/quickaccess/set/SortingStrategy', + 'verb' => 'GET', + ], + [ + 'name' => 'API#setReverseQuickaccess', + 'url' => '/api/v1/quickaccess/set/ReverseList', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getSortingStrategy', + 'url' => '/api/v1/quickaccess/get/SortingStrategy', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getReverseQuickaccess', + 'url' => '/api/v1/quickaccess/get/ReverseList', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getFavoritesFolder', + 'url' => '/api/v1/quickaccess/get/FavoriteFolders/', + 'verb' => 'GET' + ], + [ + 'name' => 'API#setSortingOrder', + 'url' => '/api/v1/quickaccess/set/CustomSortingOrder', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getSortingOrder', + 'url' => '/api/v1/quickaccess/get/CustomSortingOrder', + 'verb' => 'GET', + ], + [ + 'name' => 'API#getNodeType', + 'url' => '/api/v1/quickaccess/get/NodeType', + 'verb' => 'GET', + ], ] ] ); diff --git a/apps/files/js/app.js b/apps/files/js/app.js index 6a21bce975b..52c92645b2d 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -53,6 +53,8 @@ this.$showHiddenFiles = $('input#showhiddenfilesToggle'); var showHidden = $('#showHiddenFiles').val() === "1"; this.$showHiddenFiles.prop('checked', showHidden); + + if ($('#fileNotFound').val() === "1") { OC.Notification.show(t('files', 'File could not be found'), {type: 'error'}); } @@ -219,7 +221,7 @@ }, /** - * Persist show hidden preference on ther server + * Persist show hidden preference on the server * * @returns {undefined} */ @@ -237,8 +239,8 @@ var params; if (e && e.itemId) { params = { - view: e.itemId, - dir: '/' + view: typeof e.view === 'string' && e.view !== '' ? e.view : e.itemId, + dir: e.dir ? e.dir : '/' }; this._changeUrl(params.view, params.dir); OC.Apps.hideAppSidebar($('.detailsView')); diff --git a/apps/files/js/navigation.js b/apps/files/js/navigation.js index d213d0467b6..d4fa06cb45e 100644 --- a/apps/files/js/navigation.js +++ b/apps/files/js/navigation.js @@ -1,8 +1,9 @@ /* - * Copyright (c) 2014 + * @Copyright 2014 Vincent Petry <pvince81@owncloud.com> * * @author Vincent Petry - * @copyright 2014 Vincent Petry <pvince81@owncloud.com> + * @author Felix Nüsse <felix.nuesse@t-online.de> + * * * This file is licensed under the Affero General Public License version 3 * or later. @@ -11,7 +12,7 @@ * */ -(function() { +(function () { /** * @class OCA.Files.Navigation @@ -19,7 +20,7 @@ * * @param $el element containing the navigation */ - var Navigation = function($el) { + var Navigation = function ($el) { this.initialize($el); }; @@ -39,23 +40,47 @@ $currentContent: null, /** + * Strategy by which the quickaccesslist is sorted + * + * Possible Strategies: + * customorder + * datemodified + * date + * alphabet + * + */ + $sortingStrategy: 'alphabet', + + /** + * Key for the quick-acces-list + */ + $quickAccessListKey: 'sublist-favorites', + /** * Initializes the navigation from the given container * * @private * @param $el element containing the navigation */ - initialize: function($el) { + initialize: function ($el) { this.$el = $el; this._activeItem = null; this.$currentContent = null; this._setupEvents(); + + var scope=this; + $.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/SortingStrategy"), function (data, status) { + scope.$sortingStrategy=data; + scope.setInitialQuickaccessSettings(); + }); + }, /** * Setup UI events */ - _setupEvents: function() { - this.$el.on('click', 'li a', _.bind(this._onClickItem, this)); + _setupEvents: function () { + this.$el.on('click', 'li a', _.bind(this._onClickItem, this)) + this.$el.on('click', 'li button', _.bind(this._onClickMenuButton, this)); }, /** @@ -63,16 +88,16 @@ * * @return app container */ - getActiveContainer: function() { + getActiveContainer: function () { return this.$currentContent; }, /** * Returns the currently active item - * + * * @return item ID */ - getActiveItem: function() { + getActiveItem: function () { return this._activeItem; }, @@ -83,29 +108,42 @@ * @param string itemId id of the navigation item to select * @param array options "silent" to not trigger event */ - setActiveItem: function(itemId, options) { + setActiveItem: function (itemId, options) { + var currentItem = this.$el.find('li[data-id=' + itemId + ']'); + var itemDir = currentItem.data('dir'); + var itemView = currentItem.data('view'); var oldItemId = this._activeItem; if (itemId === this._activeItem) { if (!options || !options.silent) { this.$el.trigger( - new $.Event('itemChanged', {itemId: itemId, previousItemId: oldItemId}) + new $.Event('itemChanged', { + itemId: itemId, + previousItemId: oldItemId, + dir: itemDir, + view: itemView + }) ); } return; } - this.$el.find('li').removeClass('active'); + this.$el.find('li a').removeClass('active'); if (this.$currentContent) { this.$currentContent.addClass('hidden'); this.$currentContent.trigger(jQuery.Event('hide')); } this._activeItem = itemId; - this.$el.find('li[data-id=' + itemId + ']').addClass('active'); - this.$currentContent = $('#app-content-' + itemId); + currentItem.children('a').addClass('active'); + this.$currentContent = $('#app-content-' + (typeof itemView === 'string' && itemView !== '' ? itemView : itemId)); this.$currentContent.removeClass('hidden'); if (!options || !options.silent) { this.$currentContent.trigger(jQuery.Event('show')); this.$el.trigger( - new $.Event('itemChanged', {itemId: itemId, previousItemId: oldItemId}) + new $.Event('itemChanged', { + itemId: itemId, + previousItemId: oldItemId, + dir: itemDir, + view: itemView + }) ); } }, @@ -113,23 +151,206 @@ /** * Returns whether a given item exists */ - itemExists: function(itemId) { + itemExists: function (itemId) { return this.$el.find('li[data-id=' + itemId + ']').length; }, /** * Event handler for when clicking on an item. */ - _onClickItem: function(ev) { + _onClickItem: function (ev) { var $target = $(ev.target); var itemId = $target.closest('li').attr('data-id'); if (!_.isUndefined(itemId)) { this.setActiveItem(itemId); } ev.preventDefault(); + }, + + /** + * Event handler for clicking a button + */ + _onClickMenuButton: function (ev) { + var $target = $(ev.target); + var itemId = $target.closest('button').attr('id'); + + var collapsibleToggles = []; + var dotmenuToggles = []; + + // The collapsibleToggles-Array consists of a list of Arrays. Every subarray must contain the Button to listen to at the 0th index, + // and the parent, which should be toggled at the first arrayindex. + collapsibleToggles.push(["#button-collapse-favorites", "#button-collapse-parent-favorites"]); + + // The dotmenuToggles-Array consists of a list of Arrays. Every subarray must contain the Button to listen to at the 0th index, + // and the parent, which should be toggled at the first arrayindex. + dotmenuToggles.push(["#dotmenu-button-favorites", "dotmenu-content-favorites"]); + + collapsibleToggles.forEach(function foundToggle (item) { + if (item[0] === ("#" + itemId)) { + $(item[1]).toggleClass('open'); + var show = 1; + if (!$(item[1]).hasClass('open')) { + show = 0; + } + $.get(OC.generateUrl("/apps/files/api/v1/quickaccess/set/showList"), {show: show}, function (data, status) { + }); + } + }); + + dotmenuToggles.forEach(function foundToggle (item) { + if (item[0] === ("#" + itemId)) { + document.getElementById(item[1]).classList.toggle('open'); + } + }); + + ev.preventDefault(); + }, + + /** + * Sort initially as setup of sidebar for QuickAccess + */ + setInitialQuickaccessSettings: function () { + + var quickAccesKey = this.$quickAccessListKey; + var list = document.getElementById(quickAccesKey).getElementsByTagName('li'); + + var sort = true; + var reverse = false; + if (this.$sortingStrategy === 'datemodified') { + sort = false; + reverse = false; + + var scope = this; + $.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/FavoriteFolders/"), function (data, status) { + for (var i = 0; i < data.favoriteFolders.length; i++) { + for (var j = 0; j < list.length; j++) { + if (scope.getCompareValue(list, j, 'alphabet').toLowerCase() === data.favoriteFolders[i].name.toLowerCase()) { + list[j].setAttribute("mtime", data.favoriteFolders[i].mtime); + } + } + } + scope.QuickSort(list, 0, list.length - 1); + scope.reverse(list); + }); + + } else if (this.$sortingStrategy === 'alphabet') { + sort = true; + } else if (this.$sortingStrategy === 'date') { + sort = true; + } else if (this.$sortingStrategy === 'customorder') { + var scope = this; + $.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/CustomSortingOrder"), function (data, status) { + var ordering = JSON.parse(data); + for (var i = 0; i < ordering.length; i++) { + for (var j = 0; j < list.length; j++) { + if (scope.getCompareValue(list, j, 'alphabet').toLowerCase() === ordering[i].name.toLowerCase()) { + list[j].setAttribute("folderPosition", ordering[i].id); + } + } + } + scope.QuickSort(list, 0, list.length - 1); + }); + sort = false; + } + + if (sort) { + this.QuickSort(list, 0, list.length - 1); + } + if (reverse) { + this.reverse(list); + } + + }, + + /** + * Sorting-Algorithm for QuickAccess + */ + QuickSort: function (list, start, end) { + var lastMatch; + if (list.length > 1) { + lastMatch = this.quicksort_helper(list, start, end); + if (start < lastMatch - 1) { + this.QuickSort(list, start, lastMatch - 1); + } + if (lastMatch < end) { + this.QuickSort(list, lastMatch, end); + } + } + }, + + /** + * Sorting-Algorithm-Helper for QuickAccess + */ + quicksort_helper: function (list, start, end) { + var pivot = Math.floor((end + start) / 2); + var pivotElement = this.getCompareValue(list, pivot); + var i = start; + var j = end; + + while (i <= j) { + while (this.getCompareValue(list, i) < pivotElement) { + i++; + } + while (this.getCompareValue(list, j) > pivotElement) { + j--; + } + if (i <= j) { + this.swap(list, i, j); + i++; + j--; + } + } + return i; + }, + + /** + * Sorting-Algorithm-Helper for QuickAccess + * This method allows easy access to the element which is sorted by. + */ + getCompareValue: function (nodes, int, strategy) { + + if ((typeof strategy === 'undefined')) { + strategy = this.$sortingStrategy; + } + + if (strategy === 'alphabet') { + return nodes[int].getElementsByTagName('a')[0].innerHTML.toLowerCase(); + } else if (strategy === 'date') { + return nodes[int].getAttribute('folderPosition').toLowerCase(); + } else if (strategy === 'datemodified') { + return nodes[int].getAttribute('mtime'); + } else if (strategy === 'customorder') { + return nodes[int].getAttribute('folderPosition'); + } + return nodes[int].getElementsByTagName('a')[0].innerHTML.toLowerCase(); + }, + + /** + * Sorting-Algorithm-Helper for QuickAccess + * This method allows easy swapping of elements. + */ + swap: function (list, j, i) { + list[i].before(list[j]); + list[j].before(list[i]); + }, + + /** + * Reverse QuickAccess-List + */ + reverse: function (list) { + var len = list.length - 1; + for (var i = 0; i < len / 2; i++) { + this.swap(list, i, len - i); + } } + }; OCA.Files.Navigation = Navigation; })(); + + + + + diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index b174aa7d766..bc1396b5104 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -10,11 +10,11 @@ /* global Handlebars */ -(function(OCA) { +(function (OCA) { _.extend(OC.Files.Client, { - PROPERTY_TAGS: '{' + OC.Files.Client.NS_OWNCLOUD + '}tags', - PROPERTY_FAVORITE: '{' + OC.Files.Client.NS_OWNCLOUD + '}favorite' + PROPERTY_TAGS: '{' + OC.Files.Client.NS_OWNCLOUD + '}tags', + PROPERTY_FAVORITE: '{' + OC.Files.Client.NS_OWNCLOUD + '}favorite' }); var TEMPLATE_FAVORITE_MARK = @@ -30,7 +30,7 @@ * @param {boolean} state true if starred, false otherwise * @return {string} icon class for star image */ - function getStarIconClass(state) { + function getStarIconClass (state) { return state ? 'icon-starred' : 'icon-star'; } @@ -40,7 +40,7 @@ * @param {boolean} state true if starred, false otherwise * @return {Object} jQuery object */ - function renderStar(state) { + function renderStar (state) { if (!this._template) { this._template = Handlebars.compile(TEMPLATE_FAVORITE_MARK); } @@ -57,11 +57,95 @@ * @param {Object} $favoriteMarkEl favorite mark element * @param {boolean} state true if starred, false otherwise */ - function toggleStar($favoriteMarkEl, state) { + function toggleStar ($favoriteMarkEl, state) { $favoriteMarkEl.removeClass('icon-star icon-starred').addClass(getStarIconClass(state)); $favoriteMarkEl.toggleClass('permanent', state); } + /** + * Remove Item from Quickaccesslist + * + * @param {String} appfolder folder to be removed + */ + function removeFavoriteFromList (appfolder) { + + var quickAccessList = 'sublist-favorites'; + var collapsibleButtonId = 'button-collapse-favorites'; + var listULElements = document.getElementById(quickAccessList); + if (!listULElements) { + return; + } + var listLIElements = listULElements.getElementsByTagName('li'); + + var apppath=appfolder; + if(appfolder.startsWith("//")){ + apppath=appfolder.substring(1, appfolder.length); + } + + for (var i = 0; i <= listLIElements.length - 1; i++) { + if (listLIElements[i].getElementsByTagName('a')[0].href.endsWith("dir=" + apppath)) { + listLIElements[i].remove(); + } + } + + if (listULElements.childElementCount === 0) { + var collapsibleButton = document.getElementById("button-collapse-favorites"); + collapsibleButton.style.display = 'none'; + $("#button-collapse-parent-favorites").removeClass('collapsible'); + } + } + + /** + * Add Item to Quickaccesslist + * + * @param {String} appfolder folder to be added + */ + function addFavoriteToList (appfolder) { + var quickAccessList = 'sublist-favorites'; + var collapsibleButtonId = 'button-collapse-favorites'; + var listULElements = document.getElementById(quickAccessList); + if (!listULElements) { + return; + } + var listLIElements = listULElements.getElementsByTagName('li'); + + var appName = appfolder.substring(appfolder.lastIndexOf("/") + 1, appfolder.length); + var apppath=appfolder; + + if(appfolder.startsWith("//")){ + apppath=appfolder.substring(1, appfolder.length); + } + var url=OC.generateUrl('/apps/files/?dir=')+apppath; + + + var innerTagA = document.createElement('A'); + innerTagA.setAttribute("href", url); + innerTagA.setAttribute("class", "nav-icon-files svg"); + innerTagA.innerHTML = appName; + + var length = listLIElements.length + 1; + var innerTagLI = document.createElement('li'); + innerTagLI.setAttribute("data-id", url); + innerTagLI.setAttribute("class", "nav-" + appName); + innerTagLI.setAttribute("folderpos", length.toString()); + innerTagLI.appendChild(innerTagA); + + $.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/NodeType"),{folderpath: apppath}, function (data, status) { + if (data === "dir") { + if (listULElements.childElementCount <= 0) { + listULElements.appendChild(innerTagLI); + var collapsibleButton = document.getElementById(collapsibleButtonId); + collapsibleButton.style.display = ''; + + $("#button-collapse-parent-favorites").addClass('collapsible'); + } else { + listLIElements[listLIElements.length - 1].after(innerTagLI); + } + } + } + ); + } + OCA.Files = OCA.Files || {}; /** @@ -83,12 +167,12 @@ 'shares.link' ], - _extendFileActions: function(fileActions) { + _extendFileActions: function (fileActions) { var self = this; fileActions.registerAction({ name: 'Favorite', - displayName: function(context) { + displayName: function (context) { var $file = context.$file; var isFavorite = $file.data('favorite') === true; @@ -105,7 +189,7 @@ mime: 'all', order: -100, permissions: OC.PERMISSION_NONE, - iconClass: function(fileName, context) { + iconClass: function (fileName, context) { var $file = context.$file; var isFavorite = $file.data('favorite') === true; @@ -115,12 +199,13 @@ return 'icon-starred'; }, - actionHandler: function(fileName, context) { + actionHandler: function (fileName, context) { var $favoriteMarkEl = context.$file.find('.favorite-mark'); var $file = context.$file; var fileInfo = context.fileList.files[$file.index()]; var dir = context.dir || context.fileList.getCurrentDirectory(); var tags = $file.attr('data-tags'); + if (_.isUndefined(tags)) { tags = ''; } @@ -130,8 +215,10 @@ if (isFavorite) { // remove tag from list tags = _.without(tags, OC.TAG_FAVORITE); + removeFavoriteFromList(dir + '/' + fileName); } else { tags.push(OC.TAG_FAVORITE); + addFavoriteToList(dir + '/' + fileName); } // pre-toggle the star @@ -144,7 +231,7 @@ tags, $favoriteMarkEl, isFavorite - ).then(function(result) { + ).then(function (result) { context.fileInfoModel.trigger('busy', context.fileInfoModel, false); // response from server should contain updated tags var newTags = result.tags; @@ -160,10 +247,10 @@ }); }, - _extendFileList: function(fileList) { + _extendFileList: function (fileList) { // extend row prototype var oldCreateRow = fileList._createRow; - fileList._createRow = function(fileData) { + fileList._createRow = function (fileData) { var $tr = oldCreateRow.apply(this, arguments); var isFavorite = false; if (fileData.tags) { @@ -178,7 +265,7 @@ return $tr; }; var oldElementToFile = fileList.elementToFile; - fileList.elementToFile = function($el) { + fileList.elementToFile = function ($el) { var fileInfo = oldElementToFile.apply(this, arguments); var tags = $el.attr('data-tags'); if (_.isUndefined(tags)) { @@ -191,22 +278,22 @@ }; var oldGetWebdavProperties = fileList._getWebdavProperties; - fileList._getWebdavProperties = function() { + fileList._getWebdavProperties = function () { var props = oldGetWebdavProperties.apply(this, arguments); props.push(OC.Files.Client.PROPERTY_TAGS); props.push(OC.Files.Client.PROPERTY_FAVORITE); return props; }; - fileList.filesClient.addFileInfoParser(function(response) { + fileList.filesClient.addFileInfoParser(function (response) { var data = {}; var props = response.propStat[0].properties; var tags = props[OC.Files.Client.PROPERTY_TAGS]; var favorite = props[OC.Files.Client.PROPERTY_FAVORITE]; if (tags && tags.length) { - tags = _.chain(tags).filter(function(xmlvalue) { + tags = _.chain(tags).filter(function (xmlvalue) { return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'tag'); - }).map(function(xmlvalue) { + }).map(function (xmlvalue) { return xmlvalue.textContent || xmlvalue.text; }).value(); } @@ -221,7 +308,7 @@ }); }, - attach: function(fileList) { + attach: function (fileList) { if (this.allowedLists.indexOf(fileList.id) < 0) { return; } @@ -237,7 +324,7 @@ * @param {Object} $favoriteMarkEl favorite mark element * @param {boolean} isFavorite Was the item favorited before */ - applyFileTags: function(fileName, tagNames, $favoriteMarkEl, isFavorite) { + applyFileTags: function (fileName, tagNames, $favoriteMarkEl, isFavorite) { var encodedPath = OC.encodePath(fileName); while (encodedPath[0] === '/') { encodedPath = encodedPath.substr(1); @@ -250,10 +337,10 @@ }), dataType: 'json', type: 'POST' - }).fail(function(response) { + }).fail(function (response) { var message = ''; // show message if it is available - if(response.responseJSON && response.responseJSON.message) { + if (response.responseJSON && response.responseJSON.message) { message = ': ' + response.responseJSON.message; } OC.Notification.show(t('files', 'An error occurred while trying to update the tags' + message), {type: 'error'}); @@ -261,6 +348,7 @@ }); } }; -})(OCA); +}) +(OCA); OC.Plugins.register('OCA.Files.FileList', OCA.Files.TagsPlugin); diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 064f2baa987..d4dac5befa8 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -26,6 +26,7 @@ */ namespace OCA\Files\AppInfo; +use OCA\Files\Activity\Helper; use OCA\Files\Controller\ApiController; use OCP\AppFramework\App; use \OCA\Files\Service\TagService; @@ -65,7 +66,8 @@ class Application extends App { $server->getEventDispatcher(), $server->getUserSession(), $server->getAppManager(), - $server->getRootFolder() + $server->getRootFolder(), + $c->query(Helper::class) ); }); diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index a66b1b4d565..aae1bec2e78 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -11,7 +11,7 @@ * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Tobias Kaminsky <tobias@kaminsky.me> * @author Vincent Petry <pvince81@owncloud.com> - * + * @author Felix Nüsse <felix.nuesse@t-online.de> * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -45,6 +45,7 @@ use OCP\IPreview; use OCP\Share\IManager; use OC\Files\Node\Node; use OCP\IUserSession; +use Sabre\VObject\Property\Boolean; /** * Class ApiController @@ -54,7 +55,7 @@ use OCP\IUserSession; class ApiController extends Controller { /** @var TagService */ private $tagService; - /** @var IManager **/ + /** @var IManager * */ private $shareManager; /** @var IPreview */ private $previewManager; @@ -107,7 +108,7 @@ class ApiController extends Controller { * @return DataResponse|FileDisplayResponse */ public function getThumbnail($x, $y, $file) { - if($x < 1 || $y < 1) { + if ($x < 1 || $y < 1) { return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST); } @@ -199,6 +200,30 @@ class ApiController extends Controller { } /** + * Returns a list of favorites modifed folder. + * + * @NoAdminRequired + * + * @return DataResponse + */ + public function getFavoritesFolder() { + $nodes = $this->userFolder->searchByTag('_$!<Favorite>!$_', $this->userSession->getUser()->getUID()); + + $favorites = []; + $i = 0; + foreach ($nodes as &$node) { + + $favorites[$i]['id'] = $node->getId(); + $favorites[$i]['name'] = $node->getName(); + $favorites[$i]['path'] = $node->getInternalPath(); + $favorites[$i]['mtime'] = $node->getMTime(); + $i++; + } + + return new DataResponse(['favoriteFolders' => $favorites]); + } + + /** * Return a list of share types for outgoing shares * * @param Node $node file node @@ -261,8 +286,150 @@ class ApiController extends Controller { * @param bool $show */ public function showHiddenFiles($show) { - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', (int) $show); + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', (int)$show); + return new Response(); + } + + /** + * Toggle default for showing/hiding QuickAccess folder + * + * @NoAdminRequired + * + * @param bool $show + * + * @return Response + */ + public function showQuickAccess($show) { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_Quick_Access', (int)$show); + return new Response(); + } + + /** + * Toggle default for showing/hiding QuickAccess folder + * + * @NoAdminRequired + * + * @return String + */ + public function getShowQuickAccess() { + + return $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_Quick_Access', 0); + } + + /** + * quickaccess-sorting-strategy + * + * @NoAdminRequired + * + * @param string $strategy + * @return Response + */ + public function setSortingStrategy($strategy) { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_sorting_strategy', (String)$strategy); return new Response(); } + /** + * Get reverse-state for quickaccess-list + * + * @NoAdminRequired + * + * @return String + */ + public function getSortingStrategy() { + return $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_sorting_strategy', 'alphabet'); + } + + /** + * Toggle for reverse quickaccess-list + * + * @NoAdminRequired + * + * @param bool $reverse + * @return Response + */ + public function setReverseQuickaccess($reverse) { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_reverse_list', (int)$reverse); + return new Response(); + } + + /** + * Get reverse-state for quickaccess-list + * + * @NoAdminRequired + * + * @return bool + */ + public function getReverseQuickaccess() { + if ($this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_reverse_list', false)) { + return true; + } + return false; + } + + /** + * Set state for show sorting menu + * + * @NoAdminRequired + * + * @param bool $show + * @return Response + */ + public function setShowQuickaccessSettings($show) { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_show_settings', (int)$show); + return new Response(); + } + + /** + * Get state for show sorting menu + * + * @NoAdminRequired + * + * @return bool + */ + public function getShowQuickaccessSettings() { + if ($this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_show_settings', false)) { + return true; + } + return false; + } + + /** + * Set sorting-order for custom sorting + * + * @NoAdminRequired + * + * @param String $order + * @return Response + */ + public function setSortingOrder($order) { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_custom_sorting_order', (String)$order); + return new Response(); + } + + /** + * Get sorting-order for custom sorting + * + * @NoAdminRequired + * + * @return String + */ + public function getSortingOrder() { + return $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'quickaccess_custom_sorting_order', ""); + } + + /** + * Get sorting-order for custom sorting + * + * @NoAdminRequired + * + * @param String + * @return String + */ + public function getNodeType($folderpath) { + $node = $this->userFolder->get($folderpath); + return $node->getType(); + } + + } diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 7cb0f112f72..f240e04c721 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -8,6 +8,7 @@ * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Thomas Müller <thomas.mueller@tmit.eu> * @author Vincent Petry <pvince81@owncloud.com> + * @author Felix Nüsse <felix.nuesse@t-online.de> * * @license AGPL-3.0 * @@ -27,6 +28,7 @@ namespace OCA\Files\Controller; +use OCA\Files\Activity\Helper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\RedirectResponse; @@ -67,19 +69,10 @@ class ViewController extends Controller { protected $appManager; /** @var IRootFolder */ protected $rootFolder; + /** @var Helper */ + protected $activityHelper; - /** - * @param string $appName - * @param IRequest $request - * @param IURLGenerator $urlGenerator - * @param IL10N $l10n - * @param IConfig $config - * @param EventDispatcherInterface $eventDispatcherInterface - * @param IUserSession $userSession - * @param IAppManager $appManager - * @param IRootFolder $rootFolder - */ - public function __construct($appName, + public function __construct(string $appName, IRequest $request, IURLGenerator $urlGenerator, IL10N $l10n, @@ -87,7 +80,8 @@ class ViewController extends Controller { EventDispatcherInterface $eventDispatcherInterface, IUserSession $userSession, IAppManager $appManager, - IRootFolder $rootFolder + IRootFolder $rootFolder, + Helper $activityHelper ) { parent::__construct($appName, $request); $this->appName = $appName; @@ -99,6 +93,7 @@ class ViewController extends Controller { $this->userSession = $userSession; $this->appManager = $appManager; $this->rootFolder = $rootFolder; + $this->activityHelper = $activityHelper; } /** @@ -159,28 +154,68 @@ class ViewController extends Controller { // FIXME: Make non static $storageInfo = $this->getStorageInfo(); + $user = $this->userSession->getUser()->getUID(); + + try { + $favElements = $this->activityHelper->getFavoriteFilePaths($this->userSession->getUser()->getUID()); + } catch (\RuntimeException $e) { + $favElements['folders'] = null; + } + + $collapseClasses = ''; + if (count($favElements['folders']) > 0) { + $collapseClasses = 'collapsible'; + } + + $favoritesSublistArray = Array(); + + $navBarPositionPosition = 6; + $currentCount = 0; + foreach ($favElements['folders'] as $dir) { + + $id = substr($dir, strrpos($dir, '/') + 1, strlen($dir)); + $link = $this->urlGenerator->linkToRoute('files.view.index', ['dir' => $dir, 'view' => 'files']); + $sortingValue = ++$currentCount; + $element = [ + 'id' => str_replace('/', '-', $dir), + 'view' => 'files', + 'href' => $link, + 'dir' => $dir, + 'order' => $navBarPositionPosition, + 'folderPosition' => $sortingValue, + 'name' => $id, + 'icon' => 'files', + 'quickaccesselement' => 'true' + ]; + + array_push($favoritesSublistArray, $element); + $navBarPositionPosition++; + } + + + // show_Quick_Access stored as string + $defaultExpandedState = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_Quick_Access', '0') === '1'; + \OCA\Files\App::getNavigationManager()->add( [ 'id' => 'favorites', 'appname' => 'files', 'script' => 'simplelist.php', + 'classes' => $collapseClasses, 'order' => 5, - 'name' => $this->l10n->t('Favorites') + 'name' => $this->l10n->t('Favorites'), + 'sublist' => $favoritesSublistArray, + 'defaultExpandedState' => $defaultExpandedState, + 'enableMenuButton' => 0, ] ); $navItems = \OCA\Files\App::getNavigationManager()->getAll(); - usort($navItems, function($item1, $item2) { + usort($navItems, function ($item1, $item2) { return $item1['order'] - $item2['order']; }); - $nav->assign('navigationItems', $navItems); - $webdavurl = $this->urlGenerator->linkTo('', 'remote.php') . - '/dav/files/' . - $this->userSession->getUser()->getUID() . - '/'; - $webdavurl = $this->urlGenerator->getAbsoluteURL($webdavurl); - $nav->assign('webdavurl', $webdavurl); + $nav->assign('navigationItems', $navItems); $nav->assign('usage', \OC_Helper::humanFileSize($storageInfo['used'])); if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { @@ -215,10 +250,9 @@ class ViewController extends Controller { $params['ownerDisplayName'] = $storageInfo['ownerDisplayName']; $params['isPublic'] = false; $params['allowShareWithLink'] = $this->config->getAppValue('core', 'shareapi_allow_links', 'yes'); - $user = $this->userSession->getUser()->getUID(); $params['defaultFileSorting'] = $this->config->getUserValue($user, 'files', 'file_sorting', 'name'); $params['defaultFileSortingDirection'] = $this->config->getUserValue($user, 'files', 'file_sorting_direction', 'asc'); - $showHidden = (bool) $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', false); + $showHidden = (bool)$this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', false); $params['showHiddenFiles'] = $showHidden ? 1 : 0; $params['fileNotFound'] = $fileNotFound ? 1 : 0; $params['appNavigation'] = $nav; @@ -234,6 +268,7 @@ class ViewController extends Controller { $policy->addAllowedFrameDomain('\'self\''); $response->setContentSecurityPolicy($policy); + return $response; } diff --git a/apps/files/templates/appnavigation.php b/apps/files/templates/appnavigation.php index c811ace8abe..0b9ac665901 100644 --- a/apps/files/templates/appnavigation.php +++ b/apps/files/templates/appnavigation.php @@ -1,18 +1,17 @@ <div id="app-navigation"> <ul class="with-icon"> - <?php $pinned = 0 ?> - <?php foreach ($_['navigationItems'] as $item) { - strpos($item['classes'], 'pinned')!==false ? $pinned++ : ''; + + <?php + + $pinned = 0; + foreach ($_['navigationItems'] as $item) { + $pinned = NavigationListElements($item, $l, $pinned); + } ?> - <li data-id="<?php p($item['id']) ?>" class="nav-<?php p($item['id']) ?> <?php p($item['classes']) ?> <?php p($pinned===1?'first-pinned':'') ?>"> - <a href="<?php p(isset($item['href']) ? $item['href'] : '#') ?>" - class="nav-icon-<?php p($item['icon'] !== '' ? $item['icon'] : $item['id']) ?> svg"> - <?php p($item['name']);?> - </a> - </li> - <?php } ?> - <li id="quota" class="pinned <?php p($pinned===0?'first-pinned ':'') ?><?php - if ($_['quota'] !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { + + <li id="quota" + class="pinned <?php p($pinned === 0 ? 'first-pinned ' : '') ?><?php + if ($_['quota'] !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { ?>has-tooltip" title="<?php p($_['usage_relative'] . '%'); } ?>"> <a href="#" class="icon-quota svg"> @@ -23,26 +22,103 @@ p($l->t('%s used', [$_['usage']])); } ?></p> <div class="quota-container"> - <progress value="<?php p($_['usage_relative']); ?>" max="100" - <?php if($_['usage_relative'] > 80): ?> class="warn" <?php endif; ?>></progress> + <progress value="<?php p($_['usage_relative']); ?>" + max="100" + <?php if ($_['usage_relative'] > 80): ?> class="warn" <?php endif; ?>></progress> </div> </a> </li> </ul> <div id="app-settings"> <div id="app-settings-header"> - <button class="settings-button" data-apps-slide-toggle="#app-settings-content"> - <?php p($l->t('Settings'));?> + <button class="settings-button" + data-apps-slide-toggle="#app-settings-content"> + <?php p($l->t('Settings')); ?> </button> </div> <div id="app-settings-content"> <div id="files-setting-showhidden"> - <input class="checkbox" id="showhiddenfilesToggle" checked="checked" type="checkbox"> + <input class="checkbox" id="showhiddenfilesToggle" + checked="checked" type="checkbox"> <label for="showhiddenfilesToggle"><?php p($l->t('Show hidden files')); ?></label> </div> - <label for="webdavurl"><?php p($l->t('WebDAV'));?></label> - <input id="webdavurl" type="text" readonly="readonly" value="<?php p($_['webdavurl']); ?>" /> - <em><?php print_unescaped($l->t('Use this address to <a href="%s" target="_blank" rel="noreferrer noopener">access your Files via WebDAV</a>', array(link_to_docs('user-webdav'))));?></em> + <label for="webdavurl"><?php p($l->t('WebDAV')); ?></label> + <input id="webdavurl" type="text" readonly="readonly" + value="<?php p(\OCP\Util::linkToRemote('webdav')); ?>"/> + <em><?php print_unescaped($l->t('Use this address to <a href="%s" target="_blank" rel="noreferrer noopener">access your Files via WebDAV</a>', array(link_to_docs('user-webdav')))); ?></em> </div> </div> + </div> + + +<?php + +/** + * Prints the HTML for a single Entry. + * + * @param $item The item to be added + * @param $l Translator + * @param $pinned IntegerValue to count the pinned entries at the bottom + * + * @return int Returns the pinned value + */ +function NavigationListElements($item, $l, $pinned) { + strpos($item['classes'], 'pinned') !== false ? $pinned++ : ''; + ?> + <li <?php if (isset($item['sublist'])){ ?>id="button-collapse-parent-<?php p($item['id']); ?>"<?php } ?> + data-id="<?php p($item['id']) ?>" data-dir="<?php p($item['dir']) ?>" data-view="<?php p($item['view']) ?>" + class="nav-<?php p($item['id']) ?> <?php p($item['classes']) ?> <?php p($pinned === 1 ? 'first-pinned' : '') ?> <?php if ($item['defaultExpandedState']) { ?> open<?php } ?>" + <?php if (isset($item['folderPosition'])) { ?> folderposition="<?php p($item['folderPosition']); ?>" <?php } ?>> + + <a href="<?php p(isset($item['href']) ? $item['href'] : '#') ?>" + class="nav-icon-<?php p($item['icon'] !== '' ? $item['icon'] : $item['id']) ?> svg"><?php p($item['name']); ?></a> + + + <?php + NavigationElementMenu($item); + if (isset($item['sublist'])) { + ?> + <button id="button-collapse-<?php p($item['id']); ?>" + class="collapse app-navigation-noclose" <?php if (sizeof($item['sublist']) == 0) { ?> style="display: none" <?php } ?>></button> + <ul id="sublist-<?php p($item['id']); ?>"> + <?php + foreach ($item['sublist'] as $item) { + $pinned = NavigationListElements($item, $l, $pinned); + } + ?> + </ul> + <?php } ?> + </li> + + + <?php + return $pinned; +} + +/** + * Prints the HTML for a dotmenu. + * + * @param $item The item to be added + * + * @return void + */ +function NavigationElementMenu($item) { + if ($item['menubuttons'] === 'true') { + ?> + <div id="dotmenu-<?php p($item['id']); ?>" + class="app-navigation-entry-utils" <?php if ($item['enableMenuButton'] === 0) { ?> style="display: none"<?php } ?>> + <ul> + <li class="app-navigation-entry-utils-menu-button svg"> + <button id="dotmenu-button-<?php p($item['id']) ?>"></button> + </li> + </ul> + </div> + <div id="dotmenu-content-<?php p($item['id']) ?>" + class="app-navigation-entry-menu"> + <ul> + + </ul> + </div> + <?php } +} diff --git a/apps/files/tests/Controller/ViewControllerTest.php b/apps/files/tests/Controller/ViewControllerTest.php index eae627fd6a4..8f077645808 100644 --- a/apps/files/tests/Controller/ViewControllerTest.php +++ b/apps/files/tests/Controller/ViewControllerTest.php @@ -29,6 +29,7 @@ namespace OCA\Files\Tests\Controller; +use OCA\Files\Activity\Helper; use OCA\Files\Controller\ViewController; use OCP\AppFramework\Http; use OCP\Files\File; @@ -71,6 +72,8 @@ class ViewControllerTest extends TestCase { private $appManager; /** @var IRootFolder|\PHPUnit_Framework_MockObject_MockObject */ private $rootFolder; + /** @var Helper|\PHPUnit_Framework_MockObject_MockObject */ + private $activityHelper; public function setUp() { parent::setUp(); @@ -89,6 +92,7 @@ class ViewControllerTest extends TestCase { ->method('getUser') ->will($this->returnValue($this->user)); $this->rootFolder = $this->getMockBuilder('\OCP\Files\IRootFolder')->getMock(); + $this->activityHelper = $this->createMock(Helper::class); $this->viewController = $this->getMockBuilder('\OCA\Files\Controller\ViewController') ->setConstructorArgs([ 'files', @@ -99,7 +103,8 @@ class ViewControllerTest extends TestCase { $this->eventDispatcher, $this->userSession, $this->appManager, - $this->rootFolder + $this->rootFolder, + $this->activityHelper, ]) ->setMethods([ 'getStorageInfo', @@ -120,7 +125,7 @@ class ViewControllerTest extends TestCase { 'owner' => 'MyName', 'ownerDisplayName' => 'MyDisplayName', ])); - $this->config->expects($this->exactly(3)) + $this->config ->method('getUserValue') ->will($this->returnValueMap([ [$this->user->getUID(), 'files', 'file_sorting', 'name', 'name'], @@ -138,7 +143,7 @@ class ViewControllerTest extends TestCase { $nav->assign('usage', '123 B'); $nav->assign('quota', 100); $nav->assign('total_space', '100 B'); - $nav->assign('webdavurl', ''); + //$nav->assign('webdavurl', ''); $nav->assign('navigationItems', [ [ 'id' => 'files', @@ -172,6 +177,9 @@ class ViewControllerTest extends TestCase { 'icon' => '', 'type' => 'link', 'classes' => '', + 'sublist' => [], + 'defaultExpandedState' => false, + 'enableMenuButton' => 0, ], [ 'id' => 'sharingin', @@ -299,6 +307,14 @@ class ViewControllerTest extends TestCase { $policy = new Http\ContentSecurityPolicy(); $policy->addAllowedFrameDomain('\'self\''); $expected->setContentSecurityPolicy($policy); + + $this->activityHelper->method('getFavoriteFilePaths') + ->with($this->user->getUID()) + ->willReturn([ + 'item' => [], + 'folders' => [], + ]); + $this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView')); } diff --git a/apps/files/tests/js/appSpec.js b/apps/files/tests/js/appSpec.js index 5728991e197..15297a029d4 100644 --- a/apps/files/tests/js/appSpec.js +++ b/apps/files/tests/js/appSpec.js @@ -239,38 +239,38 @@ describe('OCA.Files.App tests', function() { expect(App.navigation.getActiveItem()).toEqual('other'); expect($('#app-content-files').hasClass('hidden')).toEqual(true); expect($('#app-content-other').hasClass('hidden')).toEqual(false); - expect($('li[data-id=files]').hasClass('active')).toEqual(false); - expect($('li[data-id=other]').hasClass('active')).toEqual(true); + expect($('li[data-id=files] > a').hasClass('active')).toEqual(false); + expect($('li[data-id=other] > a').hasClass('active')).toEqual(true); App._onPopState({view: 'files', dir: '/somedir'}); expect(App.navigation.getActiveItem()).toEqual('files'); expect($('#app-content-files').hasClass('hidden')).toEqual(false); expect($('#app-content-other').hasClass('hidden')).toEqual(true); - expect($('li[data-id=files]').hasClass('active')).toEqual(true); - expect($('li[data-id=other]').hasClass('active')).toEqual(false); + expect($('li[data-id=files] > a').hasClass('active')).toEqual(true); + expect($('li[data-id=other] > a').hasClass('active')).toEqual(false); }); it('clicking on navigation switches the panel visibility', function() { - $('li[data-id=other]>a').click(); + $('li[data-id=other] > a').click(); expect(App.navigation.getActiveItem()).toEqual('other'); expect($('#app-content-files').hasClass('hidden')).toEqual(true); expect($('#app-content-other').hasClass('hidden')).toEqual(false); - expect($('li[data-id=files]').hasClass('active')).toEqual(false); - expect($('li[data-id=other]').hasClass('active')).toEqual(true); + expect($('li[data-id=files] > a').hasClass('active')).toEqual(false); + expect($('li[data-id=other] > a').hasClass('active')).toEqual(true); - $('li[data-id=files]>a').click(); + $('li[data-id=files] > a').click(); expect(App.navigation.getActiveItem()).toEqual('files'); expect($('#app-content-files').hasClass('hidden')).toEqual(false); expect($('#app-content-other').hasClass('hidden')).toEqual(true); - expect($('li[data-id=files]').hasClass('active')).toEqual(true); - expect($('li[data-id=other]').hasClass('active')).toEqual(false); + expect($('li[data-id=files] > a').hasClass('active')).toEqual(true); + expect($('li[data-id=other] > a').hasClass('active')).toEqual(false); }); it('clicking on navigation sends "show" and "urlChanged" event', function() { var handler = sinon.stub(); var showHandler = sinon.stub(); $('#app-content-other').on('urlChanged', handler); $('#app-content-other').on('show', showHandler); - $('li[data-id=other]>a').click(); + $('li[data-id=other] > a').click(); expect(handler.calledOnce).toEqual(true); expect(handler.getCall(0).args[0].view).toEqual('other'); expect(handler.getCall(0).args[0].dir).toEqual('/'); @@ -281,7 +281,7 @@ describe('OCA.Files.App tests', function() { var showHandler = sinon.stub(); $('#app-content-files').on('urlChanged', handler); $('#app-content-files').on('show', showHandler); - $('li[data-id=files]>a').click(); + $('li[data-id=files] > a').click(); expect(handler.calledOnce).toEqual(true); expect(handler.getCall(0).args[0].view).toEqual('files'); expect(handler.getCall(0).args[0].dir).toEqual('/'); diff --git a/core/css/apps.scss b/core/css/apps.scss index 13b4c7eb9fc..6645b6868d5 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -144,11 +144,12 @@ kbd { padding-left: 38px !important; } - &:hover, - &:focus, - &:active, &.active, - a.selected { + a:hover, + a:focus, + a:active, + a.selected , + a.active { &, > a { opacity: 1; @@ -156,13 +157,6 @@ kbd { } } - /* a instead of li is focused by keyboards */ - a:focus, - a:active { - opacity: 1; - box-shadow: inset 4px 0 var(--color-primary); - } - /* align loader */ &.icon-loading-small:after { left: 22px; @@ -691,6 +685,7 @@ kbd { background-color: var(--color-main-background); } + .settings-button { display: block; height: 44px; |