diff --git a/apps/files/ajax/list.php b/apps/files/ajax/list.php index 2d76b685018f..bae3628402f7 100644 --- a/apps/files/ajax/list.php +++ b/apps/files/ajax/list.php @@ -17,8 +17,11 @@ $permissions = $dirInfo->getPermissions(); +$sortAttribute = isset( $_GET['sort'] ) ? $_GET['sort'] : 'name'; +$sortDirection = isset( $_GET['sortdirection'] ) ? ($_GET['sortdirection'] === 'desc') : false; + // make filelist -$files = \OCA\Files\Helper::getFiles($dir); +$files = \OCA\Files\Helper::getFiles($dir, $sortAttribute, $sortDirection); $data['directory'] = $dir; $data['files'] = \OCA\Files\Helper::formatFileInfos($files); diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 533050691d5e..2aae73f8bd01 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -116,10 +116,29 @@ tr:hover span.extension { table tr.mouseOver td { background-color:#eee; } table th { height:24px; padding:0 8px; color:#999; } -table th .name { - position: absolute; - left: 55px; - top: 15px; +table th .columntitle { + display: inline-block; + padding: 15px; + width: 100%; + height: 50px; + box-sizing: border-box; + -moz-box-sizing: border-box; + vertical-align: middle; +} +table th .columntitle.name { + padding-left: 5px; + margin-left: 50px; + max-width: 300px; +} +/* hover effect on sortable column */ +table th a.columntitle:hover { + background-color: #F0F0F0; +} +table th .sort-indicator { + width: 10px; + height: 8px; + margin-left: 10px; + display: inline-block; } table th, table td { border-bottom:1px solid #ddd; text-align:left; font-weight:normal; } table td { @@ -139,8 +158,11 @@ table th#headerName { } table th#headerSize, table td.filesize { min-width: 48px; - padding: 0 16px; text-align: right; + padding: 0; +} +table table td.filesize { + padding: 0 16px; } table th#headerDate, table td.date { -moz-box-sizing: border-box; @@ -197,10 +219,6 @@ table td.filename input.filename { table td.filename a, table td.login, table td.logout, table td.download, table td.upload, table td.create, table td.delete { padding:3px 8px 8px 3px; } table td.filename .nametext, .uploadtext, .modified { float:left; padding:14px 0; } -#modified { - position: absolute; - top: 15px; -} .modified { position: relative; padding-left: 8px; diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 40ec898635e4..2d3e96fbd282 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -11,6 +11,9 @@ /* global OC, t, n, FileList, FileActions, Files, FileSummary, BreadCrumb */ /* global dragOptions, folderDropOptions */ window.FileList = { + SORT_INDICATOR_ASC_CLASS: 'icon-triangle-s', + SORT_INDICATOR_DESC_CLASS: 'icon-triangle-n', + appName: t('files', 'Files'), isEmpty: true, useUndo:true, @@ -45,18 +48,19 @@ window.FileList = { _selectionSummary: null, /** - * Compare two file info objects, sorting by - * folders first, then by name. + * Sort attribute */ - _fileInfoCompare: function(fileInfo1, fileInfo2) { - if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') { - return -1; - } - if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') { - return 1; - } - return fileInfo1.name.localeCompare(fileInfo2.name); - }, + _sort: 'name', + + /** + * Sort direction: 'asc' or 'desc' + */ + _sortDirection: 'asc', + + /** + * Sort comparator function for the current sort + */ + _sortComparator: null, /** * Initialize the file list and its components @@ -76,6 +80,8 @@ window.FileList = { this.fileSummary = this._createSummary(); + this.setSort('name', 'asc'); + this.breadcrumb = new BreadCrumb({ onClick: this._onClickBreadCrumb, onDrop: _.bind(this._onDropOnBreadCrumb, this), @@ -86,6 +92,8 @@ window.FileList = { $('#controls').prepend(this.breadcrumb.$el); + this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); + $(window).resize(function() { // TODO: debounce this ? var width = $(this).width(); @@ -236,6 +244,27 @@ window.FileList = { return false; }, + /** + * Event handler when clicking on a table header + */ + _onClickHeader: function(e) { + var $target = $(e.target); + var sort; + if (!$target.is('a')) { + $target = $target.closest('a'); + } + sort = $target.attr('data-sort'); + if (sort) { + if (this._sort === sort) { + this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc'); + } + else { + this.setSort(sort, 'asc'); + } + this.reload(); + } + }, + /** * Event handler when clicking on a bread crumb */ @@ -685,8 +714,6 @@ window.FileList = { previousDir: currentDir } )); - this._selectedFiles = {}; - this._selectionSummary.clear(); this.reload(); }, linkTo: function(dir) { @@ -722,10 +749,34 @@ window.FileList = { } this.breadcrumb.setDirectory(this.getCurrentDirectory()); }, + /** + * Sets the current sorting and refreshes the list + * + * @param sort sort attribute name + * @param direction sort direction, one of "asc" or "desc" + */ + setSort: function(sort, direction) { + var comparator = this.Comparators[sort] || this.Comparators.name; + this._sort = sort; + this._sortDirection = (direction === 'desc')?'desc':'asc'; + this._sortComparator = comparator; + if (direction === 'desc') { + this._sortComparator = function(fileInfo1, fileInfo2) { + return -comparator(fileInfo1, fileInfo2); + }; + } + this.$el.find('thead th .sort-indicator') + .removeClass(this.SORT_INDICATOR_ASC_CLASS + ' ' + this.SORT_INDICATOR_DESC_CLASS); + this.$el.find('thead th.column-' + sort + ' .sort-indicator') + .addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS); + }, /** * @brief Reloads the file list using ajax call */ reload: function() { + this._selectedFiles = {}; + this._selectionSummary.clear(); + this.$el.find('#select_all').prop('checked', false); FileList.showMask(); if (FileList._reloadCall) { FileList._reloadCall.abort(); @@ -733,7 +784,9 @@ window.FileList = { FileList._reloadCall = $.ajax({ url: Files.getAjaxUrl('list'), data: { - dir : $('#dir').val() + dir: $('#dir').val(), + sort: FileList._sort, + sortdirection: FileList._sortDirection }, error: function(result) { FileList.reloadCallback(result); @@ -859,7 +912,7 @@ window.FileList = { */ _findInsertionIndex: function(fileData) { var index = 0; - while (index < this.files.length && this._fileInfoCompare(fileData, this.files[index]) > 0) { + while (index < this.files.length && this._sortComparator(fileData, this.files[index]) > 0) { index++; } return index; @@ -924,7 +977,7 @@ window.FileList = { OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); } $td.css('background-image', oldBackgroundImage); - }); + }); }); }, @@ -1218,15 +1271,15 @@ window.FileList = { updateSelectionSummary: function() { var summary = this._selectionSummary.summary; if (summary.totalFiles === 0 && summary.totalDirs === 0) { - $('#headerName span.name').text(t('files','Name')); - $('#headerSize').text(t('files','Size')); - $('#modified').text(t('files','Modified')); + $('#headerName a.name>span:first').text(t('files','Name')); + $('#headerSize a>span:first').text(t('files','Size')); + $('#modified a>span:first').text(t('files','Modified')); $('table').removeClass('multiselect'); $('.selectedActions').addClass('hidden'); } else { $('.selectedActions').removeClass('hidden'); - $('#headerSize').text(OC.Util.humanFileSize(summary.totalSize)); + $('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize)); var selection = ''; if (summary.totalDirs > 0) { selection += n('files', '%n folder', '%n folders', summary.totalDirs); @@ -1237,8 +1290,8 @@ window.FileList = { if (summary.totalFiles > 0) { selection += n('files', '%n file', '%n files', summary.totalFiles); } - $('#headerName span.name').text(selection); - $('#modified').text(''); + $('#headerName a.name>span:first').text(selection); + $('#modified a>span:first').text(''); $('table').addClass('multiselect'); } }, @@ -1542,3 +1595,49 @@ $(document).ready(function() { }, 0); }); +/** + * Sort comparators. + */ +FileList.Comparators = { + /** + * Compares two file infos by name, making directories appear + * first. + * + * @param fileInfo1 file info + * @param fileInfo2 file info + * @return -1 if the first file must appear before the second one, + * 0 if they are identify, 1 otherwise. + */ + name: function(fileInfo1, fileInfo2) { + if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') { + return -1; + } + if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') { + return 1; + } + return fileInfo1.name.localeCompare(fileInfo2.name); + }, + /** + * Compares two file infos by size. + * + * @param fileInfo1 file info + * @param fileInfo2 file info + * @return -1 if the first file must appear before the second one, + * 0 if they are identify, 1 otherwise. + */ + size: function(fileInfo1, fileInfo2) { + return fileInfo1.size - fileInfo2.size; + }, + /** + * Compares two file infos by timestamp. + * + * @param fileInfo1 file info + * @param fileInfo2 file info + * @return -1 if the first file must appear before the second one, + * 0 if they are identify, 1 otherwise. + */ + mtime: function(fileInfo1, fileInfo2) { + return fileInfo1.mtime - fileInfo2.mtime; + } +}; + diff --git a/apps/files/lib/helper.php b/apps/files/lib/helper.php index 0ae87d12fbfb..7e0f47bf84fc 100644 --- a/apps/files/lib/helper.php +++ b/apps/files/lib/helper.php @@ -1,7 +1,16 @@ + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ namespace OCA\Files; +/** + * Helper class for manipulating file information + */ class Helper { public static function buildFileStorageStatistics($dir) { @@ -9,12 +18,12 @@ public static function buildFileStorageStatistics($dir) { $storageInfo = \OC_Helper::getStorageInfo($dir); $l = new \OC_L10N('files'); - $maxUploadFilesize = \OCP\Util::maxUploadFilesize($dir, $storageInfo['free']); - $maxHumanFilesize = \OCP\Util::humanFileSize($maxUploadFilesize); - $maxHumanFilesize = $l->t('Upload (max. %s)', array($maxHumanFilesize)); + $maxUploadFileSize = \OCP\Util::maxUploadFilesize($dir, $storageInfo['free']); + $maxHumanFileSize = \OCP\Util::humanFileSize($maxUploadFileSize); + $maxHumanFileSize = $l->t('Upload (max. %s)', array($maxHumanFileSize)); - return array('uploadMaxFilesize' => $maxUploadFilesize, - 'maxHumanFilesize' => $maxHumanFilesize, + return array('uploadMaxFilesize' => $maxUploadFileSize, + 'maxHumanFilesize' => $maxHumanFileSize, 'freeSpace' => $storageInfo['free'], 'usedSpacePercent' => (int)$storageInfo['relative']); } @@ -27,7 +36,6 @@ public static function buildFileStorageStatistics($dir) { */ public static function determineIcon($file) { if($file['type'] === 'dir') { - $dir = $file['directory']; $icon = \OC_Helper::mimetypeIcon('dir'); $absPath = $file->getPath(); $mount = \OC\Files\Filesystem::getMountManager()->find($absPath); @@ -57,7 +65,7 @@ public static function determineIcon($file) { * @param \OCP\Files\FileInfo $b file * @return int -1 if $a must come before $b, 1 otherwise */ - public static function fileCmp($a, $b) { + public static function compareFileNames($a, $b) { $aType = $a->getType(); $bType = $b->getType(); if ($aType === 'dir' and $bType !== 'dir') { @@ -69,6 +77,32 @@ public static function fileCmp($a, $b) { } } + /** + * Comparator function to sort files by date + * + * @param \OCP\Files\FileInfo $a file + * @param \OCP\Files\FileInfo $b file + * @return int -1 if $a must come before $b, 1 otherwise + */ + public static function compareTimestamp($a, $b) { + $aTime = $a->getMTime(); + $bTime = $b->getMTime(); + return $aTime - $bTime; + } + + /** + * Comparator function to sort files by size + * + * @param \OCP\Files\FileInfo $a file + * @param \OCP\Files\FileInfo $b file + * @return int -1 if $a must come before $b, 1 otherwise + */ + public static function compareSize($a, $b) { + $aSize = $a->getSize(); + $bSize = $b->getSize(); + return $aSize - $bSize; + } + /** * Formats the file info to be returned as JSON to the client. * @@ -120,12 +154,35 @@ public static function formatFileInfos($fileInfos) { * returns it as a sorted array of FileInfo. * * @param string $dir path to the directory + * @param string $sortAttribute attribute to sort on + * @param bool $sortDescending true for descending sort, false otherwise * @return \OCP\Files\FileInfo[] files */ - public static function getFiles($dir) { + public static function getFiles($dir, $sortAttribute = 'name', $sortDescending = false) { $content = \OC\Files\Filesystem::getDirectoryContent($dir); - usort($content, array('\OCA\Files\Helper', 'fileCmp')); - return $content; + return self::sortFiles($content, $sortAttribute, $sortDescending); + } + + /** + * Sort the given file info array + * + * @param \OCP\Files\FileInfo[] files to sort + * @param string $sortAttribute attribute to sort on + * @param bool $sortDescending true for descending sort, false otherwise + * @return \OCP\Files\FileInfo[] sorted files + */ + public static function sortFiles($files, $sortAttribute = 'name', $sortDescending = false) { + $sortFunc = 'compareFileNames'; + if ($sortAttribute === 'mtime') { + $sortFunc = 'compareTimestamp'; + } else if ($sortAttribute === 'size') { + $sortFunc = 'compareSize'; + } + usort($files, array('\OCA\Files\Helper', $sortFunc)); + if ($sortDescending) { + $files = array_reverse($files); + } + return $files; } } diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php index 42263c880a7b..9c593a9341d8 100644 --- a/apps/files/templates/index.php +++ b/apps/files/templates/index.php @@ -1,3 +1,4 @@ +