summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/files/css/detailsView.css55
-rw-r--r--apps/files/css/files.css5
-rw-r--r--apps/files/index.php6
-rw-r--r--apps/files/js/detailfileinfoview.js96
-rw-r--r--apps/files/js/detailsview.js251
-rw-r--r--apps/files/js/detailtabview.js136
-rw-r--r--apps/files/js/filelist.js127
-rw-r--r--apps/files/js/mainfileinfodetailview.js98
-rw-r--r--apps/files/tests/js/detailsviewSpec.js105
-rw-r--r--apps/files/tests/js/favoritespluginspec.js2
-rw-r--r--apps/files/tests/js/filelistSpec.js46
-rw-r--r--apps/files/tests/js/mainfileinfodetailviewSpec.js104
-rw-r--r--apps/files_sharing/appinfo/app.php1
-rw-r--r--apps/files_sharing/css/sharetabview.css3
-rw-r--r--apps/files_sharing/js/public.js3
-rw-r--r--apps/files_sharing/js/share.js4
-rw-r--r--apps/files_sharing/js/sharetabview.js67
-rw-r--r--apps/files_sharing/tests/js/appSpec.js2
-rw-r--r--core/css/apps.css79
-rw-r--r--core/css/mobile.css4
-rw-r--r--core/js/apps.js20
-rw-r--r--core/js/core.json1
-rw-r--r--core/js/js.js4
-rw-r--r--core/js/tests/specHelper.js2
-rw-r--r--lib/private/installer.php4
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