diff options
25 files changed, 1198 insertions, 27 deletions
diff --git a/apps/files/css/detailsView.css b/apps/files/css/detailsView.css new file mode 100644 index 00000000000..76629cb790f --- /dev/null +++ b/apps/files/css/detailsView.css @@ -0,0 +1,55 @@ +#app-sidebar .detailFileInfoContainer { + min-height: 50px; + padding: 15px; +} + +#app-sidebar .detailFileInfoContainer > div { + clear: both; +} + +#app-sidebar .mainFileInfoView { + margin-right: 20px; /* accomodate for close icon */ +} + +#app-sidebar .thumbnail { + width: 50px; + height: 50px; + float: left; + margin-right: 10px; + background-size: 50px; +} + +#app-sidebar .ellipsis { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +#app-sidebar .fileName { + font-size: 16px; + padding-top: 3px; +} + +#app-sidebar .file-details { + margin-top: 3px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + opacity: .5; +} +#app-sidebar .action-favorite { + vertical-align: text-bottom; + padding: 10px; + margin: -10px; +} + +#app-sidebar .detailList { + float: left; +} + +#app-sidebar .close { + position: absolute; + top: 0; + right: 0; + padding: 15px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + opacity: .5; +} diff --git a/apps/files/css/files.css b/apps/files/css/files.css index f2f2c5ac3bc..7e3318a962b 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -103,6 +103,10 @@ min-height: 100%; } +.app-files #app-content { + overflow-x: hidden; +} + /* icons for sidebar */ .nav-icon-files { background-image: url('../img/folder.svg'); @@ -143,6 +147,7 @@ #filestable tbody tr:active { background-color: rgb(240,240,240); } +#filestable tbody tr.highlighted, #filestable tbody tr.selected { background-color: rgb(230,230,230); } diff --git a/apps/files/index.php b/apps/files/index.php index 4f103f975cb..dca3e5ae74d 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -50,6 +50,12 @@ OCP\Util::addscript('files', 'search'); \OCP\Util::addScript('files', 'tagsplugin'); \OCP\Util::addScript('files', 'favoritesplugin'); +\OCP\Util::addScript('files', 'detailfileinfoview'); +\OCP\Util::addScript('files', 'detailtabview'); +\OCP\Util::addScript('files', 'mainfileinfodetailview'); +\OCP\Util::addScript('files', 'detailsview'); +\OCP\Util::addStyle('files', 'detailsView'); + \OC_Util::addVendorScript('core', 'handlebars/handlebars'); OCP\App::setActiveNavigationEntry('files_index'); diff --git a/apps/files/js/detailfileinfoview.js b/apps/files/js/detailfileinfoview.js new file mode 100644 index 00000000000..9a88b5e2d8a --- /dev/null +++ b/apps/files/js/detailfileinfoview.js @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + /** + * @class OCA.Files.DetailFileInfoView + * @classdesc + * + * Displays a block of details about the file info. + * + */ + var DetailFileInfoView = function() { + this.initialize(); + }; + /** + * @memberof OCA.Files + */ + DetailFileInfoView.prototype = { + /** + * jQuery element + */ + $el: null, + + _template: null, + + /** + * Currently displayed file info + * + * @type OCA.Files.FileInfo + */ + _fileInfo: null, + + /** + * Initialize the details view + */ + initialize: function() { + this.$el = $('<div class="detailFileInfoView"></div>'); + }, + + /** + * returns the jQuery object for HTML output + * + * @returns {jQuery} + */ + get$: function() { + return this.$el; + }, + + /** + * Destroy / uninitialize this instance. + */ + destroy: function() { + if (this.$el) { + this.$el.remove(); + } + }, + + /** + * Renders this details view + * + * @abstract + */ + render: function() { + // to be implemented in subclass + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + this.render(); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this._fileInfo; + } + }; + + OCA.Files.DetailFileInfoView = DetailFileInfoView; +})(); + diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js new file mode 100644 index 00000000000..7b7bd013f9e --- /dev/null +++ b/apps/files/js/detailsview.js @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + var TEMPLATE = + '<div>' + + ' <div class="detailFileInfoContainer">' + + ' </div>' + + ' <div>' + + ' <ul class="tabHeaders">' + + ' </ul>' + + ' <div class="tabsContainer">' + + ' </div>' + + ' </div>' + + ' <a class="close icon-close" href="#" alt="{{closeLabel}}"></a>' + + '</div>'; + + var TEMPLATE_TAB_HEADER = + '<li class="tabHeader {{#if selected}}selected{{/if}}" data-tabid="{{tabId}}" data-tabindex="{{tabIndex}}"><a href="#">{{label}}</a></li>'; + + /** + * @class OCA.Files.DetailsView + * @classdesc + * + * The details view show details about a selected file. + * + */ + var DetailsView = function() { + this.initialize(); + }; + + /** + * @memberof OCA.Files + */ + DetailsView.prototype = { + + /** + * jQuery element + */ + $el: null, + + _template: null, + _templateTabHeader: null, + + /** + * Currently displayed file info + * + * @type OCA.Files.FileInfo + */ + _fileInfo: null, + + /** + * List of detail tab views + * + * @type Array<OCA.Files.DetailTabView> + */ + _tabViews: [], + + /** + * List of detail file info views + * + * @type Array<OCA.Files.DetailFileInfoView> + */ + _detailFileInfoViews: [], + + /** + * Id of the currently selected tab + * + * @type string + */ + _currentTabId: null, + + /** + * Initialize the details view + */ + initialize: function() { + this.$el = $('<div id="app-sidebar"></div>'); + this.fileInfo = null; + this._tabViews = []; + this._detailFileInfoViews = []; + + this.$el.on('click', 'a.close', function(event) { + OC.Apps.hideAppSidebar(); + event.preventDefault(); + }); + + this.$el.on('click', '.tabHeaders .tabHeader', _.bind(this._onClickTab, this)); + + // uncomment to add some dummy tabs for testing + //this._addTestTabs(); + }, + + /** + * Destroy / uninitialize this instance. + */ + destroy: function() { + if (this.$el) { + this.$el.remove(); + } + }, + + _onClickTab: function(e) { + var $target = $(e.target); + if (!$target.hasClass('tabHeader')) { + $target = $target.closest('.tabHeader'); + } + var tabIndex = $target.attr('data-tabindex'); + var targetTab; + if (_.isUndefined(tabIndex)) { + return; + } + + this.$el.find('.tabsContainer .tab').addClass('hidden'); + targetTab = this._tabViews[tabIndex]; + targetTab.$el.removeClass('hidden'); + + this.$el.find('.tabHeaders li').removeClass('selected'); + $target.addClass('selected'); + + e.preventDefault(); + }, + + _addTestTabs: function() { + for (var j = 0; j < 2; j++) { + var testView = new OCA.Files.DetailTabView('testtab' + j); + testView.index = j; + testView.getLabel = function() { return 'Test tab ' + this.index; }; + testView.render = function() { + this.$el.empty(); + for (var i = 0; i < 100; i++) { + this.$el.append('<div>Test tab ' + this.index + ' row ' + i + '</div>'); + } + }; + this._tabViews.push(testView); + } + }, + + /** + * Renders this details view + */ + render: function() { + var self = this; + this.$el.empty(); + + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + + if (!this._templateTabHeader) { + this._templateTabHeader = Handlebars.compile(TEMPLATE_TAB_HEADER); + } + + var $el = $(this._template({ + closeLabel: t('files', 'Close') + })); + var $tabsContainer = $el.find('.tabsContainer'); + var $tabHeadsContainer = $el.find('.tabHeaders'); + var $detailsContainer = $el.find('.detailFileInfoContainer'); + + // render details + _.each(this._detailFileInfoViews, function(detailView) { + $detailsContainer.append(detailView.get$()); + }); + + if (this._tabViews.length > 0) { + if (!this._currentTab) { + this._currentTab = this._tabViews[0].getId(); + } + + // render tabs + _.each(this._tabViews, function(tabView, i) { + // hidden by default + var $el = tabView.get$(); + var isCurrent = (tabView.getId() === self._currentTab); + if (!isCurrent) { + $el.addClass('hidden'); + } + $tabsContainer.append($el); + + $tabHeadsContainer.append(self._templateTabHeader({ + tabId: tabView.getId(), + tabIndex: i, + label: tabView.getLabel(), + selected: isCurrent + })); + }); + } + + // TODO: select current tab + + this.$el.append($el); + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + + this.render(); + + // notify all panels + _.each(this._tabViews, function(tabView) { + tabView.setFileInfo(fileInfo); + }); + _.each(this._detailFileInfoViews, function(detailView) { + detailView.setFileInfo(fileInfo); + }); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this._fileInfo; + }, + + /** + * Adds a tab in the tab view + * + * @param {OCA.Files.DetailTabView} tab view + */ + addTabView: function(tabView) { + this._tabViews.push(tabView); + }, + + /** + * Adds a detail view for file info. + * + * @param {OCA.Files.DetailFileInfoView} detail view + */ + addDetailView: function(detailView) { + this._detailFileInfoViews.push(detailView); + } + }; + + OCA.Files.DetailsView = DetailsView; +})(); + diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js new file mode 100644 index 00000000000..b9b1dda2ccc --- /dev/null +++ b/apps/files/js/detailtabview.js @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + /** + * @class OCA.Files.DetailTabView + * @classdesc + * + * Base class for tab views to display file information. + * + */ + var DetailTabView = function(id) { + this.initialize(id); + }; + + /** + * @memberof OCA.Files + */ + DetailTabView.prototype = { + /** + * jQuery element + */ + $el: null, + + /** + * Tab id + */ + _id: null, + + /** + * Tab label + */ + _label: null, + + _template: null, + + /** + * Currently displayed file info + * + * @type OCA.Files.FileInfo + */ + _fileInfo: null, + + /** + * Initialize the details view + * + * @param {string} id tab id + */ + initialize: function(id) { + if (!id) { + throw 'Argument "id" is required'; + } + this._id = id; + this.$el = $('<div class="tab"></div>'); + this.$el.attr('data-tabid', id); + }, + + /** + * Destroy / uninitialize this instance. + */ + destroy: function() { + if (this.$el) { + this.$el.remove(); + } + }, + + /** + * Returns the tab element id + * + * @return {string} tab id + */ + getId: function() { + return this._id; + }, + + /** + * Returns the tab label + * + * @return {String} label + */ + getLabel: function() { + return 'Tab ' + this._id; + }, + + /** + * returns the jQuery object for HTML output + * + * @returns {jQuery} + */ + get$: function() { + return this.$el; + }, + + /** + * Renders this details view + * + * @abstract + */ + render: function() { + // to be implemented in subclass + // FIXME: code is only for testing + this.$el.empty(); + this.$el.append('<div>Hello ' + this._id + '</div>'); + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + this.render(); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this._fileInfo; + } + }; + + OCA.Files.DetailTabView = DetailTabView; +})(); + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index c56c786929a..f5629ecd2c3 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -23,6 +23,7 @@ * @param [options.scrollContainer] scrollable container, defaults to $(window) * @param [options.dragOptions] drag options, disabled by default * @param [options.folderDropOptions] folder drop options, disabled by default + * @param [options.detailsViewEnabled=true] whether to enable details view */ var FileList = function($el, options) { this.initialize($el, options); @@ -65,6 +66,11 @@ fileSummary: null, /** + * @type OCA.Files.DetailsView + */ + _detailsView: null, + + /** * Whether the file list was initialized already. * @type boolean */ @@ -205,6 +211,13 @@ } this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions); + if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) { + this._detailsView = new OCA.Files.DetailsView(); + this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView()); + this._detailsView.$el.insertBefore(this.$el); + this._detailsView.$el.addClass('disappear'); + } + this.$el.find('#controls').prepend(this.breadcrumb.$el); this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); @@ -216,6 +229,13 @@ this.updateSearch(); + this.$el.on('click', function(event) { + var $target = $(event.target); + // click outside file row ? + if (!$target.closest('tbody').length && !$target.closest('#app-sidebar').length) { + self._updateDetailsView(null); + } + }); this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this)); this.$el.on('urlChanged', _.bind(this._onUrlChanged, this)); @@ -263,6 +283,37 @@ }, /** + * Update the details view to display the given file + * + * @param {OCA.Files.FileInfo} fileInfo file info to display + */ + _updateDetailsView: function(fileInfo) { + if (!this._detailsView) { + return; + } + + var self = this; + var oldFileInfo = this._detailsView.getFileInfo(); + if (oldFileInfo) { + // TODO: use more efficient way, maybe track the highlight + this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.id).removeClass('highlighted'); + } + + if (!fileInfo) { + OC.Apps.hideAppSidebar(); + this._detailsView.setFileInfo(null); + return; + } + + this.$fileList.children().filterAttr('data-id', '' + fileInfo.id).addClass('highlighted'); + this._detailsView.setFileInfo(_.extend({ + path: this.getCurrentDirectory() + }, fileInfo)); + this._detailsView.$el.scrollTop(0); + _.defer(OC.Apps.showAppSidebar); + }, + + /** * Event handler for when the window size changed */ _onResize: function() { @@ -315,6 +366,12 @@ delete this._selectedFiles[$tr.data('id')]; this._selectionSummary.remove(data); } + if (this._selectionSummary.getTotal() === 1) { + this._updateDetailsView(_.values(this._selectedFiles)[0]); + } else { + // show nothing when multiple files are selected + this._updateDetailsView(null); + } this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length); }, @@ -350,27 +407,34 @@ this._selectFileEl($tr, !$checkbox.prop('checked')); this.updateSelectionSummary(); } else { - var filename = $tr.attr('data-file'); - var renaming = $tr.data('renaming'); - if (!renaming) { - this.fileActions.currentFile = $tr.find('td'); - var mime = this.fileActions.getCurrentMimeType(); - var type = this.fileActions.getCurrentType(); - var permissions = this.fileActions.getCurrentPermissions(); - var action = this.fileActions.getDefault(mime,type, permissions); - if (action) { - event.preventDefault(); - // also set on global object for legacy apps - window.FileActions.currentFile = this.fileActions.currentFile; - action(filename, { - $file: $tr, - fileList: this, - fileActions: this.fileActions, - dir: $tr.attr('data-path') || this.getCurrentDirectory() - }); + // clicked directly on the name + if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) { + var filename = $tr.attr('data-file'); + var renaming = $tr.data('renaming'); + if (!renaming) { + this.fileActions.currentFile = $tr.find('td'); + var mime = this.fileActions.getCurrentMimeType(); + var type = this.fileActions.getCurrentType(); + var permissions = this.fileActions.getCurrentPermissions(); + var action = this.fileActions.getDefault(mime,type, permissions); + if (action) { + event.preventDefault(); + // also set on global object for legacy apps + window.FileActions.currentFile = this.fileActions.currentFile; + action(filename, { + $file: $tr, + fileList: this, + fileActions: this.fileActions, + dir: $tr.attr('data-path') || this.getCurrentDirectory() + }); + } + // deselect row + $(event.target).closest('a').blur(); } - // deselect row - $(event.target).closest('a').blur(); + } else { + var fileInfo = this.files[$tr.index()]; + this._updateDetailsView(fileInfo); + event.preventDefault(); } } }, @@ -825,7 +889,7 @@ var formatted; var text; if (mtime > 0) { - formatted = formatDate(mtime); + formatted = OC.Util.formatDate(mtime); text = OC.Util.relativeModifiedDate(mtime); } else { formatted = t('files', 'Unable to determine date'); @@ -1239,6 +1303,12 @@ ready(iconURL); // set mimeicon URL urlSpec.file = OCA.Files.Files.fixPath(path); + if (options.x) { + urlSpec.x = options.x; + } + if (options.y) { + urlSpec.y = options.y; + } if (etag){ // use etag as cache buster @@ -1521,6 +1591,7 @@ tr.remove(); tr = self.add(fileInfo, {updateSummary: false, silent: true}); self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); + self._updateDetailsView(fileInfo); } }); } else { @@ -2177,6 +2248,20 @@ } }); + }, + + /** + * Register a tab view to be added to all views + */ + registerTabView: function(tabView) { + this._detailsView.addTabView(tabView); + }, + + /** + * Register a detail view to be added to all views + */ + registerDetailView: function(detailView) { + this._detailsView.addDetailView(detailView); } }; diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js new file mode 100644 index 00000000000..a00d907d0d6 --- /dev/null +++ b/apps/files/js/mainfileinfodetailview.js @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var TEMPLATE = + '<div class="thumbnail"></div><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' + + '<div class="file-details ellipsis">' + + ' <a href="#" ' + + ' alt="{{starAltText}}"' + + ' class="action action-favorite favorite">' + + ' <img class="svg" src="{{starIcon}}" />' + + ' </a>' + + ' <span class="size" title="{{altSize}}">{{size}}</span>, <span class="date" title="{{altDate}}">{{date}}</span>' + + '</div>'; + + /** + * @class OCA.Files.MainFileInfoDetailView + * @classdesc + * + * Displays main details about a file + * + */ + var MainFileInfoDetailView = function() { + this.initialize(); + }; + /** + * @memberof OCA.Files + */ + MainFileInfoDetailView.prototype = _.extend({}, OCA.Files.DetailFileInfoView.prototype, + /** @lends OCA.Files.MainFileInfoDetailView.prototype */ { + _template: null, + + /** + * Initialize the details view + */ + initialize: function() { + this.$el = $('<div class="mainFileInfoView"></div>'); + }, + + /** + * Renders this details view + */ + render: function() { + this.$el.empty(); + + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + + if (this._fileInfo) { + var isFavorite = (this._fileInfo.tags || []).indexOf(OC.TAG_FAVORITE) >= 0; + this.$el.append(this._template({ + nameLabel: t('files', 'Name'), + name: this._fileInfo.name, + pathLabel: t('files', 'Path'), + path: this._fileInfo.path, + sizeLabel: t('files', 'Size'), + size: OC.Util.humanFileSize(this._fileInfo.size, true), + altSize: n('files', '%n byte', '%n bytes', this._fileInfo.size), + dateLabel: t('files', 'Modified'), + altDate: OC.Util.formatDate(this._fileInfo.mtime), + date: OC.Util.relativeModifiedDate(this._fileInfo.mtime), + starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'), + starIcon: OC.imagePath('core', isFavorite ? 'actions/starred' : 'actions/star') + })); + + // TODO: we really need OC.Previews + var $iconDiv = this.$el.find('.thumbnail'); + if (this._fileInfo.mimetype !== 'httpd/unix-directory') { + // TODO: inject utility class? + FileList.lazyLoadPreview({ + path: this._fileInfo.path + '/' + this._fileInfo.name, + mime: this._fileInfo.mimetype, + etag: this._fileInfo.etag, + x: 50, + y: 50, + callback: function(previewUrl) { + $iconDiv.css('background-image', 'url("' + previewUrl + '")'); + } + }); + } else { + // TODO: special icons / shared / external + $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")'); + } + this.$el.find('[title]').tooltip({placement: 'bottom'}); + } + } + }); + + OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView; +})(); diff --git a/apps/files/tests/js/detailsviewSpec.js b/apps/files/tests/js/detailsviewSpec.js new file mode 100644 index 00000000000..db1e24fd68e --- /dev/null +++ b/apps/files/tests/js/detailsviewSpec.js @@ -0,0 +1,105 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 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/>. +* +*/ + +describe('OCA.Files.DetailsView tests', function() { + var detailsView; + + beforeEach(function() { + detailsView = new OCA.Files.DetailsView(); + }); + afterEach(function() { + detailsView.destroy(); + detailsView = undefined; + }); + it('renders itself empty when nothing registered', function() { + detailsView.render(); + expect(detailsView.$el.find('.detailFileInfoContainer').length).toEqual(1); + expect(detailsView.$el.find('.tabsContainer').length).toEqual(1); + }); + describe('file info detail view', function() { + it('renders registered view', function() { + var testView = new OCA.Files.DetailFileInfoView(); + var testView2 = new OCA.Files.DetailFileInfoView(); + detailsView.addDetailView(testView); + detailsView.addDetailView(testView2); + detailsView.render(); + + expect(detailsView.$el.find('.detailFileInfoContainer .detailFileInfoView').length).toEqual(2); + }); + it('updates registered tabs when fileinfo is updated', function() { + var viewRenderStub = sinon.stub(OCA.Files.DetailFileInfoView.prototype, 'render'); + var testView = new OCA.Files.DetailFileInfoView(); + var testView2 = new OCA.Files.DetailFileInfoView(); + detailsView.addDetailView(testView); + detailsView.addDetailView(testView2); + detailsView.render(); + + var fileInfo = {id: 5, name: 'test.txt'}; + viewRenderStub.reset(); + detailsView.setFileInfo(fileInfo); + + expect(testView.getFileInfo()).toEqual(fileInfo); + expect(testView2.getFileInfo()).toEqual(fileInfo); + + expect(viewRenderStub.callCount).toEqual(2); + viewRenderStub.restore(); + }); + }); + describe('tabs', function() { + var testView, testView2; + + beforeEach(function() { + testView = new OCA.Files.DetailTabView('test1'); + testView2 = new OCA.Files.DetailTabView('test2'); + detailsView.addTabView(testView); + detailsView.addTabView(testView2); + detailsView.render(); + }); + it('renders registered tabs', function() { + expect(detailsView.$el.find('.tab').length).toEqual(2); + }); + it('updates registered tabs when fileinfo is updated', function() { + var tabRenderStub = sinon.stub(OCA.Files.DetailTabView.prototype, 'render'); + var fileInfo = {id: 5, name: 'test.txt'}; + tabRenderStub.reset(); + detailsView.setFileInfo(fileInfo); + + expect(testView.getFileInfo()).toEqual(fileInfo); + expect(testView2.getFileInfo()).toEqual(fileInfo); + + expect(tabRenderStub.callCount).toEqual(2); + tabRenderStub.restore(); + }); + it('selects the first tab by default', function() { + expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(true); + expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(false); + expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(false); + expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(true); + }); + it('switches the current tab when clicking on tab header', function() { + detailsView.$el.find('.tabHeader').eq(1).click(); + expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(false); + expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(true); + expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(true); + expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(false); + }); + }); +}); diff --git a/apps/files/tests/js/favoritespluginspec.js b/apps/files/tests/js/favoritespluginspec.js index 90b40ede74b..1b144c28707 100644 --- a/apps/files/tests/js/favoritespluginspec.js +++ b/apps/files/tests/js/favoritespluginspec.js @@ -113,7 +113,7 @@ describe('OCA.Files.FavoritesPlugin tests', function() { shareOwner: 'user2' }]); - fileList.findFileEl('testdir').find('td a.name').click(); + fileList.findFileEl('testdir').find('td .nametext').click(); expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir'); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 09d698088ae..5c0c8c96bc5 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -1870,6 +1870,50 @@ describe('OCA.Files.FileList tests', function() { }); }) }); + describe('Details sidebar', function() { + beforeEach(function() { + fileList.setFiles(testFiles); + }); + it('Clicking on a file row will trigger file action if no details view configured', function() { + fileList._detailsView = null; + var updateDetailsViewStub = sinon.stub(fileList, '_updateDetailsView'); + var actionStub = sinon.stub(); + fileList.setFiles(testFiles); + fileList.fileActions.register( + 'text/plain', + 'Test', + OC.PERMISSION_ALL, + function() { + // Specify icon for hitory button + return OC.imagePath('core','actions/history'); + }, + actionStub + ); + fileList.fileActions.setDefault('text/plain', 'Test'); + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + expect(actionStub.calledOnce).toEqual(true); + expect(updateDetailsViewStub.notCalled).toEqual(true); + updateDetailsViewStub.restore(); + }); + it('Clicking on a file row will trigger details sidebar', function() { + fileList.fileActions.setDefault('text/plain', 'Test'); + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + expect($tr.hasClass('highlighted')).toEqual(true); + + expect(fileList._detailsView.getFileInfo().id).toEqual(1); + }); + it('Clicking outside to deselect a file row will trigger details sidebar', function() { + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + + fileList.$el.find('tfoot').click(); + + expect($tr.hasClass('highlighted')).toEqual(false); + expect(fileList._detailsView.getFileInfo()).toEqual(null); + }); + }); describe('File actions', function() { it('Clicking on a file name will trigger default action', function() { var actionStub = sinon.stub(); @@ -1886,7 +1930,7 @@ describe('OCA.Files.FileList tests', function() { ); fileList.fileActions.setDefault('text/plain', 'Test'); var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filename>a.name').click(); + $tr.find('td.filename .nametext').click(); expect(actionStub.calledOnce).toEqual(true); expect(actionStub.getCall(0).args[0]).toEqual('One.txt'); var context = actionStub.getCall(0).args[1]; diff --git a/apps/files/tests/js/mainfileinfodetailviewSpec.js b/apps/files/tests/js/mainfileinfodetailviewSpec.js new file mode 100644 index 00000000000..10ad38097c6 --- /dev/null +++ b/apps/files/tests/js/mainfileinfodetailviewSpec.js @@ -0,0 +1,104 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 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/>. +* +*/ + +describe('OCA.Files.MainFileInfoDetailView tests', function() { + var view, tooltipStub, previewStub, fncLazyLoadPreview, fileListMock; + + beforeEach(function() { + tooltipStub = sinon.stub($.fn, 'tooltip'); + fileListMock = sinon.mock(OCA.Files.FileList.prototype); + view = new OCA.Files.MainFileInfoDetailView(); + }); + afterEach(function() { + view.destroy(); + view = undefined; + tooltipStub.restore(); + fileListMock.restore(); + + }); + describe('rendering', function() { + var testFileInfo; + beforeEach(function() { + view = new OCA.Files.MainFileInfoDetailView(); + testFileInfo = { + id: 5, + name: 'One.txt', + path: '/subdir', + size: 123456789, + mtime: Date.UTC(2015, 6, 17, 1, 2, 0, 0) + }; + }); + it('displays basic info', function() { + var clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3)); + var dateExpected = OC.Util.formatDate(Date(Date.UTC(2015, 6, 17, 1, 2, 0, 0))); + view.setFileInfo(testFileInfo); + expect(view.$el.find('.fileName').text()).toEqual('One.txt'); + expect(view.$el.find('.fileName').attr('title')).toEqual('One.txt'); + expect(view.$el.find('.size').text()).toEqual('117.7 MB'); + expect(view.$el.find('.size').attr('title')).toEqual('123456789 bytes'); + expect(view.$el.find('.date').text()).toEqual('a few seconds ago'); + expect(view.$el.find('.date').attr('title')).toEqual(dateExpected); + clock.restore(); + }); + it('displays favorite icon', function() { + view.setFileInfo(_.extend(testFileInfo, { + tags: [OC.TAG_FAVORITE] + })); + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/starred')); + + view.setFileInfo(_.extend(testFileInfo, { + tags: [] + })); + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/star')); + }); + it('displays mime icon', function() { + // File + view.setFileInfo(_.extend(testFileInfo, { + mimetype: 'text/calendar' + })); + + expect(view.$el.find('.thumbnail').css('background-image')) + .toContain('filetypes/text-calendar.svg'); + + // Folder + view.setFileInfo(_.extend(testFileInfo, { + mimetype: 'httpd/unix-directory' + })); + + expect(view.$el.find('.thumbnail').css('background-image')) + .toContain('filetypes/folder.svg'); + }); + it('displays thumbnail', function() { + view.setFileInfo(_.extend(testFileInfo, { + mimetype: 'text/plain' + })); + + var expectation = fileListMock.expects('lazyLoadPreview'); + expectation.once(); + + view.setFileInfo(testFileInfo); + + fileListMock.verify(); + }); + }); +}); diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php index f72f5024622..9000fafd8dd 100644 --- a/apps/files_sharing/appinfo/app.php +++ b/apps/files_sharing/appinfo/app.php @@ -56,6 +56,7 @@ $application->setupPropagation(); \OCP\Util::addScript('files_sharing', 'share'); \OCP\Util::addScript('files_sharing', 'external'); +\OCP\Util::addStyle('files_sharing', 'sharetabview'); // FIXME: registering a job here will cause additional useless SQL queries // when the route is not cron.php, needs a better way diff --git a/apps/files_sharing/css/sharetabview.css b/apps/files_sharing/css/sharetabview.css new file mode 100644 index 00000000000..42c9bee7173 --- /dev/null +++ b/apps/files_sharing/css/sharetabview.css @@ -0,0 +1,3 @@ +.app-files .shareTabView { + min-height: 100px; +} diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index 5923e426f05..1993efe7d73 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -57,7 +57,8 @@ OCA.Sharing.PublicApp = { scrollContainer: $(window), dragOptions: dragOptions, folderDropOptions: folderDropOptions, - fileActions: fileActions + fileActions: fileActions, + detailsViewEnabled: false } ); this.files = OCA.Files.Files; diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index e7823454c53..12bec0e8c9a 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -140,6 +140,10 @@ } }); }, t('files_sharing', 'Share')); + + OC.addScript('files_sharing', 'sharetabview').done(function() { + fileList.registerTabView(new OCA.Sharing.ShareTabView('shareTabView')); + }); }, /** diff --git a/apps/files_sharing/js/sharetabview.js b/apps/files_sharing/js/sharetabview.js new file mode 100644 index 00000000000..e02de923751 --- /dev/null +++ b/apps/files_sharing/js/sharetabview.js @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var TEMPLATE = + '<div>Owner: {{owner}}'; + + /** + * @class OCA.Sharing.ShareTabView + * @classdesc + * + * Displays sharing information + * + */ + var ShareTabView = function(id) { + this.initialize(id); + }; + /** + * @memberof OCA.Sharing + */ + ShareTabView.prototype = _.extend({}, OCA.Files.DetailTabView.prototype, + /** @lends OCA.Sharing.ShareTabView.prototype */ { + _template: null, + + /** + * Initialize the details view + */ + initialize: function() { + OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments); + this.$el.addClass('shareTabView'); + }, + + getLabel: function() { + return t('files_sharing', 'Sharing'); + }, + + /** + * Renders this details view + */ + render: function() { + this.$el.empty(); + + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + + if (this._fileInfo) { + this.$el.append(this._template({ + owner: this._fileInfo.shareOwner || OC.currentUser + })); + + } else { + // TODO: render placeholder text? + } + } + }); + + OCA.Sharing.ShareTabView = ShareTabView; +})(); + diff --git a/apps/files_sharing/tests/js/appSpec.js b/apps/files_sharing/tests/js/appSpec.js index 49bca568001..133bd44f750 100644 --- a/apps/files_sharing/tests/js/appSpec.js +++ b/apps/files_sharing/tests/js/appSpec.js @@ -132,7 +132,7 @@ describe('OCA.Sharing.App tests', function() { shareOwner: 'user2' }]); - fileListIn.findFileEl('testdir').find('td a.name').click(); + fileListIn.findFileEl('testdir').find('td .nametext').click(); expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir'); diff --git a/core/css/apps.css b/core/css/apps.css index 57133729f15..5769120c5ed 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -417,7 +417,39 @@ min-height: 100%; } +/* APP-SIDEBAR ----------------------------------------------------------------*/ + +/* + Sidebar: a sidebar to be used within #app-content + have it as first element within app-content in order to shrink other + sibling containers properly. Compare Files app for example. +*/ +#app-sidebar { + position: fixed; + top: 45px; + right: 0; + left: auto; + bottom: 0; + width: 27%; + display: block; + background: #eee; + -webkit-transition: margin-right 300ms; + -moz-transition: margin-right 300ms; + -o-transition: margin-right 300ms; + transition: margin-right 300ms; + overflow-x: hidden; + overflow-y: auto; + visibility: visible; + z-index: 500; +} +#app-content.with-app-sidebar { + margin-right: 27%; +} + +#app-sidebar.disappear { + visibility: hidden; +} /* APP-SETTINGS ---------------------------------------------------------------*/ @@ -556,3 +588,50 @@ em { padding:16px; } +/* generic tab styles */ +.tabHeaders { + margin: 15px; + background-color: #1D2D44; +} + +.tabHeaders .tabHeader { + float: left; + border: 1px solid #ddd; + padding: 5px; + cursor: pointer; + background-color: #f8f8f8; + font-weight: bold; +} +.tabHeaders .tabHeader, .tabHeaders .tabHeader a { + color: #888; +} + +.tabHeaders .tabHeader:first-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.tabHeaders .tabHeader:last-child { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.tabHeaders .tabHeader.selected, +.tabHeaders .tabHeader:hover { + background-color: #e8e8e8; +} + +.tabHeaders .tabHeader.selected, +.tabHeaders .tabHeader.selected a, +.tabHeaders .tabHeader:hover, +.tabHeaders .tabHeader:hover a { + color: #000; +} + +.tabsContainer { + clear: left; +} + +.tabsContainer .tab { + padding: 15px; +} diff --git a/core/css/mobile.css b/core/css/mobile.css index 80217d7069c..2256d821d73 100644 --- a/core/css/mobile.css +++ b/core/css/mobile.css @@ -103,6 +103,10 @@ z-index: 1000; } +#app-sidebar{ + width: 100%; +} + /* allow horizontal scrollbar in settings otherwise user management is not usable on mobile */ #body-settings #app-content { diff --git a/core/js/apps.js b/core/js/apps.js index 71170bbc23a..d0d351f5147 100644 --- a/core/js/apps.js +++ b/core/js/apps.js @@ -21,6 +21,26 @@ }; /** + * Shows the #app-sidebar and add .with-app-sidebar to subsequent siblings + */ + exports.Apps.showAppSidebar = function() { + var $appSidebar = $('#app-sidebar'); + $appSidebar.removeClass('disappear') + $('#app-content').addClass('with-app-sidebar'); + + }; + + /** + * Shows the #app-sidebar and removes .with-app-sidebar from subsequent + * siblings + */ + exports.Apps.hideAppSidebar = function() { + var $appSidebar = $('#app-sidebar'); + $appSidebar.addClass('disappear'); + $('#app-content').removeClass('with-app-sidebar'); + }; + + /** * Provides a way to slide down a target area through a button and slide it * up if the user clicks somewhere else. Used for the news app settings and * add new field. diff --git a/core/js/core.json b/core/js/core.json index 0f052b798a9..1053debaa99 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -20,6 +20,7 @@ "oc-dialogs.js", "js.js", "l10n.js", + "apps.js", "share.js", "octemplate.js", "eventsource.js", diff --git a/core/js/js.js b/core/js/js.js index 45c9c90362f..72d4edd28dd 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1366,13 +1366,13 @@ function initCore() { // if there is a scrollbar … if($('#app-content').get(0).scrollHeight > $('#app-content').height()) { if($(window).width() > 768) { - controlsWidth = $('#content').width() - $('#app-navigation').width() - getScrollBarWidth(); + controlsWidth = $('#content').width() - $('#app-navigation').width() - $('#app-sidebar').width() - getScrollBarWidth(); } else { controlsWidth = $('#content').width() - getScrollBarWidth(); } } else { // if there is none if($(window).width() > 768) { - controlsWidth = $('#content').width() - $('#app-navigation').width(); + controlsWidth = $('#content').width() - $('#app-navigation').width() - $('#app-sidebar').width(); } else { controlsWidth = $('#content').width(); } diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js index 29293e89bcb..dbe005ba2e9 100644 --- a/core/js/tests/specHelper.js +++ b/core/js/tests/specHelper.js @@ -121,6 +121,8 @@ window.isPhantom = /phantom/i.test(navigator.userAgent); OC.TestUtil = TestUtil; } + moment.locale('en'); + // reset plugins OC.Plugins._plugins = []; diff --git a/lib/private/installer.php b/lib/private/installer.php index 37af8d0edcb..392dc1c0817 100644 --- a/lib/private/installer.php +++ b/lib/private/installer.php @@ -107,6 +107,10 @@ class OC_Installer{ } $extractDir .= '/' . $info['id']; + if(!file_exists($extractDir)) { + OC_Helper::rmdirr($basedir); + throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id'])); + } OC_Helper::copyr($extractDir, $basedir); //remove temporary files |