diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index 474f1af0720f52fd73b9e1deee3a151825644a49..533050691d5e75b94bce6bcf047eec6828dfa1ea 100644
--- a/apps/files/css/files.css
+++ b/apps/files/css/files.css
@@ -310,7 +310,6 @@ a.action>img { max-height:16px; max-width:16px; vertical-align:text-bottom; }
 
 /* Actions for selected files */
 .selectedActions {
-	display: none;
 	position: absolute;
 	top: -1px;
 	right: 0;
diff --git a/apps/files/index.php b/apps/files/index.php
index b8ff08c1b05573dadcd74aaeac5dc4d71581f625..a4e9a938507c36541cb7df9f0f600d577095074a 100644
--- a/apps/files/index.php
+++ b/apps/files/index.php
@@ -32,6 +32,7 @@ OCP\Util::addscript('files', 'file-upload');
 OCP\Util::addscript('files', 'jquery.iframe-transport');
 OCP\Util::addscript('files', 'jquery.fileupload');
 OCP\Util::addscript('files', 'jquery-visibility');
+OCP\Util::addscript('files', 'filesummary');
 OCP\Util::addscript('files', 'breadcrumb');
 OCP\Util::addscript('files', 'filelist');
 
diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js
index 03ebdccb32df25ad3b85d83e972e9508ca5db202..963fc6478287fa5f26ef2d426c2fbfbcefcbdf36 100644
--- a/apps/files/js/file-upload.js
+++ b/apps/files/js/file-upload.js
@@ -606,7 +606,7 @@ OC.Upload = {
 								{dir:$('#dir').val(), filename:name},
 								function(result) {
 									if (result.status === 'success') {
-										FileList.add(result.data, {hidden: hidden, insert: true});
+										FileList.add(result.data, {hidden: hidden, animate: true});
 									} else {
 										OC.dialogs.alert(result.data.message, t('core', 'Could not create file'));
 									}
@@ -619,7 +619,7 @@ OC.Upload = {
 								{dir:$('#dir').val(), foldername:name},
 								function(result) {
 									if (result.status === 'success') {
-										FileList.add(result.data, {hidden: hidden, insert: true});
+										FileList.add(result.data, {hidden: hidden, animate: true});
 									} else {
 										OC.dialogs.alert(result.data.message, t('core', 'Could not create folder'));
 									}
@@ -657,7 +657,7 @@ OC.Upload = {
 								var file = data;
 								$('#uploadprogressbar').fadeOut();
 
-								FileList.add(file, {hidden: hidden, insert: true});
+								FileList.add(file, {hidden: hidden, animate: true});
 							});
 							eventSource.listen('error',function(error) {
 								$('#uploadprogressbar').fadeOut();
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index c33b638b5a6520cdc98cdda4fd2a0318da725806..40ec898635e426ca16fd68c4a6939c301d8828d1 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -8,8 +8,8 @@
  *
  */
 
-/* global OC, t, n, FileList, FileActions, Files, BreadCrumb */
-/* global procesSelection, dragOptions, folderDropOptions */
+/* global OC, t, n, FileList, FileActions, Files, FileSummary, BreadCrumb */
+/* global dragOptions, folderDropOptions */
 window.FileList = {
 	appName: t('files', 'Files'),
 	isEmpty: true,
@@ -17,8 +17,47 @@ window.FileList = {
 	$el: $('#filestable'),
 	$fileList: $('#fileList'),
 	breadcrumb: null,
+
+	/**
+	 * Instance of FileSummary
+	 */
+	fileSummary: null,
 	initialized: false,
 
+	// number of files per page
+	pageSize: 20,
+
+	/**
+	 * Array of files in the current folder.
+	 * The entries are of file data.
+	 */
+	files: [],
+
+	/**
+	 * Map of file id to file data
+	 */
+	_selectedFiles: {},
+
+	/**
+	 * Summary of selected files.
+	 * Instance of FileSummary.
+	 */
+	_selectionSummary: null,
+
+	/**
+	 * Compare two file info objects, sorting by
+	 * folders first, then by name.
+	 */
+	_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);
+	},
+
 	/**
 	 * Initialize the file list and its components
 	 */
@@ -31,10 +70,15 @@ window.FileList = {
 		// TODO: FileList should not know about global elements
 		this.$el = $('#filestable');
 		this.$fileList = $('#fileList');
+		this.files = [];
+		this._selectedFiles = {};
+		this._selectionSummary = new FileSummary();
+
+		this.fileSummary = this._createSummary();
 
 		this.breadcrumb = new BreadCrumb({
 			onClick: this._onClickBreadCrumb,
-			onDrop: this._onDropOnBreadCrumb,
+			onDrop: _.bind(this._onDropOnBreadCrumb, this),
 			getCrumbUrl: function(part, index) {
 				return self.linkTo(part.dir);
 			}
@@ -47,6 +91,149 @@ window.FileList = {
 			var width = $(this).width();
 			FileList.breadcrumb.resize(width, false);
 		});
+
+		this.$fileList.on('click','td.filename a', _.bind(this._onClickFile, this));
+		this.$fileList.on('change', 'td.filename input:checkbox', _.bind(this._onClickFileCheckbox, this));
+		this.$el.find('#select_all').click(_.bind(this._onClickSelectAll, this));
+		this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this));
+		this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this));
+	},
+
+	/**
+	 * Selected/deselects the given file element and updated
+	 * the internal selection cache.
+	 *
+	 * @param $tr single file row element
+	 * @param state true to select, false to deselect
+	 */
+	_selectFileEl: function($tr, state) {
+		var $checkbox = $tr.find('input:checkbox');
+		var oldData = !!this._selectedFiles[$tr.data('id')];
+		var data;
+		$checkbox.prop('checked', state);
+		$tr.toggleClass('selected', state);
+		// already selected ?
+		if (state === oldData) {
+			return;
+		}
+		data = this.elementToFile($tr);
+		if (state) {
+			this._selectedFiles[$tr.data('id')] = data;
+			this._selectionSummary.add(data);
+		}
+		else {
+			delete this._selectedFiles[$tr.data('id')];
+			this._selectionSummary.remove(data);
+		}
+		this.$el.find('#select_all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
+	},
+
+	/**
+	 * Event handler for when clicking on files to select them
+	 */
+	_onClickFile: function(event) {
+		var $tr = $(event.target).closest('tr');
+		if (event.ctrlKey || event.shiftKey) {
+			event.preventDefault();
+			if (event.shiftKey) {
+				var $lastTr = $(this._lastChecked);
+				var lastIndex = $lastTr.index();
+				var currentIndex = $tr.index();
+				var $rows = this.$fileList.children('tr');
+
+				// last clicked checkbox below current one ?
+				if (lastIndex > currentIndex) {
+					var aux = lastIndex;
+					lastIndex = currentIndex;
+					currentIndex = aux;
+				}
+
+				// auto-select everything in-between
+				for (var i = lastIndex + 1; i < currentIndex; i++) {
+					this._selectFileEl($rows.eq(i), true);
+				}
+			}
+			else {
+				this._lastChecked = $tr;
+			}
+			var $checkbox = $tr.find('input:checkbox');
+			this._selectFileEl($tr, !$checkbox.prop('checked'));
+			this.updateSelectionSummary();
+		} else {
+			var filename = $tr.attr('data-file');
+			var renaming = $tr.data('renaming');
+			if (!renaming) {
+				FileActions.currentFile = $tr.find('td');
+				var mime=FileActions.getCurrentMimeType();
+				var type=FileActions.getCurrentType();
+				var permissions = FileActions.getCurrentPermissions();
+				var action=FileActions.getDefault(mime,type, permissions);
+				if (action) {
+					event.preventDefault();
+					action(filename);
+				}
+			}
+		}
+	},
+
+	/**
+	 * Event handler for when clicking on a file's checkbox
+	 */
+	_onClickFileCheckbox: function(e) {
+		var $tr = $(e.target).closest('tr');
+		this._selectFileEl($tr, !$tr.hasClass('selected'));
+		this._lastChecked = $tr;
+		this.updateSelectionSummary();
+	},
+
+	/**
+	 * Event handler for when selecting/deselecting all files
+	 */
+	_onClickSelectAll: function(e) {
+		var checked = $(e.target).prop('checked');
+		this.$fileList.find('td.filename input:checkbox').prop('checked', checked)
+			.closest('tr').toggleClass('selected', checked);
+		this._selectedFiles = {};
+		this._selectionSummary.clear();
+		if (checked) {
+			for (var i = 0; i < this.files.length; i++) {
+				var fileData = this.files[i];
+				this._selectedFiles[fileData.id] = fileData;
+				this._selectionSummary.add(fileData);
+			}
+		}
+		this.updateSelectionSummary();
+	},
+
+	/**
+	 * Event handler for when clicking on "Download" for the selected files
+	 */
+	_onClickDownloadSelected: function(event) {
+		var files;
+		var dir = this.getCurrentDirectory();
+		if (this.isAllSelected()) {
+			files = OC.basename(dir);
+			dir = OC.dirname(dir) || '/';
+		}
+		else {
+			files = _.pluck(this.getSelectedFiles(), 'name');
+		}
+		OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.'));
+		OC.redirect(Files.getDownloadUrl(files, dir));
+		return false;
+	},
+
+	/**
+	 * Event handler for when clicking on "Delete" for the selected files
+	 */
+	_onClickDeleteSelected: function(event) {
+		var files = null;
+		if (!FileList.isAllSelected()) {
+			files = _.pluck(this.getSelectedFiles(), 'name');
+		}
+		this.do_delete(files);
+		event.preventDefault();
+		return false;
 	},
 
 	/**
@@ -62,48 +249,41 @@ window.FileList = {
 		}
 	},
 
+	_onScroll: function(e) {
+		if ($(window).scrollTop() + $(window).height() > $(document).height() - 500) {
+			this._nextPage(true);
+		}
+	},
+
 	/**
 	 * Event handler when dropping on a breadcrumb
 	 */
 	_onDropOnBreadCrumb: function( event, ui ) {
-		var target=$(this).data('dir');
-		var dir = FileList.getCurrentDirectory();
-		while(dir.substr(0,1) === '/') {//remove extra leading /'s
-			dir=dir.substr(1);
+		var $target = $(event.target);
+		if (!$target.is('.crumb')) {
+			$target = $target.closest('.crumb');
+		}
+		var targetPath = $(event.target).data('dir');
+		var dir = this.getCurrentDirectory();
+		while (dir.substr(0,1) === '/') {//remove extra leading /'s
+			dir = dir.substr(1);
 		}
 		dir = '/' + dir;
 		if (dir.substr(-1,1) !== '/') {
 			dir = dir + '/';
 		}
-		if (target === dir || target+'/' === dir) {
+		// do nothing if dragged on current dir
+		if (targetPath === dir || targetPath + '/' === dir) {
 			return;
 		}
-		var files = ui.helper.find('tr');
-		$(files).each(function(i,row) {
-			var dir = $(row).data('dir');
-			var file = $(row).data('filename');
-			//slapdash selector, tracking down our original element that the clone budded off of.
-			var origin = $('tr[data-id=' + $(row).data('origin') + ']');
-			var td = origin.children('td.filename');
-			var oldBackgroundImage = td.css('background-image');
-			td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
-			$.post(OC.filePath('files', 'ajax', 'move.php'), { dir: dir, file: file, target: target }, function(result) {
-				if (result) {
-					if (result.status === 'success') {
-						FileList.remove(file);
-						procesSelection();
-						$('#notification').hide();
-					} else {
-						$('#notification').hide();
-						$('#notification').text(result.data.message);
-						$('#notification').fadeIn();
-					}
-				} else {
-					OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error'));
-				}
-				td.css('background-image', oldBackgroundImage);
-			});
-		});
+
+		var files = this.getSelectedFiles();
+		if (files.length === 0) {
+			// single one selected without checkbox?
+			files = _.map(ui.helper.find('tr'), FileList.elementToFile);
+		}
+
+		FileList.move(_.pluck(files, 'name'), targetPath);
 	},
 
 	/**
@@ -129,21 +309,83 @@ window.FileList = {
 		// use filterAttr to avoid escaping issues
 		return this.$fileList.find('tr').filterAttr('data-file', fileName);
 	},
+
+	/**
+	 * Returns the file data from a given file element.
+	 * @param $el file tr element
+	 * @return file data
+	 */
+	elementToFile: function($el){
+		$el = $($el);
+		return {
+			id: parseInt($el.attr('data-id'), 10),
+			name: $el.attr('data-file'),
+			mimetype: $el.attr('data-mime'),
+			type: $el.attr('data-type'),
+			size: parseInt($el.attr('data-size'), 10),
+			etag: $el.attr('data-etag')
+		};
+	},
+
+	/**
+	 * Appends the next page of files into the table
+	 * @param animate true to animate the new elements
+	 */
+	_nextPage: function(animate) {
+		var index = this.$fileList.children().length,
+			count = this.pageSize,
+			tr,
+			fileData,
+			newTrs = [],
+			isAllSelected = this.isAllSelected();
+
+		if (index >= this.files.length) {
+			return;
+		}
+
+		while (count > 0 && index < this.files.length) {
+			fileData = this.files[index];
+			tr = this._renderRow(fileData, {updateSummary: false});
+			this.$fileList.append(tr);
+			if (isAllSelected || this._selectedFiles[fileData.id]) {
+				tr.addClass('selected');
+				tr.find('input:checkbox').prop('checked', true);
+			}
+			if (animate) {
+				tr.addClass('appear transparent');
+				newTrs.push(tr);
+			}
+			index++;
+			count--;
+		}
+
+		if (animate) {
+			// defer, for animation
+			window.setTimeout(function() {
+				for (var i = 0; i < newTrs.length; i++ ) {
+					newTrs[i].removeClass('transparent');
+				}
+			}, 0);
+		}
+	},
+
 	/**
 	 * Sets the files to be displayed in the list.
-	 * This operation will rerender the list and update the summary.
+	 * This operation will re-render the list and update the summary.
 	 * @param filesArray array of file data (map)
 	 */
-	setFiles:function(filesArray) {
+	setFiles: function(filesArray) {
 		// detach to make adding multiple rows faster
-		this.$fileList.detach();
+		this.files = filesArray;
 
+		this.$fileList.detach();
 		this.$fileList.empty();
 
-		this.isEmpty = filesArray.length === 0;
-		for (var i = 0; i < filesArray.length; i++) {
-			this.add(filesArray[i], {updateSummary: false});
-		}
+		// clear "Select all" checkbox
+		this.$el.find('#select_all').prop('checked', false);
+
+		this.isEmpty = this.files.length === 0;
+		this._nextPage();
 
 		this.$el.find('thead').after(this.$fileList);
 
@@ -153,8 +395,10 @@ window.FileList = {
 		if (window.Files) {
 			Files.setupDragAndDrop();
 		}
-		this.updateFileSummary();
-		procesSelection();
+
+		this.fileSummary.calculate(filesArray);
+
+		FileList.updateSelectionSummary();
 		$(window).scrollTop(0);
 
 		this.$fileList.trigger(jQuery.Event("updated"));
@@ -276,15 +520,82 @@ window.FileList = {
 		tr.append(td);
 		return tr;
 	},
+
 	/**
-	 * Adds an entry to the files table using the data from the given file data
+	 * Adds an entry to the files array and also into the DOM
+	 * in a sorted manner.
+	 *
 	 * @param fileData map of file attributes
 	 * @param options map of attributes:
-	 * - "insert" true to insert in a sorted manner, false to append (default)
 	 * - "updateSummary" true to update the summary after adding (default), false otherwise
 	 * @return new tr element (not appended to the table)
 	 */
 	add: function(fileData, options) {
+		var index = -1;
+		var $tr;
+		var $rows;
+		var $insertionPoint;
+		options = options || {};
+
+		// there are three situations to cover:
+		// 1) insertion point is visible on the current page
+		// 2) insertion point is on a not visible page (visible after scrolling)
+		// 3) insertion point is at the end of the list
+
+		$rows = this.$fileList.children();
+		index = this._findInsertionIndex(fileData);
+		if (index > this.files.length) {
+			index = this.files.length;
+		}
+		else {
+			$insertionPoint = $rows.eq(index);
+		}
+
+		// is the insertion point visible ?
+		if ($insertionPoint.length) {
+			// only render if it will really be inserted
+			$tr = this._renderRow(fileData, options);
+			$insertionPoint.before($tr);
+		}
+		else {
+			// if insertion point is after the last visible
+			// entry, append
+			if (index === $rows.length) {
+				$tr = this._renderRow(fileData, options);
+				this.$fileList.append($tr);
+			}
+		}
+
+		this.isEmpty = false;
+		this.files.splice(index, 0, fileData);
+
+		if ($tr && options.animate) {
+			$tr.addClass('appear transparent');
+			window.setTimeout(function() {
+				$tr.removeClass('transparent');
+			});
+		}
+
+		// defaults to true if not defined
+		if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
+			this.fileSummary.add(fileData, true);
+			this.updateEmptyContent();
+		}
+
+		return $tr;
+	},
+
+	/**
+	 * Creates a new row element based on the given attributes
+	 * and returns it.
+	 *
+	 * @param fileData map of file attributes
+	 * @param options map of attributes:
+	 * - "index" optional index at which to insert the element
+	 * - "updateSummary" true to update the summary after adding (default), false otherwise
+	 * @return new tr element (not appended to the table)
+	 */
+	_renderRow: function(fileData, options) {
 		options = options || {};
 		var type = fileData.type || 'file',
 			mime = fileData.mimetype,
@@ -303,16 +614,6 @@ window.FileList = {
 		);
 		var filenameTd = tr.find('td.filename');
 
-		// sorted insert is expensive, so needs to be explicitly
-		// requested
-		if (options.insert) {
-			this.insertElement(fileData.name, type, tr);
-		}
-		else {
-			this.$fileList.append(tr);
-		}
-		FileList.isEmpty = false;
-
 		// TODO: move dragging to FileActions ?
 		// enable drag only for deletable files
 		if (permissions & OC.PERMISSION_DELETE) {
@@ -348,12 +649,6 @@ window.FileList = {
 				filenameTd.css('background-image', 'url(' + previewUrl + ')');
 			}
 		}
-
-		// defaults to true if not defined
-		if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
-			this.updateFileSummary();
-			this.updateEmptyContent();
-		}
 		return tr;
 	},
 	/**
@@ -378,7 +673,6 @@ window.FileList = {
 	 */
 	changeDirectory: function(targetDir, changeUrl, force) {
 		var $dir = $('#dir'),
-			url,
 			currentDir = $dir.val() || '/';
 		targetDir = targetDir || '/';
 		if (!force && currentDir === targetDir) {
@@ -391,7 +685,9 @@ window.FileList = {
 				previousDir: currentDir
 			}
 		));
-		FileList.reload();
+		this._selectedFiles = {};
+		this._selectionSummary.clear();
+		this.reload();
 	},
 	linkTo: function(dir) {
 		return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
@@ -519,60 +815,130 @@ window.FileList = {
 	 * @param name name of the file to remove
 	 * @param options optional options as map:
 	 * "updateSummary": true to update the summary (default), false otherwise
+	 * @return deleted element
 	 */
-	remove:function(name, options){
+	remove: function(name, options){
 		options = options || {};
 		var fileEl = FileList.findFileEl(name);
+		var index = fileEl.index();
+		if (!fileEl.length) {
+			return null;
+		}
+		if (this._selectedFiles[fileEl.data('id')]) {
+			// remove from selection first
+			this._selectFileEl(fileEl, false);
+			this.updateSelectionSummary();
+		}
 		if (fileEl.data('permissions') & OC.PERMISSION_DELETE) {
 			// file is only draggable when delete permissions are set
 			fileEl.find('td.filename').draggable('destroy');
 		}
+		this.files.splice(index, 1);
 		fileEl.remove();
 		// TODO: improve performance on batch update
-		FileList.isEmpty = !this.$fileList.find('tr:not(.summary)').length;
+		FileList.isEmpty = !this.files.length;
 		if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
 			FileList.updateEmptyContent();
-			FileList.updateFileSummary();
+			this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, true);
+		}
+
+		var lastIndex = this.$fileList.children().length;
+		// if there are less elements visible than one page
+		// but there are still pending elements in the array,
+		// then directly append the next page
+		if (lastIndex < this.files.length && lastIndex < this.pageSize) {
+			this._nextPage(true);
 		}
+
 		return fileEl;
 	},
-	insertElement:function(name, type, element) {
-		// find the correct spot to insert the file or folder
-		var pos,
-			fileElements = this.$fileList.find('tr[data-file][data-type="'+type+'"]:not(.hidden)');
-		if (name.localeCompare($(fileElements[0]).attr('data-file')) < 0) {
-			pos = -1;
-		} else if (name.localeCompare($(fileElements[fileElements.length-1]).attr('data-file')) > 0) {
-			pos = fileElements.length - 1;
-		} else {
-			for(pos = 0; pos<fileElements.length-1; pos++) {
-				if (name.localeCompare($(fileElements[pos]).attr('data-file')) > 0
-					&& name.localeCompare($(fileElements[pos+1]).attr('data-file')) < 0)
-				{
-					break;
-				}
-			}
-		}
-		if (fileElements.exists()) {
-			if (pos === -1) {
-				$(fileElements[0]).before(element);
-			} else {
-				$(fileElements[pos]).after(element);
-			}
-		} else if (type === 'dir' && !FileList.isEmpty) {
-			this.$fileList.find('tr[data-file]:first').before(element);
-		} else if (type === 'file' && !FileList.isEmpty) {
-			this.$fileList.find('tr[data-file]:last').before(element);
-		} else {
-			this.$fileList.append(element);
+	/**
+	 * Finds the index of the row before which the given
+	 * fileData should be inserted, considering the current
+	 * sorting
+	 */
+	_findInsertionIndex: function(fileData) {
+		var index = 0;
+		while (index < this.files.length && this._fileInfoCompare(fileData, this.files[index]) > 0) {
+			index++;
 		}
-		FileList.isEmpty = false;
-		FileList.updateEmptyContent();
-		FileList.updateFileSummary();
+		return index;
 	},
+	/**
+	 * Moves a file to a given target folder.
+	 *
+	 * @param fileNames array of file names to move
+	 * @param targetPath absolute target path
+	 */
+	move: function(fileNames, targetPath) {
+		var self = this;
+		var dir = this.getCurrentDirectory();
+		var target = OC.basename(targetPath);
+		if (!_.isArray(fileNames)) {
+			fileNames = [fileNames];
+		}
+		_.each(fileNames, function(fileName) {
+			var $tr = self.findFileEl(fileName);
+			var $td = $tr.children('td.filename');
+			var oldBackgroundImage = $td.css('background-image');
+			$td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
+			// TODO: improve performance by sending all file names in a single call
+			$.post(
+				OC.filePath('files', 'ajax', 'move.php'),
+				{
+					dir: dir,
+					file: fileName,
+					target: targetPath
+				},
+				function(result) {
+					if (result) {
+						if (result.status === 'success') {
+							// if still viewing the same directory
+							if (self.getCurrentDirectory() === dir) {
+								// recalculate folder size
+								var oldFile = self.findFileEl(target);
+								var newFile = self.findFileEl(fileName);
+								var oldSize = oldFile.data('size');
+								var newSize = oldSize + newFile.data('size');
+								oldFile.data('size', newSize);
+								oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
+
+								// TODO: also update entry in FileList.files
+
+								self.remove(fileName);
+							}
+						} else {
+							OC.Notification.hide();
+							if (result.status === 'error' && result.data.message) {
+								OC.Notification.show(result.data.message);
+							}
+							else {
+								OC.Notification.show(t('files', 'Error moving file.'));
+							}
+							// hide notification after 10 sec
+							setTimeout(function() {
+								OC.Notification.hide();
+							}, 10000);
+						}
+					} else {
+						OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error'));
+					}
+					$td.css('background-image', oldBackgroundImage);
+			});
+		});
+
+	},
+
+	/**
+	 * Triggers file rename input field for the given file name.
+	 * If the user enters a new name, the file will be renamed.
+	 *
+	 * @param oldname file name of the file to rename
+	 */
 	rename: function(oldname) {
 		var tr, td, input, form;
 		tr = FileList.findFileEl(oldname);
+		var oldFileInfo = this.files[tr.index()];
 		tr.data('renaming',true);
 		td = tr.children('td.filename');
 		input = $('<input type="text" class="filename"/>').val(oldname);
@@ -604,86 +970,50 @@ window.FileList = {
 			event.stopPropagation();
 			event.preventDefault();
 			try {
-				var newname = input.val();
-				var directory = FileList.getCurrentDirectory();
-				if (newname !== oldname) {
+				var newName = input.val();
+				if (newName !== oldname) {
 					checkInput();
-					// save background image, because it's replaced by a spinner while async request
-					var oldBackgroundImage = td.css('background-image');
 					// mark as loading
 					td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
 					$.ajax({
 						url: OC.filePath('files','ajax','rename.php'),
 						data: {
 							dir : $('#dir').val(),
-							newname: newname,
+							newname: newName,
 							file: oldname
 						},
 						success: function(result) {
+							var fileInfo;
 							if (!result || result.status === 'error') {
 								OC.dialogs.alert(result.data.message, t('core', 'Could not rename file'));
-								// revert changes
-								newname = oldname;
-								tr.attr('data-file', newname);
-								var path = td.children('a.name').attr('href');
-								td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newname)));
-								var basename = newname;
-								if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') {
-									basename = newname.substr(0,newname.lastIndexOf('.'));
-								}
-								td.find('a.name span.nametext').text(basename);
-								if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') {
-									if ( ! td.find('a.name span.extension').exists() ) {
-										td.find('a.name span.nametext').append('<span class="extension"></span>');
-									}
-									td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.')));
-								}
-								tr.find('.fileactions').effect('highlight', {}, 5000);
-								tr.effect('highlight', {}, 5000);
-								// remove loading mark and recover old image
-								td.css('background-image', oldBackgroundImage);
+								fileInfo = oldFileInfo;
 							}
 							else {
-								var fileInfo = result.data;
-								tr.attr('data-mime', fileInfo.mime);
-								tr.attr('data-etag', fileInfo.etag);
-								if (fileInfo.isPreviewAvailable) {
-									Files.lazyLoadPreview(directory + '/' + fileInfo.name, result.data.mime, function(previewpath) {
-										tr.find('td.filename').attr('style','background-image:url('+previewpath+')');
-									}, null, null, result.data.etag);
-								}
-								else {
-									tr.find('td.filename')
-										.removeClass('preview')
-										.attr('style','background-image:url('
-												+ OC.Util.replaceSVGIcon(fileInfo.icon)
-												+ ')');
-								}
+								fileInfo = result.data;
 							}
 							// reinsert row
-							tr.detach();
-							FileList.insertElement( tr.attr('data-file'), tr.attr('data-type'),tr );
-							// update file actions in case the extension changed
-							FileActions.display( tr.find('td.filename'), true);
+							FileList.files.splice(tr.index(), 1);
+							tr.remove();
+							FileList.add(fileInfo);
 						}
 					});
 				}
 				input.tipsy('hide');
 				tr.data('renaming',false);
-				tr.attr('data-file', newname);
+				tr.attr('data-file', newName);
 				var path = td.children('a.name').attr('href');
 				// FIXME this will fail if the path contains the filename.
-				td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newname)));
-				var basename = newname;
-				if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') {
-					basename = newname.substr(0, newname.lastIndexOf('.'));
+				td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newName)));
+				var basename = newName;
+				if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
+					basename = newName.substr(0, newName.lastIndexOf('.'));
 				}
 				td.find('a.name span.nametext').text(basename);
-				if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') {
+				if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
 					if ( ! td.find('a.name span.extension').exists() ) {
 						td.find('a.name span.nametext').append('<span class="extension"></span>');
 					}
-					td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.')));
+					td.find('a.name span.extension').text(newName.substr(newName.lastIndexOf('.')));
 				}
 				form.remove();
 				FileActions.display( tr.find('td.filename'), true);
@@ -764,20 +1094,22 @@ window.FileList = {
 				function(result) {
 					if (result.status === 'success') {
 						if (params.allfiles) {
-							// clear whole list
-							$('#fileList tr').remove();
+							FileList.setFiles([]);
 						}
 						else {
 							$.each(files,function(index,file) {
 								var fileEl = FileList.remove(file, {updateSummary: false});
+								// FIXME: not sure why we need this after the
+								// element isn't even in the DOM any more
 								fileEl.find('input[type="checkbox"]').prop('checked', false);
 								fileEl.removeClass('selected');
+								FileList.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')});
 							});
 						}
-						procesSelection();
 						checkTrashStatus();
-						FileList.updateFileSummary();
 						FileList.updateEmptyContent();
+						FileList.fileSummary.update();
+						FileList.updateSelectionSummary();
 						Files.updateStorageStatistics();
 					} else {
 						if (result.status === 'error' && result.data.message) {
@@ -804,108 +1136,14 @@ window.FileList = {
 					}
 				});
 	},
-	createFileSummary: function() {
-		if ( !FileList.isEmpty ) {
-			var summary = this._calculateFileSummary();
-
-			// Get translations
-			var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
-			var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
-
-			var infoVars = {
-				dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">',
-				files: '</span><span class="fileinfo">'+fileInfo+'</span>'
-			};
-
-			var info = t('files', '{dirs} and {files}', infoVars);
-
-			// don't show the filesize column, if filesize is NaN (e.g. in trashbin)
-			var fileSize = '';
-			if (!isNaN(summary.totalSize)) {
-				fileSize = '<td class="filesize">'+humanFileSize(summary.totalSize)+'</td>';
-			}
-
-			var $summary = $('<tr class="summary" data-file="undefined"><td><span class="info">'+info+'</span></td>'+fileSize+'<td></td></tr>');
-			this.$fileList.append($summary);
-
-			var $dirInfo = $summary.find('.dirinfo');
-			var $fileInfo = $summary.find('.fileinfo');
-			var $connector = $summary.find('.connector');
+	/**
+	 * Creates the file summary section
+	 */
+	_createSummary: function() {
+		var $tr = $('<tr class="summary"></tr>');
+		this.$el.find('tfoot').append($tr);
 
-			// Show only what's necessary, e.g.: no files: don't show "0 files"
-			if (summary.totalDirs === 0) {
-				$dirInfo.addClass('hidden');
-				$connector.addClass('hidden');
-			}
-			if (summary.totalFiles === 0) {
-				$fileInfo.addClass('hidden');
-				$connector.addClass('hidden');
-			}
-		}
-	},
-	_calculateFileSummary: function() {
-		var result = {
-			totalDirs: 0,
-			totalFiles: 0,
-			totalSize: 0
-		};
-		$.each($('tr[data-file]'), function(index, value) {
-			var $value = $(value);
-			if ($value.data('type') === 'dir') {
-				result.totalDirs++;
-			} else if ($value.data('type') === 'file') {
-				result.totalFiles++;
-			}
-			if ($value.data('size') !== undefined && $value.data('id') !== -1) {
-				//Skip shared as it does not count toward quota
-				result.totalSize += parseInt($value.data('size'));
-			}
-		});
-		return result;
-	},
-	updateFileSummary: function() {
-		var $summary = this.$el.find('.summary');
-
-		// always make it the last element
-		this.$fileList.append($summary.detach());
-
-		// Check if we should remove the summary to show "Upload something"
-		if (this.isEmpty && $summary.length === 1) {
-			$summary.remove();
-		}
-		// If there's no summary create one (createFileSummary checks if there's data)
-		else if ($summary.length === 0) {
-			FileList.createFileSummary();
-		}
-		// There's a summary and data -> Update the summary
-		else if (!this.isEmpty && $summary.length === 1) {
-			var fileSummary = this._calculateFileSummary();
-			var $dirInfo = $('.summary .dirinfo');
-			var $fileInfo = $('.summary .fileinfo');
-			var $connector = $('.summary .connector');
-
-			// Substitute old content with new translations
-			$dirInfo.html(n('files', '%n folder', '%n folders', fileSummary.totalDirs));
-			$fileInfo.html(n('files', '%n file', '%n files', fileSummary.totalFiles));
-			$('.summary .filesize').html(humanFileSize(fileSummary.totalSize));
-
-			// Show only what's necessary (may be hidden)
-			if (fileSummary.totalDirs === 0) {
-				$dirInfo.addClass('hidden');
-				$connector.addClass('hidden');
-			} else {
-				$dirInfo.removeClass('hidden');
-			}
-			if (fileSummary.totalFiles === 0) {
-				$fileInfo.addClass('hidden');
-				$connector.addClass('hidden');
-			} else {
-				$fileInfo.removeClass('hidden');
-			}
-			if (fileSummary.totalDirs > 0 && fileSummary.totalFiles > 0) {
-				$connector.removeClass('hidden');
-			}
-		}
+		return new FileSummary($tr);
 	},
 	updateEmptyContent: function() {
 		var permissions = $('#permissions').val();
@@ -956,7 +1194,7 @@ window.FileList = {
 		}
 	},
 	filter:function(query) {
-		$('#fileList tr:not(.summary)').each(function(i,e) {
+		$('#fileList tr').each(function(i,e) {
 			if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) {
 				$(e).addClass("searchresult");
 			} else {
@@ -974,12 +1212,52 @@ window.FileList = {
 			$(e).removeClass("searchresult");
 		});
 	},
+	/**
+	 * Update UI based on the current selection
+	 */
+	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'));
+			$('table').removeClass('multiselect');
+			$('.selectedActions').addClass('hidden');
+		}
+		else {
+			$('.selectedActions').removeClass('hidden');
+			$('#headerSize').text(OC.Util.humanFileSize(summary.totalSize));
+			var selection = '';
+			if (summary.totalDirs > 0) {
+				selection += n('files', '%n folder', '%n folders', summary.totalDirs);
+				if (summary.totalFiles > 0) {
+					selection += ' & ';
+				}
+			}
+			if (summary.totalFiles > 0) {
+				selection += n('files', '%n file', '%n files', summary.totalFiles);
+			}
+			$('#headerName span.name').text(selection);
+			$('#modified').text('');
+			$('table').addClass('multiselect');
+		}
+	},
+
 	/**
 	 * Returns whether all files are selected
 	 * @return true if all files are selected, false otherwise
 	 */
 	isAllSelected: function() {
-		return $('#select_all').prop('checked');
+		return this.$el.find('#select_all').prop('checked');
+	},
+
+	/**
+	 * Returns the file info of the selected files
+	 *
+	 * @return array of file names
+	 */
+	getSelectedFiles: function() {
+		return _.values(this._selectedFiles);
 	}
 };
 
@@ -1146,7 +1424,7 @@ $(document).ready(function() {
 				FileList.remove(file.name);
 
 				// create new file context
-				data.context = FileList.add(file, {insert: true});
+				data.context = FileList.add(file, {animate: true});
 			}
 		}
 	});
@@ -1255,12 +1533,12 @@ $(document).ready(function() {
 		}
 	};
 
+	$(window).scroll(function(e) {FileList._onScroll(e);});
+
 	var dir = parseCurrentDirFromUrl();
 	// trigger ajax load, deferred to let sub-apps do their overrides first
 	setTimeout(function() {
 		FileList.changeDirectory(dir, false, true);
 	}, 0);
-
-	FileList.createFileSummary();
 });
 
diff --git a/apps/files/js/files.js b/apps/files/js/files.js
index 5e669a796a9efdb95f95f0f69cebb59822408619..6d167851e64465249f0a3e23a1299e3da22c1cb4 100644
--- a/apps/files/js/files.js
+++ b/apps/files/js/files.js
@@ -8,8 +8,8 @@
  *
  */
 
-/* global OC, t, n, FileList, FileActions */
-/* global getURLParameter, isPublic */
+/* global OC, t, FileList */
+/* global getURLParameter */
 var Files = {
 	// file space size sync
 	_updateStorageStatistics: function() {
@@ -96,10 +96,10 @@ var Files = {
 			throw t('files', 'File name cannot be empty.');
 		}
 		// check for invalid characters
-		var invalid_characters =
+		var invalidCharacters =
 			['\\', '/', '<', '>', ':', '"', '|', '?', '*', '\n'];
-		for (var i = 0; i < invalid_characters.length; i++) {
-			if (trimmedName.indexOf(invalid_characters[i]) !== -1) {
+		for (var i = 0; i < invalidCharacters.length; i++) {
+			if (trimmedName.indexOf(invalidCharacters[i]) !== -1) {
 				throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed.");
 			}
 		}
@@ -116,7 +116,8 @@ var Files = {
 			return;
 		}
 		if (usedSpacePercent > 90) {
-			OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', {usedSpacePercent: usedSpacePercent}));
+			OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)',
+				{usedSpacePercent: usedSpacePercent}));
 		}
 	},
 
@@ -142,6 +143,7 @@ var Files = {
 		}
 	},
 
+	// TODO: move to FileList class
 	setupDragAndDrop: function() {
 		var $fileList = $('#fileList');
 
@@ -209,7 +211,7 @@ $(document).ready(function() {
 	// Trigger cancelling of file upload
 	$('#uploadprogresswrapper .stop').on('click', function() {
 		OC.Upload.cancelUploads();
-		procesSelection();
+		FileList.updateSelectionSummary();
 	});
 
 	// Show trash bin
@@ -217,135 +219,11 @@ $(document).ready(function() {
 		window.location=OC.filePath('files_trashbin', '', 'index.php');
 	});
 
-	var lastChecked;
-
-	// Sets the file link behaviour :
-	$('#fileList').on('click','td.filename a',function(event) {
-		if (event.ctrlKey || event.shiftKey) {
-			event.preventDefault();
-			if (event.shiftKey) {
-				var last = $(lastChecked).parent().parent().prevAll().length;
-				var first = $(this).parent().parent().prevAll().length;
-				var start = Math.min(first, last);
-				var end = Math.max(first, last);
-				var rows = $(this).parent().parent().parent().children('tr');
-				for (var i = start; i < end; i++) {
-					$(rows).each(function(index) {
-						if (index === i) {
-							var checkbox = $(this).children().children('input:checkbox');
-							$(checkbox).attr('checked', 'checked');
-							$(checkbox).parent().parent().addClass('selected');
-						}
-					});
-				}
-			}
-			var checkbox = $(this).parent().children('input:checkbox');
-			lastChecked = checkbox;
-			if ($(checkbox).attr('checked')) {
-				$(checkbox).removeAttr('checked');
-				$(checkbox).parent().parent().removeClass('selected');
-				$('#select_all').removeAttr('checked');
-			} else {
-				$(checkbox).attr('checked', 'checked');
-				$(checkbox).parent().parent().toggleClass('selected');
-				var selectedCount = $('td.filename input:checkbox:checked').length;
-				if (selectedCount === $('td.filename input:checkbox').length) {
-					$('#select_all').attr('checked', 'checked');
-				}
-			}
-			procesSelection();
-		} else {
-			var filename=$(this).parent().parent().attr('data-file');
-			var tr = FileList.findFileEl(filename);
-			var renaming=tr.data('renaming');
-			if (!renaming) {
-				FileActions.currentFile = $(this).parent();
-				var mime=FileActions.getCurrentMimeType();
-				var type=FileActions.getCurrentType();
-				var permissions = FileActions.getCurrentPermissions();
-				var action=FileActions.getDefault(mime,type, permissions);
-				if (action) {
-					event.preventDefault();
-					action(filename);
-				}
-			}
-		}
-
-	});
-
-	// Sets the select_all checkbox behaviour :
-	$('#select_all').click(function() {
-		if ($(this).attr('checked')) {
-			// Check all
-			$('td.filename input:checkbox').attr('checked', true);
-			$('td.filename input:checkbox').parent().parent().addClass('selected');
-		} else {
-			// Uncheck all
-			$('td.filename input:checkbox').attr('checked', false);
-			$('td.filename input:checkbox').parent().parent().removeClass('selected');
-		}
-		procesSelection();
-	});
-
-	$('#fileList').on('change', 'td.filename input:checkbox',function(event) {
-		if (event.shiftKey) {
-			var last = $(lastChecked).parent().parent().prevAll().length;
-			var first = $(this).parent().parent().prevAll().length;
-			var start = Math.min(first, last);
-			var end = Math.max(first, last);
-			var rows = $(this).parent().parent().parent().children('tr');
-			for (var i = start; i < end; i++) {
-				$(rows).each(function(index) {
-					if (index === i) {
-						var checkbox = $(this).children().children('input:checkbox');
-						$(checkbox).attr('checked', 'checked');
-						$(checkbox).parent().parent().addClass('selected');
-					}
-				});
-			}
-		}
-		var selectedCount=$('td.filename input:checkbox:checked').length;
-		$(this).parent().parent().toggleClass('selected');
-		if (!$(this).attr('checked')) {
-			$('#select_all').attr('checked',false);
-		} else {
-			if (selectedCount===$('td.filename input:checkbox').length) {
-				$('#select_all').attr('checked',true);
-			}
-		}
-		procesSelection();
-	});
-
-	$('.download').click('click',function(event) {
-		var files;
-		var dir = FileList.getCurrentDirectory();
-		if (FileList.isAllSelected()) {
-			files = OC.basename(dir);
-			dir = OC.dirname(dir) || '/';
-		}
-		else {
-			files = Files.getSelectedFiles('name');
-		}
-		OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.'));
-		OC.redirect(Files.getDownloadUrl(files, dir));
-		return false;
-	});
-
-	$('.delete-selected').click(function(event) {
-		var files = Files.getSelectedFiles('name');
-		event.preventDefault();
-		if (FileList.isAllSelected()) {
-			files = null;
-		}
-		FileList.do_delete(files);
-		return false;
-	});
-
 	// drag&drop support using jquery.fileupload
 	// TODO use OC.dialogs
 	$(document).bind('drop dragover', function (e) {
 			e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone
-	});
+		});
 
 	//do a background scan if needed
 	scanFiles();
@@ -422,34 +300,22 @@ function scanFiles(force, dir, users) {
 }
 scanFiles.scanning=false;
 
-function boolOperationFinished(data, callback) {
-	result = jQuery.parseJSON(data.responseText);
-	Files.updateMaxUploadFilesize(result);
-	if (result.status === 'success') {
-		callback.call();
-	} else {
-		alert(result.data.message);
-	}
-}
-
+// TODO: move to FileList
 var createDragShadow = function(event) {
 	//select dragged file
 	var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked');
 	if (!isDragSelected) {
 		//select dragged file
-		$(event.target).parents('tr').find('td input:first').prop('checked',true);
+		FileList._selectFileEl($(event.target).parents('tr:first'), true);
 	}
 
-	var selectedFiles = Files.getSelectedFiles();
+	// do not show drag shadow for too many files
+	var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize);
+	selectedFiles.sort(FileList._fileInfoCompare);
 
 	if (!isDragSelected && selectedFiles.length === 1) {
 		//revert the selection
-		$(event.target).parents('tr').find('td input:first').prop('checked',false);
-	}
-
-	//also update class when we dragged more than one file
-	if (selectedFiles.length > 1) {
-		$(event.target).parents('tr').addClass('selected');
+		FileList._selectFileEl($(event.target).parents('tr:first'), false);
 	}
 
 	// build dragshadow
@@ -460,9 +326,12 @@ var createDragShadow = function(event) {
 	var dir=$('#dir').val();
 
 	$(selectedFiles).each(function(i,elem) {
-		var newtr = $('<tr/>').attr('data-dir', dir).attr('data-filename', elem.name).attr('data-origin', elem.origin);
+		var newtr = $('<tr/>')
+			.attr('data-dir', dir)
+			.attr('data-file', elem.name)
+			.attr('data-origin', elem.origin);
 		newtr.append($('<td/>').addClass('filename').text(elem.name));
-		newtr.append($('<td/>').addClass('size').text(humanFileSize(elem.size)));
+		newtr.append($('<td/>').addClass('size').text(OC.Util.humanFileSize(elem.size)));
 		tbody.append(newtr);
 		if (elem.type === 'dir') {
 			newtr.find('td.filename').attr('style','background-image:url('+OC.imagePath('core', 'filetypes/folder.png')+')');
@@ -479,154 +348,57 @@ var createDragShadow = function(event) {
 
 //options for file drag/drop
 //start&stop handlers needs some cleaning up
+// TODO: move to FileList class
 var dragOptions={
 	revert: 'invalid', revertDuration: 300,
 	opacity: 0.7, zIndex: 100, appendTo: 'body', cursorAt: { left: 24, top: 18 },
 	helper: createDragShadow, cursor: 'move',
-		start: function(event, ui){
-			var $selectedFiles = $('td.filename input:checkbox:checked');
-			if($selectedFiles.length > 1){
-				$selectedFiles.parents('tr').fadeTo(250, 0.2);
-			}
-			else{
-				$(this).fadeTo(250, 0.2);
-			}
-		},
-		stop: function(event, ui) {
-			var $selectedFiles = $('td.filename input:checkbox:checked');
-			if($selectedFiles.length > 1){
-				$selectedFiles.parents('tr').fadeTo(250, 1);
-			}
-			else{
-				$(this).fadeTo(250, 1);
-			}
-			$('#fileList tr td.filename').addClass('ui-draggable');
+	start: function(event, ui){
+		var $selectedFiles = $('td.filename input:checkbox:checked');
+		if($selectedFiles.length > 1){
+			$selectedFiles.parents('tr').fadeTo(250, 0.2);
+		}
+		else{
+			$(this).fadeTo(250, 0.2);
+		}
+	},
+	stop: function(event, ui) {
+		var $selectedFiles = $('td.filename input:checkbox:checked');
+		if($selectedFiles.length > 1){
+			$selectedFiles.parents('tr').fadeTo(250, 1);
+		}
+		else{
+			$(this).fadeTo(250, 1);
 		}
+		$('#fileList tr td.filename').addClass('ui-draggable');
+	}
 };
 // sane browsers support using the distance option
 if ( $('html.ie').length === 0) {
 	dragOptions['distance'] = 20;
 }
 
-var folderDropOptions={
+// TODO: move to FileList class
+var folderDropOptions = {
 	hoverClass: "canDrop",
 	drop: function( event, ui ) {
-		//don't allow moving a file into a selected folder
+		// don't allow moving a file into a selected folder
 		if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) {
 			return false;
 		}
 
-		var target = $(this).closest('tr').data('file');
-
-		var files = ui.helper.find('tr');
-		$(files).each(function(i,row) {
-			var dir = $(row).data('dir');
-			var file = $(row).data('filename');
-							//slapdash selector, tracking down our original element that the clone budded off of.
-				var origin = $('tr[data-id=' + $(row).data('origin') + ']');
-				var td = origin.children('td.filename');
-				var oldBackgroundImage = td.css('background-image');
-				td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
-			$.post(OC.filePath('files', 'ajax', 'move.php'), { dir: dir, file: file, target: dir+'/'+target }, function(result) {
-				if (result) {
-					if (result.status === 'success') {
-						//recalculate folder size
-						var oldFile = FileList.findFileEl(target);
-						var newFile = FileList.findFileEl(file);
-						var oldSize = oldFile.data('size');
-						var newSize = oldSize + newFile.data('size');
-						oldFile.data('size', newSize);
-						oldFile.find('td.filesize').text(humanFileSize(newSize));
-
-						FileList.remove(file);
-						procesSelection();
-						$('#notification').hide();
-					} else {
-						$('#notification').hide();
-						$('#notification').text(result.data.message);
-						$('#notification').fadeIn();
-					}
-				} else {
-					OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error'));
-				}
-				td.css('background-image', oldBackgroundImage);
-			});
-		});
-	},
-	tolerance: 'pointer'
-};
+		var targetPath = FileList.getCurrentDirectory() + '/' + $(this).closest('tr').data('file');
 
-function procesSelection() {
-	var selected = Files.getSelectedFiles();
-	var selectedFiles = selected.filter(function(el) {
-		return el.type==='file';
-	});
-	var selectedFolders = selected.filter(function(el) {
-		return el.type==='dir';
-	});
-	if (selectedFiles.length === 0 && selectedFolders.length === 0) {
-		$('#headerName span.name').text(t('files','Name'));
-		$('#headerSize').text(t('files','Size'));
-		$('#modified').text(t('files','Modified'));
-		$('table').removeClass('multiselect');
-		$('.selectedActions').hide();
-		$('#select_all').removeAttr('checked');
-	}
-	else {
-		$('.selectedActions').show();
-		var totalSize = 0;
-		for(var i=0; i<selectedFiles.length; i++) {
-			totalSize+=selectedFiles[i].size;
-		}
-		for(var i=0; i<selectedFolders.length; i++) {
-			totalSize+=selectedFolders[i].size;
-		}
-		$('#headerSize').text(humanFileSize(totalSize));
-		var selection = '';
-		if (selectedFolders.length > 0) {
-			selection += n('files', '%n folder', '%n folders', selectedFolders.length);
-			if (selectedFiles.length > 0) {
-				selection += ' & ';
-			}
+		var files = FileList.getSelectedFiles();
+		if (files.length === 0) {
+			// single one selected without checkbox?
+			files = _.map(ui.helper.find('tr'), FileList.elementToFile);
 		}
-		if (selectedFiles.length>0) {
-			selection += n('files', '%n file', '%n files', selectedFiles.length);
-		}
-		$('#headerName span.name').text(selection);
-		$('#modified').text('');
-		$('table').addClass('multiselect');
-	}
-}
 
-/**
- * @brief get a list of selected files
- * @param {string} property (option) the property of the file requested
- * @return {array}
- *
- * possible values for property: name, mime, size and type
- * if property is set, an array with that property for each file is returnd
- * if it's ommited an array of objects with all properties is returned
- */
-Files.getSelectedFiles = function(property) {
-	var elements=$('td.filename input:checkbox:checked').parent().parent();
-	var files=[];
-	elements.each(function(i,element) {
-		var file={
-			name:$(element).attr('data-file'),
-			mime:$(element).data('mime'),
-			type:$(element).data('type'),
-			size:$(element).data('size'),
-			etag:$(element).data('etag'),
-			origin: $(element).data('id')
-		};
-		if (property) {
-			files.push(file[property]);
-		} else {
-			files.push(file);
-		}
-	});
-	return files;
-}
+		FileList.move(_.pluck(files, 'name'), targetPath);
+	},
+	tolerance: 'pointer'
+};
 
 Files.getMimeIcon = function(mime, ready) {
 	if (Files.getMimeIcon.cache[mime]) {
@@ -665,7 +437,7 @@ Files.generatePreviewUrl = function(urlSpec) {
 	urlSpec.x *= window.devicePixelRatio;
 	urlSpec.forceIcon = 0;
 	return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
-}
+};
 
 Files.lazyLoadPreview = function(path, mime, ready, width, height, etag) {
 	// get mime icon url
diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5130247cc99911f068fe1a952b7f72fd738b29a
--- /dev/null
+++ b/apps/files/js/filesummary.js
@@ -0,0 +1,195 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+/* global OC, n, t */
+
+(function() {
+	/**
+	 * The FileSummary class encapsulates the file summary values and
+	 * the logic to render it in the given container
+	 * @param $tr table row element
+	 * $param summary optional initial summary value
+	 */
+	var FileSummary = function($tr) {
+		this.$el = $tr;
+		this.clear();
+		this.render();
+	};
+
+	FileSummary.prototype = {
+		summary: {
+			totalFiles: 0,
+			totalDirs: 0,
+			totalSize: 0
+		},
+
+		/**
+		 * Adds file
+		 * @param file file to add
+		 * @param update whether to update the display
+		 */
+		add: function(file, update) {
+			if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
+				this.summary.totalDirs++;
+			}
+			else {
+				this.summary.totalFiles++;
+			}
+			this.summary.totalSize += parseInt(file.size, 10) || 0;
+			if (!!update) {
+				this.update();
+			}
+		},
+		/**
+		 * Removes file
+		 * @param file file to remove
+		 * @param update whether to update the display
+		 */
+		remove: function(file, update) {
+			if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
+				this.summary.totalDirs--;
+			}
+			else {
+				this.summary.totalFiles--;
+			}
+			this.summary.totalSize -= parseInt(file.size, 10) || 0;
+			if (!!update) {
+				this.update();
+			}
+		},
+		/**
+		 * Returns the total of files and directories
+		 */
+		getTotal: function() {
+			return this.summary.totalDirs + this.summary.totalFiles;
+		},
+		/**
+		 * Recalculates the summary based on the given files array
+		 * @param files array of files
+		 */
+		calculate: function(files) {
+			var file;
+			var summary = {
+				totalDirs: 0,
+				totalFiles: 0,
+				totalSize: 0
+			};
+
+			for (var i = 0; i < files.length; i++) {
+				file = files[i];
+				if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
+					summary.totalDirs++;
+				}
+				else {
+					summary.totalFiles++;
+				}
+				summary.totalSize += parseInt(file.size, 10) || 0;
+			}
+			this.setSummary(summary);
+		},
+		/**
+		 * Clears the summary
+		 */
+		clear: function() {
+			this.calculate([]);
+		},
+		/**
+		 * Sets the current summary values
+		 * @param summary map
+		 */
+		setSummary: function(summary) {
+			this.summary = summary;
+			this.update();
+		},
+
+		/**
+		 * Renders the file summary element
+		 */
+		update: function() {
+			if (!this.$el) {
+				return;
+			}
+			if (!this.summary.totalFiles && !this.summary.totalDirs) {
+				this.$el.addClass('hidden');
+				return;
+			}
+			// There's a summary and data -> Update the summary
+			this.$el.removeClass('hidden');
+			var $dirInfo = this.$el.find('.dirinfo');
+			var $fileInfo = this.$el.find('.fileinfo');
+			var $connector = this.$el.find('.connector');
+
+			// Substitute old content with new translations
+			$dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs));
+			$fileInfo.html(n('files', '%n file', '%n files', this.summary.totalFiles));
+			this.$el.find('.filesize').html(OC.Util.humanFileSize(this.summary.totalSize));
+
+			// Show only what's necessary (may be hidden)
+			if (this.summary.totalDirs === 0) {
+				$dirInfo.addClass('hidden');
+				$connector.addClass('hidden');
+			} else {
+				$dirInfo.removeClass('hidden');
+			}
+			if (this.summary.totalFiles === 0) {
+				$fileInfo.addClass('hidden');
+				$connector.addClass('hidden');
+			} else {
+				$fileInfo.removeClass('hidden');
+			}
+			if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) {
+				$connector.removeClass('hidden');
+			}
+		},
+		render: function() {
+			if (!this.$el) {
+				return;
+			}
+			// TODO: ideally this should be separate to a template or something
+			var summary = this.summary;
+			var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
+			var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
+
+			var infoVars = {
+				dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">',
+				files: '</span><span class="fileinfo">'+fileInfo+'</span>'
+			};
+
+			// don't show the filesize column, if filesize is NaN (e.g. in trashbin)
+			var fileSize = '';
+			if (!isNaN(summary.totalSize)) {
+				fileSize = '<td class="filesize">' + OC.Util.humanFileSize(summary.totalSize) + '</td>';
+			}
+
+			var info = t('files', '{dirs} and {files}', infoVars);
+
+			var $summary = $('<td><span class="info">'+info+'</span></td>'+fileSize+'<td></td>');
+
+			if (!this.summary.totalFiles && !this.summary.totalDirs) {
+				this.$el.addClass('hidden');
+			}
+
+			this.$el.append($summary);
+		}
+	};
+	window.FileSummary = FileSummary;
+})();
+
diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php
index a8437835d959bf8ca662de8492684a6b26fc82d5..42263c880a7b530ccfa78d12e8a5410dcd81c830 100644
--- a/apps/files/templates/index.php
+++ b/apps/files/templates/index.php
@@ -91,6 +91,8 @@
 	</thead>
 	<tbody id="fileList">
 	</tbody>
+	<tfoot>
+	</tfoot>
 </table>
 <div id="editor"></div><!-- FIXME Do not use this div in your app! It is deprecated and will be removed in the future! -->
 <div id="uploadsize-message" title="<?php p($l->t('Upload too large'))?>">
diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js
index 3c22c84b866d979397eaaac831a402dbd72149be..f5eafba509f6d5ed8925aa5831ddc1012367582a 100644
--- a/apps/files/tests/js/fileactionsSpec.js
+++ b/apps/files/tests/js/fileactionsSpec.js
@@ -30,6 +30,7 @@ describe('FileActions tests', function() {
 		$body.append('<input type="hidden" id="permissions" value="31"></input>');
 		// dummy files table
 		$filesTable = $body.append('<table id="filestable"></table>');
+		FileList.files = [];
 	});
 	afterEach(function() {
 		$('#dir, #permissions, #filestable').remove();
diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js
index ca85a360cf508a0e684b17e123b76fa7da7039de..eab364644cdef10fdcc59bded34c893c90fd81eb 100644
--- a/apps/files/tests/js/filelistSpec.js
+++ b/apps/files/tests/js/filelistSpec.js
@@ -24,6 +24,33 @@ describe('FileList tests', function() {
 	var testFiles, alertStub, notificationStub,
 		pushStateStub;
 
+	/**
+	 * Generate test file data
+	 */
+	function generateFiles(startIndex, endIndex) {
+		var files = [];
+		var name;
+		for (var i = startIndex; i <= endIndex; i++) {
+			name = 'File with index ';
+			if (i < 10) {
+				// do not rely on localeCompare here
+				// and make the sorting predictable
+				// cross-browser
+				name += '0';
+			}
+			name += i + '.txt';
+			files.push({
+				id: i,
+				type: 'file',
+				name: name,
+				mimetype: 'text/plain',
+				size: i * 2,
+				etag: 'abc'
+			});
+		}
+		return files;
+	}
+
 	beforeEach(function() {
 		// init horrible parameters
 		var $body = $('body');
@@ -48,9 +75,17 @@ describe('FileList tests', function() {
 			'   <div class="notCreatable"></div>' +
 			'</div>' +
 			// dummy table
+			// TODO: at some point this will be rendered by the FileList class itself!
 			'<table id="filestable">' +
-			'<thead><tr><th class="hidden">Name</th></tr></thead>' +
+			'<thead><tr><th id="headerName" class="hidden">' +
+			'<input type="checkbox" id="select_all">' +
+			'<span class="name">Name</span>' +
+			'<span class="selectedActions hidden">' +
+			'<a href class="download">Download</a>' +
+			'<a href class="delete-selected">Delete</a></span>' +
+			'</th></tr></thead>' +
 		   	'<tbody id="fileList"></tbody>' +
+			'<tfoot></tfoot>' +
 			'</table>' +
 			'<div id="emptycontent">Empty content message</div>'
 		);
@@ -60,25 +95,29 @@ describe('FileList tests', function() {
 			type: 'file',
 			name: 'One.txt',
 			mimetype: 'text/plain',
-			size: 12
+			size: 12,
+			etag: 'abc'
 		}, {
 			id: 2,
 			type: 'file',
 			name: 'Two.jpg',
 			mimetype: 'image/jpeg',
-			size: 12049
+			size: 12049,
+			etag: 'def',
 		}, {
 			id: 3,
 			type: 'file',
 			name: 'Three.pdf',
 			mimetype: 'application/pdf',
-			size: 58009
+			size: 58009,
+			etag: '123',
 		}, {
 			id: 4,
 			type: 'dir',
 			name: 'somedir',
 			mimetype: 'httpd/unix-directory',
-			size: 250
+			size: 250,
+			etag: '456'
 		}];
 
 		FileList.initialize();
@@ -220,25 +259,65 @@ describe('FileList tests', function() {
 			var $tr = FileList.add(fileData);
 			expect($tr.find('.filesize').text()).toEqual('0 B');
 		});
-		it('adds new file to the end of the list before the summary', function() {
+		it('adds new file to the end of the list', function() {
+			var $tr;
 			var fileData = {
 				type: 'file',
-				name: 'P comes after O.txt'
+				name: 'ZZZ.txt'
 			};
 			FileList.setFiles(testFiles);
 			$tr = FileList.add(fileData);
 			expect($tr.index()).toEqual(4);
-			expect($tr.next().hasClass('summary')).toEqual(true);
 		});
-		it('adds new file at correct position in insert mode', function() {
+		it('inserts files in a sorted manner when insert option is enabled', function() {
+			var $tr;
+			for (var i = 0; i < testFiles.length; i++) {
+				FileList.add(testFiles[i]);
+			}
+			expect(FileList.files[0].name).toEqual('somedir');
+			expect(FileList.files[1].name).toEqual('One.txt');
+			expect(FileList.files[2].name).toEqual('Three.pdf');
+			expect(FileList.files[3].name).toEqual('Two.jpg');
+		});
+		it('inserts new file at correct position', function() {
+			var $tr;
 			var fileData = {
 				type: 'file',
 				name: 'P comes after O.txt'
 			};
-			FileList.setFiles(testFiles);
-			$tr = FileList.add(fileData, {insert: true});
+			for (var i = 0; i < testFiles.length; i++) {
+				FileList.add(testFiles[i]);
+			}
+			$tr = FileList.add(fileData);
 			// after "One.txt"
+			expect($tr.index()).toEqual(2);
+			expect(FileList.files[2]).toEqual(fileData);
+		});
+		it('inserts new folder at correct position in insert mode', function() {
+			var $tr;
+			var fileData = {
+				type: 'dir',
+				name: 'somedir2 comes after somedir'
+			};
+			for (var i = 0; i < testFiles.length; i++) {
+				FileList.add(testFiles[i]);
+			}
+			$tr = FileList.add(fileData);
 			expect($tr.index()).toEqual(1);
+			expect(FileList.files[1]).toEqual(fileData);
+		});
+		it('inserts new file at the end correctly', function() {
+			var $tr;
+			var fileData = {
+				type: 'file',
+				name: 'zzz.txt'
+			};
+			for (var i = 0; i < testFiles.length; i++) {
+				FileList.add(testFiles[i]);
+			}
+			$tr = FileList.add(fileData);
+			expect($tr.index()).toEqual(4);
+			expect(FileList.files[4]).toEqual(fileData);
 		});
 		it('removes empty content message and shows summary when adding first file', function() {
 			var fileData = {
@@ -249,8 +328,8 @@ describe('FileList tests', function() {
 			FileList.setFiles([]);
 			expect(FileList.isEmpty).toEqual(true);
 			FileList.add(fileData);
-			$summary = $('#fileList .summary');
-			expect($summary.length).toEqual(1);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(false);
 			// yes, ugly...
 			expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
 			expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true);
@@ -268,11 +347,12 @@ describe('FileList tests', function() {
 			$removedEl = FileList.remove('One.txt');
 			expect($removedEl).toBeDefined();
 			expect($removedEl.attr('data-file')).toEqual('One.txt');
-			expect($('#fileList tr:not(.summary)').length).toEqual(3);
+			expect($('#fileList tr').length).toEqual(3);
+			expect(FileList.files.length).toEqual(3);
 			expect(FileList.findFileEl('One.txt').length).toEqual(0);
 
-			$summary = $('#fileList .summary');
-			expect($summary.length).toEqual(1);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(false);
 			expect($summary.find('.info').text()).toEqual('1 folder and 2 files');
 			expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false);
 			expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false);
@@ -282,11 +362,12 @@ describe('FileList tests', function() {
 		it('Shows empty content when removing last file', function() {
 			FileList.setFiles([testFiles[0]]);
 			FileList.remove('One.txt');
-			expect($('#fileList tr:not(.summary)').length).toEqual(0);
+			expect($('#fileList tr').length).toEqual(0);
+			expect(FileList.files.length).toEqual(0);
 			expect(FileList.findFileEl('One.txt').length).toEqual(0);
 
-			$summary = $('#fileList .summary');
-			expect($summary.length).toEqual(0);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(true);
 			expect($('#filestable thead th').hasClass('hidden')).toEqual(true);
 			expect($('#emptycontent').hasClass('hidden')).toEqual(false);
 			expect(FileList.isEmpty).toEqual(true);
@@ -318,10 +399,10 @@ describe('FileList tests', function() {
 			expect(FileList.findFileEl('One.txt').length).toEqual(0);
 			expect(FileList.findFileEl('Two.jpg').length).toEqual(0);
 			expect(FileList.findFileEl('Three.pdf').length).toEqual(1);
-			expect(FileList.$fileList.find('tr:not(.summary)').length).toEqual(2);
+			expect(FileList.$fileList.find('tr').length).toEqual(2);
 
-			$summary = $('#fileList .summary');
-			expect($summary.length).toEqual(1);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(false);
 			expect($summary.find('.info').text()).toEqual('1 folder and 1 file');
 			expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false);
 			expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false);
@@ -342,11 +423,12 @@ describe('FileList tests', function() {
 				JSON.stringify({status: 'success'})
 			);
 
-			expect(FileList.$fileList.find('tr:not(.summary)').length).toEqual(0);
+			expect(FileList.$fileList.find('tr').length).toEqual(0);
 
-			$summary = $('#fileList .summary');
-			expect($summary.length).toEqual(0);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(true);
 			expect(FileList.isEmpty).toEqual(true);
+			expect(FileList.files.length).toEqual(0);
 			expect($('#filestable thead th').hasClass('hidden')).toEqual(true);
 			expect($('#emptycontent').hasClass('hidden')).toEqual(false);
 		});
@@ -363,7 +445,7 @@ describe('FileList tests', function() {
 			// files are still in the list
 			expect(FileList.findFileEl('One.txt').length).toEqual(1);
 			expect(FileList.findFileEl('Two.jpg').length).toEqual(1);
-			expect(FileList.$fileList.find('tr:not(.summary)').length).toEqual(4);
+			expect(FileList.$fileList.find('tr').length).toEqual(4);
 
 			expect(notificationStub.calledOnce).toEqual(true);
 		});
@@ -372,37 +454,41 @@ describe('FileList tests', function() {
 		function doRename() {
 			var $input, request;
 
-			FileList.setFiles(testFiles);
+			for (var i = 0; i < testFiles.length; i++) {
+				FileList.add(testFiles[i]);
+			}
 
 			// trigger rename prompt
 			FileList.rename('One.txt');
 			$input = FileList.$fileList.find('input.filename');
-			$input.val('One_renamed.txt').blur();
+			$input.val('Tu_after_three.txt').blur();
 
 			expect(fakeServer.requests.length).toEqual(1);
-			var request = fakeServer.requests[0];
+			request = fakeServer.requests[0];
 			expect(request.url.substr(0, request.url.indexOf('?'))).toEqual(OC.webroot + '/index.php/apps/files/ajax/rename.php');
-			expect(OC.parseQueryString(request.url)).toEqual({'dir': '/subdir', newname: 'One_renamed.txt', file: 'One.txt'});
+			expect(OC.parseQueryString(request.url)).toEqual({'dir': '/subdir', newname: 'Tu_after_three.txt', file: 'One.txt'});
 
 			// element is renamed before the request finishes
 			expect(FileList.findFileEl('One.txt').length).toEqual(0);
-			expect(FileList.findFileEl('One_renamed.txt').length).toEqual(1);
+			expect(FileList.findFileEl('Tu_after_three.txt').length).toEqual(1);
 			// input is gone
 			expect(FileList.$fileList.find('input.filename').length).toEqual(0);
 		}
-		it('Keeps renamed file entry if rename ajax call suceeded', function() {
+		it('Inserts renamed file entry at correct position if rename ajax call suceeded', function() {
 			doRename();
 
 			fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
 				status: 'success',
 				data: {
-					name: 'One_renamed.txt'
+					name: 'Tu_after_three.txt',
+					type: 'file'
 				}
 			}));
 
 			// element stays renamed
 			expect(FileList.findFileEl('One.txt').length).toEqual(0);
-			expect(FileList.findFileEl('One_renamed.txt').length).toEqual(1);
+			expect(FileList.findFileEl('Tu_after_three.txt').length).toEqual(1);
+			expect(FileList.findFileEl('Tu_after_three.txt').index()).toEqual(2); // after Two.txt
 
 			expect(alertStub.notCalled).toEqual(true);
 		});
@@ -418,7 +504,8 @@ describe('FileList tests', function() {
 
 			// element was reverted
 			expect(FileList.findFileEl('One.txt').length).toEqual(1);
-			expect(FileList.findFileEl('One_renamed.txt').length).toEqual(0);
+			expect(FileList.findFileEl('One.txt').index()).toEqual(1); // after somedir
+			expect(FileList.findFileEl('Tu_after_three.txt').length).toEqual(0);
 
 			expect(alertStub.calledOnce).toEqual(true);
 		});
@@ -429,12 +516,12 @@ describe('FileList tests', function() {
 			fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
 				status: 'success',
 				data: {
-					name: 'One_renamed.txt'
+					name: 'Tu_after_three.txt'
 				}
 			}));
 
-			$tr = FileList.findFileEl('One_renamed.txt');
-			expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=One_renamed.txt');
+			$tr = FileList.findFileEl('Tu_after_three.txt');
+			expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=Tu_after_three.txt');
 		});
 		// FIXME: fix this in the source code!
 		xit('Correctly updates file link after rename when path has same name', function() {
@@ -446,27 +533,122 @@ describe('FileList tests', function() {
 			fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
 				status: 'success',
 				data: {
-					name: 'One_renamed.txt'
+					name: 'Tu_after_three.txt'
 				}
 			}));
 
-			$tr = FileList.findFileEl('One_renamed.txt');
+			$tr = FileList.findFileEl('Tu_after_three.txt');
 			expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=One.txt');
 		});
 	});
+	describe('Moving files', function() {
+		beforeEach(function() {
+			FileList.setFiles(testFiles);
+		});
+		it('Moves single file to target folder', function() {
+			var request;
+			FileList.move('One.txt', '/somedir');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			request = fakeServer.requests[0];
+			expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php');
+			expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'});
+
+			fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
+				status: 'success',
+				data: {
+					name: 'One.txt',
+					type: 'file'
+				}
+			}));
+
+			expect(FileList.findFileEl('One.txt').length).toEqual(0);
+
+			// folder size has increased
+			expect(FileList.findFileEl('somedir').data('size')).toEqual(262);
+			expect(FileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B');
+
+			expect(notificationStub.notCalled).toEqual(true);
+		});
+		it('Moves list of files to target folder', function() {
+			var request;
+			FileList.move(['One.txt', 'Two.jpg'], '/somedir');
+
+			expect(fakeServer.requests.length).toEqual(2);
+			request = fakeServer.requests[0];
+			expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php');
+			expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'});
+
+			request = fakeServer.requests[1];
+			expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php');
+			expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'Two.jpg', target: '/somedir'});
+
+			fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
+				status: 'success',
+				data: {
+					name: 'One.txt',
+					type: 'file'
+				}
+			}));
+
+			expect(FileList.findFileEl('One.txt').length).toEqual(0);
+
+			// folder size has increased
+			expect(FileList.findFileEl('somedir').data('size')).toEqual(262);
+			expect(FileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B');
+
+			fakeServer.requests[1].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
+				status: 'success',
+				data: {
+					name: 'Two.jpg',
+					type: 'file'
+				}
+			}));
+
+			expect(FileList.findFileEl('Two.jpg').length).toEqual(0);
+
+			// folder size has increased
+			expect(FileList.findFileEl('somedir').data('size')).toEqual(12311);
+			expect(FileList.findFileEl('somedir').find('.filesize').text()).toEqual('12 kB');
+
+			expect(notificationStub.notCalled).toEqual(true);
+		});
+		it('Shows notification if a file could not be moved', function() {
+			var request;
+			FileList.move('One.txt', '/somedir');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			request = fakeServer.requests[0];
+			expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php');
+			expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'});
+
+			fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({
+				status: 'error',
+				data: {
+					message: 'Error while moving file',
+				}
+			}));
+
+			expect(FileList.findFileEl('One.txt').length).toEqual(1);
+
+			expect(notificationStub.calledOnce).toEqual(true);
+			expect(notificationStub.getCall(0).args[0]).toEqual('Error while moving file');
+		});
+	});
 	describe('List rendering', function() {
 		it('renders a list of files using add()', function() {
-			var addSpy = sinon.spy(FileList, 'add');
+			expect(FileList.files.length).toEqual(0);
+			expect(FileList.files).toEqual([]);
 			FileList.setFiles(testFiles);
-			expect(addSpy.callCount).toEqual(4);
-			expect($('#fileList tr:not(.summary)').length).toEqual(4);
-			addSpy.restore();
+			expect($('#fileList tr').length).toEqual(4);
+			expect(FileList.files.length).toEqual(4);
+			expect(FileList.files).toEqual(testFiles);
 		});
 		it('updates summary using the file sizes', function() {
 			var $summary;
 			FileList.setFiles(testFiles);
-			$summary = $('#fileList .summary');
-			expect($summary.length).toEqual(1);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(false);
 			expect($summary.find('.info').text()).toEqual('1 folder and 3 files');
 			expect($summary.find('.filesize').text()).toEqual('69 kB');
 		});
@@ -474,20 +656,20 @@ describe('FileList tests', function() {
 			FileList.setFiles(testFiles);
 			expect($('#filestable thead th').hasClass('hidden')).toEqual(false);
 			expect($('#emptycontent').hasClass('hidden')).toEqual(true);
-			expect(FileList.$fileList.find('.summary').length).toEqual(1);
+			expect(FileList.$el.find('.summary').hasClass('hidden')).toEqual(false);
 		});
 		it('hides headers, summary and show empty content message after setting empty file list', function(){
 			FileList.setFiles([]);
 			expect($('#filestable thead th').hasClass('hidden')).toEqual(true);
 			expect($('#emptycontent').hasClass('hidden')).toEqual(false);
-			expect(FileList.$fileList.find('.summary').length).toEqual(0);
+			expect(FileList.$el.find('.summary').hasClass('hidden')).toEqual(true);
 		});
 		it('hides headers, empty content message, and summary when list is empty and user has no creation permission', function(){
 			$('#permissions').val(0);
 			FileList.setFiles([]);
 			expect($('#filestable thead th').hasClass('hidden')).toEqual(true);
 			expect($('#emptycontent').hasClass('hidden')).toEqual(true);
-			expect(FileList.$fileList.find('.summary').length).toEqual(0);
+			expect(FileList.$el.find('.summary').hasClass('hidden')).toEqual(true);
 		});
 		it('calling findFileEl() can find existing file element', function() {
 			FileList.setFiles(testFiles);
@@ -519,6 +701,110 @@ describe('FileList tests', function() {
 			FileList.setFiles(testFiles);
 			expect(handler.calledOnce).toEqual(true);
 		});
+		it('does not update summary when removing non-existing files', function() {
+			// single file
+			FileList.setFiles([testFiles[0]]);
+			$summary = $('#filestable .summary');
+			expect($summary.hasClass('hidden')).toEqual(false);
+			expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+			FileList.remove('unexist.txt');
+			expect($summary.hasClass('hidden')).toEqual(false);
+			expect($summary.find('.info').text()).toEqual('0 folders and 1 file');
+		});
+	});
+	describe('Rendering next page on scroll', function() {
+		beforeEach(function() {
+			FileList.setFiles(generateFiles(0, 64));
+		});
+		it('renders only the first page', function() {
+			expect(FileList.files.length).toEqual(65);
+			expect($('#fileList tr').length).toEqual(20);
+		});
+		it('renders the second page when scrolling down (trigger nextPage)', function() {
+			// TODO: can't simulate scrolling here, so calling nextPage directly
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(40);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(60);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(65);
+			FileList._nextPage(true);
+			// stays at 65
+			expect($('#fileList tr').length).toEqual(65);
+		});
+		it('inserts into the DOM if insertion point is in the visible page ', function() {
+			FileList.add({
+				id: 2000,
+				type: 'file',
+				name: 'File with index 15b.txt'
+			});
+			expect($('#fileList tr').length).toEqual(21);
+			expect(FileList.findFileEl('File with index 15b.txt').index()).toEqual(16);
+		});
+		it('does not inserts into the DOM if insertion point is not the visible page ', function() {
+			FileList.add({
+				id: 2000,
+				type: 'file',
+				name: 'File with index 28b.txt'
+			});
+			expect($('#fileList tr').length).toEqual(20);
+			expect(FileList.findFileEl('File with index 28b.txt').length).toEqual(0);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(40);
+			expect(FileList.findFileEl('File with index 28b.txt').index()).toEqual(29);
+		});
+		it('appends into the DOM when inserting a file after the last visible element', function() {
+			FileList.add({
+				id: 2000,
+				type: 'file',
+				name: 'File with index 19b.txt'
+			});
+			expect($('#fileList tr').length).toEqual(21);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(41);
+		});
+		it('appends into the DOM when inserting a file on the last page when visible', function() {
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(40);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(60);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(65);
+			FileList._nextPage(true);
+			FileList.add({
+				id: 2000,
+				type: 'file',
+				name: 'File with index 88.txt'
+			});
+			expect($('#fileList tr').length).toEqual(66);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(66);
+		});
+		it('shows additional page when appending a page of files and scrolling down', function() {
+			var newFiles = generateFiles(66, 81);
+			for (var i = 0; i < newFiles.length; i++) {
+				FileList.add(newFiles[i]);
+			}
+			expect($('#fileList tr').length).toEqual(20);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(40);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(60);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(80);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(81);
+			FileList._nextPage(true);
+			expect($('#fileList tr').length).toEqual(81);
+		});
+		it('automatically renders next page when there are not enough elements visible', function() {
+			// delete the 15 first elements
+			for (var i = 0; i < 15; i++) {
+				FileList.remove(FileList.files[0].name);
+			}
+			// still makes sure that there are 20 elements visible, if any
+			expect($('#fileList tr').length).toEqual(25);
+		});
 	});
 	describe('file previews', function() {
 		var previewLoadStub;
@@ -642,7 +928,7 @@ describe('FileList tests', function() {
 			var query = url.substr(url.indexOf('?') + 1);
 			expect(OC.parseQueryString(query)).toEqual({'dir': '/subdir'});
 			fakeServer.respond();
-			expect($('#fileList tr:not(.summary)').length).toEqual(4);
+			expect($('#fileList tr').length).toEqual(4);
 			expect(FileList.findFileEl('One.txt').length).toEqual(1);
 		});
 		it('switches dir and fetches file list when calling changeDirectory()', function() {
@@ -740,14 +1026,12 @@ describe('FileList tests', function() {
 				}
 			};
 			// returns a list of tr that were dragged
-			// FIXME: why are their attributes different than the
-			// regular file trs ?
 			ui.helper.find.returns([
-				$('<tr data-filename="One.txt" data-dir="' + testDir + '"></tr>'),
-				$('<tr data-filename="Two.jpg" data-dir="' + testDir + '"></tr>')
+				$('<tr data-file="One.txt" data-dir="' + testDir + '"></tr>'),
+				$('<tr data-file="Two.jpg" data-dir="' + testDir + '"></tr>')
 			]);
 			// simulate drop event
-			FileList._onDropOnBreadCrumb.call($crumb, new $.Event('drop'), ui);
+			FileList._onDropOnBreadCrumb(new $.Event('drop', {target: $crumb}), ui);
 
 			// will trigger two calls to move.php (first one was previous list.php)
 			expect(fakeServer.requests.length).toEqual(3);
@@ -784,14 +1068,12 @@ describe('FileList tests', function() {
 				}
 			};
 			// returns a list of tr that were dragged
-			// FIXME: why are their attributes different than the
-			// regular file trs ?
 			ui.helper.find.returns([
-				$('<tr data-filename="One.txt" data-dir="' + testDir + '"></tr>'),
-				$('<tr data-filename="Two.jpg" data-dir="' + testDir + '"></tr>')
+				$('<tr data-file="One.txt" data-dir="' + testDir + '"></tr>'),
+				$('<tr data-file="Two.jpg" data-dir="' + testDir + '"></tr>')
 			]);
 			// simulate drop event
-			FileList._onDropOnBreadCrumb.call($crumb, new $.Event('drop'), ui);
+			FileList._onDropOnBreadCrumb(new $.Event('drop', {target: $crumb}), ui);
 
 			// no extra server request
 			expect(fakeServer.requests.length).toEqual(1);
@@ -811,4 +1093,329 @@ describe('FileList tests', function() {
 			expect(Files.getAjaxUrl('test', {a:1, b:'x y'})).toEqual(OC.webroot + '/index.php/apps/files/ajax/test.php?a=1&b=x%20y');
 		});
 	});
+	describe('File selection', function() {
+		beforeEach(function() {
+			FileList.setFiles(testFiles);
+		});
+		it('Selects a file when clicking its checkbox', function() {
+			var $tr = FileList.findFileEl('One.txt');
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(false);
+			$tr.find('td.filename input:checkbox').click();
+
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(true);
+		});
+		it('Selects/deselect a file when clicking on the name while holding Ctrl', function() {
+			var $tr = FileList.findFileEl('One.txt');
+			var $tr2 = FileList.findFileEl('Three.pdf');
+			var e;
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(false);
+			expect($tr2.find('input:checkbox').prop('checked')).toEqual(false);
+			e = new $.Event('click');
+			e.ctrlKey = true;
+			$tr.find('td.filename .name').trigger(e);
+
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(true);
+			expect($tr2.find('input:checkbox').prop('checked')).toEqual(false);
+
+			// click on second entry, does not clear the selection
+			e = new $.Event('click');
+			e.ctrlKey = true;
+			$tr2.find('td.filename .name').trigger(e);
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(true);
+			expect($tr2.find('input:checkbox').prop('checked')).toEqual(true);
+
+			expect(_.pluck(FileList.getSelectedFiles(), 'name')).toEqual(['One.txt', 'Three.pdf']);
+
+			// deselect now
+			e = new $.Event('click');
+			e.ctrlKey = true;
+			$tr2.find('td.filename .name').trigger(e);
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(true);
+			expect($tr2.find('input:checkbox').prop('checked')).toEqual(false);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name')).toEqual(['One.txt']);
+		});
+		it('Selects a range when clicking on one file then Shift clicking on another one', function() {
+			var $tr = FileList.findFileEl('One.txt');
+			var $tr2 = FileList.findFileEl('Three.pdf');
+			var e;
+			$tr.find('td.filename input:checkbox').click();
+			e = new $.Event('click');
+			e.shiftKey = true;
+			$tr2.find('td.filename .name').trigger(e);
+
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(true);
+			expect($tr2.find('input:checkbox').prop('checked')).toEqual(true);
+			expect(FileList.findFileEl('Two.jpg').find('input:checkbox').prop('checked')).toEqual(true);
+			var selection = _.pluck(FileList.getSelectedFiles(), 'name');
+			expect(selection.length).toEqual(3);
+			expect(selection).toContain('One.txt');
+			expect(selection).toContain('Two.jpg');
+			expect(selection).toContain('Three.pdf');
+		});
+		it('Selects a range when clicking on one file then Shift clicking on another one that is above the first one', function() {
+			var $tr = FileList.findFileEl('One.txt');
+			var $tr2 = FileList.findFileEl('Three.pdf');
+			var e;
+			$tr2.find('td.filename input:checkbox').click();
+			e = new $.Event('click');
+			e.shiftKey = true;
+			$tr.find('td.filename .name').trigger(e);
+
+			expect($tr.find('input:checkbox').prop('checked')).toEqual(true);
+			expect($tr2.find('input:checkbox').prop('checked')).toEqual(true);
+			expect(FileList.findFileEl('Two.jpg').find('input:checkbox').prop('checked')).toEqual(true);
+			var selection = _.pluck(FileList.getSelectedFiles(), 'name');
+			expect(selection.length).toEqual(3);
+			expect(selection).toContain('One.txt');
+			expect(selection).toContain('Two.jpg');
+			expect(selection).toContain('Three.pdf');
+		});
+		it('Selecting all files will automatically check "select all" checkbox', function() {
+			expect($('#select_all').prop('checked')).toEqual(false);
+			$('#fileList tr td.filename input:checkbox').click();
+			expect($('#select_all').prop('checked')).toEqual(true);
+		});
+		it('Selecting all files on the first visible page will not automatically check "select all" checkbox', function() {
+			FileList.setFiles(generateFiles(0, 41));
+			expect($('#select_all').prop('checked')).toEqual(false);
+			$('#fileList tr td.filename input:checkbox').click();
+			expect($('#select_all').prop('checked')).toEqual(false);
+		});
+		it('Clicking "select all" will select/deselect all files', function() {
+			FileList.setFiles(generateFiles(0, 41));
+			$('#select_all').click();
+			expect($('#select_all').prop('checked')).toEqual(true);
+			$('#fileList tr input:checkbox').each(function() {
+				expect($(this).prop('checked')).toEqual(true);
+			});
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(42);
+
+			$('#select_all').click();
+			expect($('#select_all').prop('checked')).toEqual(false);
+
+			$('#fileList tr input:checkbox').each(function() {
+				expect($(this).prop('checked')).toEqual(false);
+			});
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(0);
+		});
+		it('Clicking "select all" then deselecting a file will uncheck "select all"', function() {
+			$('#select_all').click();
+			expect($('#select_all').prop('checked')).toEqual(true);
+
+			var $tr = FileList.findFileEl('One.txt');
+			$tr.find('input:checkbox').click();
+
+			expect($('#select_all').prop('checked')).toEqual(false);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3);
+		});
+		it('Updates the selection summary when doing a few manipulations with "Select all"', function() {
+			$('#select_all').click();
+			expect($('#select_all').prop('checked')).toEqual(true);
+
+			var $tr = FileList.findFileEl('One.txt');
+			// unselect one
+			$tr.find('input:checkbox').click();
+
+			expect($('#select_all').prop('checked')).toEqual(false);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3);
+
+			// select all
+			$('#select_all').click();
+			expect($('#select_all').prop('checked')).toEqual(true);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(4);
+
+			// unselect one
+			$tr.find('input:checkbox').click();
+			expect($('#select_all').prop('checked')).toEqual(false);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3);
+
+			// re-select it
+			$tr.find('input:checkbox').click();
+			expect($('#select_all').prop('checked')).toEqual(true);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(4);
+		});
+		it('Auto-selects files on next page when "select all" is checked', function() {
+			FileList.setFiles(generateFiles(0, 41));
+			$('#select_all').click();
+
+			expect(FileList.$fileList.find('tr input:checkbox:checked').length).toEqual(20);
+			FileList._nextPage(true);
+			expect(FileList.$fileList.find('tr input:checkbox:checked').length).toEqual(40);
+			FileList._nextPage(true);
+			expect(FileList.$fileList.find('tr input:checkbox:checked').length).toEqual(42);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(42);
+		});
+		it('Selecting files updates selection summary', function() {
+			var $summary = $('#headerName span.name');
+			expect($summary.text()).toEqual('Name');
+			FileList.findFileEl('One.txt').find('input:checkbox').click();
+			FileList.findFileEl('Three.pdf').find('input:checkbox').click();
+			FileList.findFileEl('somedir').find('input:checkbox').click();
+			expect($summary.text()).toEqual('1 folder & 2 files');
+		});
+		it('Unselecting files hides selection summary', function() {
+			var $summary = $('#headerName span.name');
+			FileList.findFileEl('One.txt').find('input:checkbox').click().click();
+			expect($summary.text()).toEqual('Name');
+		});
+		it('Select/deselect files shows/hides file actions', function() {
+			var $actions = $('#headerName .selectedActions');
+			var $checkbox = FileList.findFileEl('One.txt').find('input:checkbox');
+			expect($actions.hasClass('hidden')).toEqual(true);
+			$checkbox.click();
+			expect($actions.hasClass('hidden')).toEqual(false);
+			$checkbox.click();
+			expect($actions.hasClass('hidden')).toEqual(true);
+		});
+		it('Selection is cleared when switching dirs', function() {
+			$('#select_all').click();
+			var data = {
+				status: 'success',
+				data: {
+					files: testFiles,
+					permissions: 31
+				}
+			};
+			fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php/, [
+					200, {
+						"Content-Type": "application/json"
+					},
+					JSON.stringify(data)
+			]);
+			FileList.changeDirectory('/');
+			fakeServer.respond();
+			expect($('#select_all').prop('checked')).toEqual(false);
+			expect(_.pluck(FileList.getSelectedFiles(), 'name')).toEqual([]);
+		});
+		it('getSelectedFiles returns the selected files even when they are on the next page', function() {
+			var selectedFiles;
+			FileList.setFiles(generateFiles(0, 41));
+			$('#select_all').click();
+			// unselect one to not have the "allFiles" case
+			FileList.$fileList.find('tr input:checkbox:first').click();
+
+			// only 20 files visible, must still return all the selected ones
+			selectedFiles = _.pluck(FileList.getSelectedFiles(), 'name');
+
+			expect(selectedFiles.length).toEqual(41);
+		});
+		describe('Actions', function() {
+			beforeEach(function() {
+				FileList.findFileEl('One.txt').find('input:checkbox').click();
+				FileList.findFileEl('Three.pdf').find('input:checkbox').click();
+				FileList.findFileEl('somedir').find('input:checkbox').click();
+			});
+			it('getSelectedFiles returns the selected file data', function() {
+				var files = FileList.getSelectedFiles();
+				expect(files.length).toEqual(3);
+				expect(files[0]).toEqual({
+					id: 1,
+					name: 'One.txt',
+					mimetype: 'text/plain',
+					type: 'file',
+					size: 12,
+					etag: 'abc'
+				});
+				expect(files[1]).toEqual({
+					id: 3,
+					type: 'file',
+					name: 'Three.pdf',
+					mimetype: 'application/pdf',
+					size: 58009,
+					etag: '123'
+				});
+				expect(files[2]).toEqual({
+					id: 4,
+					type: 'dir',
+					name: 'somedir',
+					mimetype: 'httpd/unix-directory',
+					size: 250,
+					etag: '456'
+				});
+			});
+			it('Removing a file removes it from the selection', function() {
+				FileList.remove('Three.pdf');
+				var files = FileList.getSelectedFiles();
+				expect(files.length).toEqual(2);
+				expect(files[0]).toEqual({
+					id: 1,
+					name: 'One.txt',
+					mimetype: 'text/plain',
+					type: 'file',
+					size: 12,
+					etag: 'abc'
+				});
+				expect(files[1]).toEqual({
+					id: 4,
+					type: 'dir',
+					name: 'somedir',
+					mimetype: 'httpd/unix-directory',
+					size: 250,
+					etag: '456'
+				});
+			});
+			describe('Download', function() {
+				it('Opens download URL when clicking "Download"', function() {
+					var redirectStub = sinon.stub(OC, 'redirect');
+					$('.selectedActions .download').click();
+					expect(redirectStub.calledOnce).toEqual(true);
+					expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22One.txt%22%2C%22Three.pdf%22%2C%22somedir%22%5D');
+					redirectStub.restore();
+				});
+				it('Downloads root folder when all selected in root folder', function() {
+					$('#dir').val('/');
+					$('#select_all').click();
+					var redirectStub = sinon.stub(OC, 'redirect');
+					$('.selectedActions .download').click();
+					expect(redirectStub.calledOnce).toEqual(true);
+					expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=');
+					redirectStub.restore();
+				});
+				it('Downloads parent folder when all selected in subfolder', function() {
+					$('#select_all').click();
+					var redirectStub = sinon.stub(OC, 'redirect');
+					$('.selectedActions .download').click();
+					expect(redirectStub.calledOnce).toEqual(true);
+					expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=subdir');
+					redirectStub.restore();
+				});
+			});
+			describe('Delete', function() {
+				it('Deletes selected files when "Delete" clicked', function() {
+					var request;
+					$('.selectedActions .delete-selected').click();
+					expect(fakeServer.requests.length).toEqual(1);
+					request = fakeServer.requests[0];
+					expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php');
+					expect(OC.parseQueryString(request.requestBody))
+						.toEqual({'dir': '/subdir', files: '["One.txt","Three.pdf","somedir"]'});
+					fakeServer.requests[0].respond(
+						200,
+						{ 'Content-Type': 'application/json' },
+						JSON.stringify({status: 'success'})
+					);
+					expect(FileList.findFileEl('One.txt').length).toEqual(0);
+					expect(FileList.findFileEl('Three.pdf').length).toEqual(0);
+					expect(FileList.findFileEl('somedir').length).toEqual(0);
+					expect(FileList.findFileEl('Two.jpg').length).toEqual(1);
+				});
+				it('Deletes all files when all selected when "Delete" clicked', function() {
+					var request;
+					$('#select_all').click();
+					$('.selectedActions .delete-selected').click();
+					expect(fakeServer.requests.length).toEqual(1);
+					request = fakeServer.requests[0];
+					expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php');
+					expect(OC.parseQueryString(request.requestBody))
+						.toEqual({'dir': '/subdir', allfiles: 'true'});
+					fakeServer.requests[0].respond(
+						200,
+						{ 'Content-Type': 'application/json' },
+						JSON.stringify({status: 'success'})
+					);
+					expect(FileList.isEmpty).toEqual(true);
+				});
+			});
+		});
+	});
 });
diff --git a/apps/files/tests/js/filesSpec.js b/apps/files/tests/js/filesSpec.js
index 018c8ef0f3cff215a718a9491e9fbad12a47c1b2..7f8848619f500c57b795439783d3d29237a3a689 100644
--- a/apps/files/tests/js/filesSpec.js
+++ b/apps/files/tests/js/filesSpec.js
@@ -19,7 +19,7 @@
 *
 */
 
-/* global Files */
+/* global OC, Files */
 describe('Files tests', function() {
 	describe('File name validation', function() {
 		it('Validates correct file names', function() {
@@ -82,4 +82,30 @@ describe('Files tests', function() {
 			}
 		});
 	});
+	describe('getDownloadUrl', function() {
+		var curDirStub;
+		beforeEach(function() {
+			curDirStub = sinon.stub(FileList, 'getCurrentDirectory');
+		});
+		afterEach(function() {
+			curDirStub.restore();
+		});
+		it('returns the ajax download URL when only filename specified', function() {
+			curDirStub.returns('/subdir');
+			var url = Files.getDownloadUrl('test file.txt');
+			expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=test%20file.txt');
+		});
+		it('returns the ajax download URL when filename and dir specified', function() {
+			var url = Files.getDownloadUrl('test file.txt', '/subdir');
+			expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=test%20file.txt');
+		});
+		it('returns the ajax download URL when filename and root dir specific', function() {
+			var url = Files.getDownloadUrl('test file.txt', '/');
+			expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=test%20file.txt');
+		});
+		it('returns the ajax download URL when multiple files specified', function() {
+			var url = Files.getDownloadUrl(['test file.txt', 'abc.txt'], '/subdir');
+			expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22test%20file.txt%22%2C%22abc.txt%22%5D');
+		});
+	});
 });
diff --git a/apps/files/tests/js/filesummarySpec.js b/apps/files/tests/js/filesummarySpec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c493700de3801d2d361092bdec90d8519c6c413e
--- /dev/null
+++ b/apps/files/tests/js/filesummarySpec.js
@@ -0,0 +1,87 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+/* global FileSummary */
+describe('FileSummary tests', function() {
+	var $container;
+
+	beforeEach(function() {
+		$container = $('<table><tr></tr></table>').find('tr');
+	});
+	afterEach(function() {
+		$container = null;
+	});
+
+	it('renders summary as text', function() {
+		var s = new FileSummary($container);
+		s.setSummary({
+			totalDirs: 5,
+			totalFiles: 2,
+			totalSize: 256000
+		});
+		expect($container.hasClass('hidden')).toEqual(false);
+		expect($container.find('.info').text()).toEqual('5 folders and 2 files');
+		expect($container.find('.filesize').text()).toEqual('250 kB');
+	});
+	it('hides summary when no files or folders', function() {
+		var s = new FileSummary($container);
+		s.setSummary({
+			totalDirs: 0,
+			totalFiles: 0,
+			totalSize: 0
+		});
+		expect($container.hasClass('hidden')).toEqual(true);
+	});
+	it('increases summary when adding files', function() {
+		var s = new FileSummary($container);
+		s.setSummary({
+			totalDirs: 5,
+			totalFiles: 2,
+			totalSize: 256000
+		});
+		s.add({type: 'file', size: 256000});
+		s.add({type: 'dir', size: 100});
+		s.update();
+		expect($container.hasClass('hidden')).toEqual(false);
+		expect($container.find('.info').text()).toEqual('6 folders and 3 files');
+		expect($container.find('.filesize').text()).toEqual('500 kB');
+		expect(s.summary.totalDirs).toEqual(6);
+		expect(s.summary.totalFiles).toEqual(3);
+		expect(s.summary.totalSize).toEqual(512100);
+	});
+	it('decreases summary when removing files', function() {
+		var s = new FileSummary($container);
+		s.setSummary({
+			totalDirs: 5,
+			totalFiles: 2,
+			totalSize: 256000
+		});
+		s.remove({type: 'file', size: 128000});
+		s.remove({type: 'dir', size: 100});
+		s.update();
+		expect($container.hasClass('hidden')).toEqual(false);
+		expect($container.find('.info').text()).toEqual('4 folders and 1 file');
+		expect($container.find('.filesize').text()).toEqual('125 kB');
+		expect(s.summary.totalDirs).toEqual(4);
+		expect(s.summary.totalFiles).toEqual(1);
+		expect(s.summary.totalSize).toEqual(127900);
+	});
+});
diff --git a/apps/files_sharing/public.php b/apps/files_sharing/public.php
index ce51eca6ddb8928bfbfbbec9f99cd1abe291d131..3abcbf291ff653a8b1a55404ef3cc465c2629598 100644
--- a/apps/files_sharing/public.php
+++ b/apps/files_sharing/public.php
@@ -138,6 +138,7 @@ if (isset($path)) {
 
 			OCP\Util::addStyle('files', 'files');
 			OCP\Util::addStyle('files', 'upload');
+			OCP\Util::addScript('files', 'filesummary');
 			OCP\Util::addScript('files', 'breadcrumb');
 			OCP\Util::addScript('files', 'files');
 			OCP\Util::addScript('files', 'filelist');
diff --git a/apps/files_trashbin/index.php b/apps/files_trashbin/index.php
index e63fe1e4188e46e2052818e53b3873d5b6b8d1a0..6e6a8a38307faa5dc7ce5da6df14b0e32267697b 100644
--- a/apps/files_trashbin/index.php
+++ b/apps/files_trashbin/index.php
@@ -11,6 +11,7 @@ $tmpl = new OCP\Template('files_trashbin', 'index', 'user');
 
 OCP\Util::addStyle('files', 'files');
 OCP\Util::addStyle('files_trashbin', 'trash');
+OCP\Util::addScript('files', 'filesummary');
 OCP\Util::addScript('files', 'breadcrumb');
 OCP\Util::addScript('files', 'filelist');
 // filelist overrides
diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js
index 7795daf2775bbc609c82c7dfcac868d565cccc48..3bb3a92b60ddd43ca46541b3cb4ce4e221ee2a3c 100644
--- a/apps/files_trashbin/js/filelist.js
+++ b/apps/files_trashbin/js/filelist.js
@@ -49,8 +49,8 @@
 		}
 	};
 
-	var oldAdd = FileList.add;
-	FileList.add = function(fileData, options) {
+	var oldRenderRow = FileList._renderRow;
+	FileList._renderRow = function(fileData, options) {
 		options = options || {};
 		var dir = FileList.getCurrentDirectory();
 		var dirListing = dir !== '' && dir !== '/';
@@ -62,7 +62,7 @@
 			fileData.displayName = fileData.name;
 			fileData.name = fileData.name + '.d' + Math.floor(fileData.mtime / 1000);
 		}
-		return oldAdd.call(this, fileData, options);
+		return oldRenderRow.call(this, fileData, options);
 	};
 
 	FileList.linkTo = function(dir){
@@ -75,4 +75,130 @@
 		$('#emptycontent').toggleClass('hidden', exists);
 		$('#filestable th').toggleClass('hidden', !exists);
 	};
+
+	var oldInit = FileList.initialize;
+	FileList.initialize = function() {
+		var result = oldInit.apply(this, arguments);
+		$('.undelete').click('click', FileList._onClickRestoreSelected);
+		return result;
+	};
+
+	FileList._removeCallback = function(result) {
+		if (result.status !== 'success') {
+			OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
+		}
+
+		var files = result.data.success;
+		var $el;
+		for (var i = 0; i < files.length; i++) {
+			$el = FileList.remove(OC.basename(files[i].filename), {updateSummary: false});
+			FileList.fileSummary.remove({type: $el.attr('data-type'), size: $el.attr('data-size')});
+		}
+		FileList.fileSummary.update();
+		FileList.updateEmptyContent();
+		enableActions();
+	}
+
+	FileList._onClickRestoreSelected = function(event) {
+		event.preventDefault();
+		var allFiles = $('#select_all').is(':checked');
+		var files = [];
+		var params = {};
+		disableActions();
+		if (allFiles) {
+			FileList.showMask();
+			params = {
+				allfiles: true,
+				dir: FileList.getCurrentDirectory()
+			};
+		}
+		else {
+			files = _.pluck(FileList.getSelectedFiles(), 'name');
+			for (var i = 0; i < files.length; i++) {
+				var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete");
+				deleteAction.removeClass('delete-icon').addClass('progress-icon');
+			}
+			params = {
+				files: JSON.stringify(files),
+				dir: FileList.getCurrentDirectory()
+			};
+		}
+
+		$.post(OC.filePath('files_trashbin', 'ajax', 'undelete.php'),
+			params,
+			function(result) {
+				if (allFiles) {
+					if (result.status !== 'success') {
+						OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
+					}
+					FileList.hideMask();
+					// simply remove all files
+					FileList.setFiles([]);
+					enableActions();
+				}
+				else {
+					FileList._removeCallback(result);
+				}
+			}
+		);
+	};
+
+	FileList._onClickDeleteSelected = function(event) {
+		event.preventDefault();
+		var allFiles = $('#select_all').is(':checked');
+		var files = [];
+		var params = {};
+		if (allFiles) {
+			params = {
+				allfiles: true,
+				dir: FileList.getCurrentDirectory()
+			};
+		}
+		else {
+			files = _.pluck(FileList.getSelectedFiles(), 'name');
+			params = {
+				files: JSON.stringify(files),
+				dir: FileList.getCurrentDirectory()
+			};
+		}
+
+		disableActions();
+		if (allFiles) {
+			FileList.showMask();
+		}
+		else {
+			for (var i = 0; i < files.length; i++) {
+				var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete");
+				deleteAction.removeClass('delete-icon').addClass('progress-icon');
+			}
+		}
+
+		$.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'),
+				params,
+				function(result) {
+					if (allFiles) {
+						if (result.status !== 'success') {
+							OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
+						}
+						FileList.hideMask();
+						// simply remove all files
+						FileList.setFiles([]);
+						enableActions();
+					}
+					else {
+						FileList._removeCallback(result);
+					}
+				}
+		);
+	};
+
+	var oldClickFile = FileList._onClickFile;
+	FileList._onClickFile = function(event) {
+		var mime = $(this).parent().parent().data('mime');
+		if (mime !== 'httpd/unix-directory') {
+			event.preventDefault();
+		}
+		return oldClickFile.apply(this, arguments);
+	};
+
 })();
diff --git a/apps/files_trashbin/js/trash.js b/apps/files_trashbin/js/trash.js
index f7724d07d2b86ee20c119216ebef49abfb4d5191..5f2436de8096c78bb7eaffdc50c0dc29f9ee1713 100644
--- a/apps/files_trashbin/js/trash.js
+++ b/apps/files_trashbin/js/trash.js
@@ -28,20 +28,6 @@ $(document).ready(function() {
 		return name;
 	}
 
-	function removeCallback(result) {
-		if (result.status !== 'success') {
-			OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
-		}
-
-		var files = result.data.success;
-		for (var i = 0; i < files.length; i++) {
-			FileList.remove(OC.basename(files[i].filename), {updateSummary: false});
-		}
-		FileList.updateFileSummary();
-		FileList.updateEmptyContent();
-		enableActions();
-	}
-
 	Files.updateStorageStatistics = function() {
 		// no op because the trashbin doesn't have
 		// storage info like free space / used space
@@ -57,7 +43,7 @@ $(document).ready(function() {
 					files: JSON.stringify([filename]),
 					dir: FileList.getCurrentDirectory()
 				},
-			    removeCallback
+			    FileList._removeCallback
 			);
 		}, t('files_trashbin', 'Restore'));
 	};
@@ -74,153 +60,10 @@ $(document).ready(function() {
 				files: JSON.stringify([filename]),
 				dir: FileList.getCurrentDirectory()
 			},
-			removeCallback
+			FileList._removeCallback
 		);
 	});
 
-	// Sets the select_all checkbox behaviour :
-	$('#select_all').click(function() {
-		if ($(this).attr('checked')) {
-			// Check all
-			$('td.filename input:checkbox').attr('checked', true);
-			$('td.filename input:checkbox').parent().parent().addClass('selected');
-		} else {
-			// Uncheck all
-			$('td.filename input:checkbox').attr('checked', false);
-			$('td.filename input:checkbox').parent().parent().removeClass('selected');
-		}
-		procesSelection();
-	});
-	$('.undelete').click('click', function(event) {
-		event.preventDefault();
-		var allFiles = $('#select_all').is(':checked');
-		var files = [];
-		var params = {};
-		disableActions();
-		if (allFiles) {
-			FileList.showMask();
-			params = {
-				allfiles: true,
-				dir: FileList.getCurrentDirectory()
-			};
-		}
-		else {
-			files = Files.getSelectedFiles('name');
-			for (var i = 0; i < files.length; i++) {
-				var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete");
-				deleteAction.removeClass('delete-icon').addClass('progress-icon');
-			}
-			params = {
-				files: JSON.stringify(files),
-				dir: FileList.getCurrentDirectory()
-			};
-		}
-
-		$.post(OC.filePath('files_trashbin', 'ajax', 'undelete.php'),
-			params,
-			function(result) {
-				if (allFiles) {
-					if (result.status !== 'success') {
-						OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
-					}
-					FileList.hideMask();
-					// simply remove all files
-					FileList.update('');
-					enableActions();
-				}
-				else {
-					removeCallback(result);
-				}
-			}
-		);
-	});
-
-	$('.delete').click('click', function(event) {
-		event.preventDefault();
-		var allFiles = $('#select_all').is(':checked');
-		var files = [];
-		var params = {};
-		if (allFiles) {
-			params = {
-				allfiles: true,
-				dir: FileList.getCurrentDirectory()
-			};
-		}
-		else {
-			files = Files.getSelectedFiles('name');
-			params = {
-				files: JSON.stringify(files),
-				dir: FileList.getCurrentDirectory()
-			};
-		}
-
-		disableActions();
-		if (allFiles) {
-			FileList.showMask();
-		}
-		else {
-			for (var i = 0; i < files.length; i++) {
-				var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete");
-				deleteAction.removeClass('delete-icon').addClass('progress-icon');
-			}
-		}
-
-		$.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'),
-				params,
-				function(result) {
-					if (allFiles) {
-						if (result.status !== 'success') {
-							OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error'));
-						}
-						FileList.hideMask();
-						// simply remove all files
-						FileList.setFiles([]);
-						enableActions();
-					}
-					else {
-						removeCallback(result);
-					}
-				}
-		);
-
-	});
-
-	$('#fileList').on('click', 'td.filename input', function() {
-		var checkbox = $(this).parent().children('input:checkbox');
-		$(checkbox).parent().parent().toggleClass('selected');
-		if ($(checkbox).is(':checked')) {
-			var selectedCount = $('td.filename input:checkbox:checked').length;
-			if (selectedCount === $('td.filename input:checkbox').length) {
-				$('#select_all').prop('checked', true);
-			}
-		} else {
-			$('#select_all').prop('checked',false);
-		}
-		procesSelection();
-	});
-
-	$('#fileList').on('click', 'td.filename a', function(event) {
-		var mime = $(this).parent().parent().data('mime');
-		if (mime !== 'httpd/unix-directory') {
-			event.preventDefault();
-		}
-		var filename = $(this).parent().parent().attr('data-file');
-		var tr = FileList.findFileEl(filename);
-		var renaming = tr.data('renaming');
-		if(!renaming){
-			if(mime.substr(0, 5) === 'text/'){ //no texteditor for now
-				return;
-			}
-			var type = $(this).parent().parent().data('type');
-			var permissions = $(this).parent().parent().data('permissions');
-			var action = FileActions.getDefault(mime, type, permissions);
-			if(action){
-				event.preventDefault();
-				action(filename);
-			}
-		}
-	});
-
 	/**
 	 * Override crumb URL maker (hacky!)
 	 */
diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php
index b6c61c9b1c3ea136b2727fda463d8e385a505668..323e7495535ad08ba10b65aa95a43bb3e1e1a1e4 100644
--- a/apps/files_trashbin/templates/index.php
+++ b/apps/files_trashbin/templates/index.php
@@ -29,7 +29,7 @@
 			<th id="headerDate">
 				<span id="modified"><?php p($l->t( 'Deleted' )); ?></span>
 				<span class="selectedActions">
-					<a href="" class="delete">
+					<a href="" class="delete-selected">
 						<?php p($l->t('Delete'))?>
 						<img class="svg" alt="<?php p($l->t('Delete'))?>"
 							src="<?php print_unescaped(OCP\image_path("core", "actions/delete.svg")); ?>" />
@@ -40,4 +40,6 @@
 	</thead>
 	<tbody id="fileList">
 	</tbody>
+	<tfoot>
+	</tfoot>
 </table>
diff --git a/core/css/apps.css b/core/css/apps.css
index a8dfc5b7ed12881a795c1a1e144cb715cb66e5dc..a0bb262854dcbbb4ca97b03cc911021649cd5aa0 100644
--- a/core/css/apps.css
+++ b/core/css/apps.css
@@ -243,7 +243,6 @@ button.loading {
 	padding-right: 30px;
 }
 
-
 /* general styles for the content area */
 .section {
 	display: block;
@@ -264,3 +263,14 @@ button.loading {
 	vertical-align: -2px;
 	margin-right: 4px;
 }
+.appear {
+	opacity: 1;
+	transition: opacity 500ms ease 0s;
+	-moz-transition: opacity 500ms ease 0s;
+	-ms-transition: opacity 500ms ease 0s;
+	-o-transition: opacity 500ms ease 0s;
+	-webkit-transition: opacity 500ms ease 0s;
+}
+.appear.transparent {
+	opacity: 0;
+}
diff --git a/core/js/js.js b/core/js/js.js
index 0aa8d12b3d67bf90e4408e2f47ca1356ca3363a6..27bc3c651e33125ef27986314b3576df4ac27477 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -1250,9 +1250,12 @@ function relative_modified_date(timestamp) {
 }
 
 /**
- *  @todo Write documentation
+ * Utility functions
  */
 OC.Util = {
+	// TODO: remove original functions from global namespace
+	humanFileSize: humanFileSize,
+	formatDate: formatDate,
 	/**
 	 * Returns whether the browser supports SVG
 	 * @return {boolean} true if the browser supports SVG, false otherwise
diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js
index ccd9f7a128898003adb606154b2d88a028ce3724..65f768fbc5173416387e57e0dfdc234e8632b02d 100644
--- a/core/js/tests/specs/coreSpec.js
+++ b/core/js/tests/specs/coreSpec.js
@@ -474,5 +474,22 @@ describe('Core base tests', function() {
 			);
 		});
 	});
+	describe('Util', function() {
+		describe('humanFileSize', function() {
+			it('renders file sizes with the correct unit', function() {
+				var data = [
+					[0, '0 B'],
+					[125, '125 B'],
+					[128000, '125 kB'],
+					[128000000, '122.1 MB'],
+					[128000000000, '119.2 GB'],
+					[128000000000000, '116.4 TB']
+				];
+				for (var i = 0; i < data.length; i++) {
+					expect(OC.Util.humanFileSize(data[i][0])).toEqual(data[i][1]);
+				}
+			});
+		});
+	});
 });