diff --git a/controller/pagecontroller.php b/controller/pagecontroller.php index 4cae46125b..186f31796e 100644 --- a/controller/pagecontroller.php +++ b/controller/pagecontroller.php @@ -82,7 +82,10 @@ public function index() { $appName = $this->appName; // Parameters sent to the template - $params = ['appName' => $appName]; + $params = [ + 'appName' => $appName, + 'uploadUrl' => $this->urlGenerator->linkTo('files', 'ajax/upload.php') + ]; // Will render the page using the template found in templates/index.php $response = new TemplateResponse($appName, 'index', $params); diff --git a/css/mobile.css b/css/mobile.css index 44e1ba9627..fbb960f0ac 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -5,4 +5,9 @@ margin-right: 0 !important; } + /* shorten elements for mobile */ + #uploadprogressbar { + width: 50px; + } + } diff --git a/css/upload.css b/css/upload.css new file mode 100644 index 0000000000..72c6b13642 --- /dev/null +++ b/css/upload.css @@ -0,0 +1,95 @@ +/* Uploading */ +.oc-dialog .fileexists .icon { + background-position: center center; + background-size: cover !important; +} + +#uploadprogressbar { + margin-top: 5px; +} + +.stop.icon-close { + margin-top: 5px; +} + +.actions { + float: left; +} + +.actions input, .actions button, .actions .button { + margin: 0; + float: left; +} + +.actions .button a { + color: #555; +} + +.actions .button a:hover, +.actions .button a:focus, +.actions .button a:active { + color: #333; +} + +.actions.creatable { + position: relative; + z-index: -30; +} + +.newFileMenu { + width: 140px; + margin-left: -56px; + margin-top: 25px; + z-index: 1001; +} + +.newFileMenu .error { + color: #e9322d; + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.newFileMenu .menuitem { + white-space: nowrap; + overflow: hidden; +} +.newFileMenu.popovermenu .menuitem .icon { + margin-bottom: -2px; +} +.newFileMenu.popovermenu a.menuitem, +.newFileMenu.popovermenu label.menuitem, +.newFileMenu.popovermenu .menuitem { + padding: 0; + margin: 0; +} + +.newFileMenu.popovermenu a.menuitem.active { + opacity: 1; +} + +.newFileMenu.bubble:after { + left: 75px; + right: auto; +} +.newFileMenu.bubble:before { + left: 75px; + right: auto; +} + +.newFileMenu .filenameform { + display: inline-block; +} + +.newFileMenu .filenameform input { + width: 100px; +} + +#fileList .popovermenu .action { + display: block; + line-height: 30px; + padding-left: 5px; + color: #000; + padding: 0; +} \ No newline at end of file diff --git a/js/app.js b/js/app.js index 84e7964b2c..b0f00cd201 100644 --- a/js/app.js +++ b/js/app.js @@ -2,7 +2,6 @@ $(document).ready(function () { "use strict"; $('#controls').insertBefore($('#content-wrapper')); - Gallery.hideSearch(); Gallery.utility = new Gallery.Utility(); Gallery.view = new Gallery.View(); Gallery.token = Gallery.utility.getPublicToken(); @@ -11,14 +10,14 @@ $(document).ready(function () { // The first thing to do is to detect if we're on IE if (Gallery.ieVersion === 'unsupportedIe') { Gallery.utility.showIeWarning(Gallery.ieVersion); - Gallery.showEmpty(); + Gallery.view.showEmptyFolder('', null); } else { if (Gallery.ieVersion === 'oldIe') { Gallery.utility.showIeWarning(Gallery.ieVersion); } // Get the config, the files and initialise the slideshow - Gallery.showLoading(); + Gallery.view.showLoading(); $.getJSON(Gallery.utility.buildGalleryUrl('config', '', {})) .then(function (config) { Gallery.config = new Gallery.Config(config); @@ -26,11 +25,11 @@ $(document).ready(function () { Gallery.getFiles(currentLocation).then(function () { Gallery.activeSlideShow = new SlideShow(); $.when( - Gallery.activeSlideShow.init( - false, - null, - Gallery.config.galleryFeatures - )) + Gallery.activeSlideShow.init( + false, + null, + Gallery.config.galleryFeatures + )) .then(function () { window.onhashchange(); }); @@ -59,7 +58,7 @@ $(document).ready(function () { Gallery.view.viewAlbum(Gallery.currentAlbum); infoContentContainer.css('max-width', $(window).width()); } - if(Gallery.currentAlbum) { + if (Gallery.currentAlbum) { Gallery.view.breadcrumb.setMaxWidth($(window).width() - Gallery.buttonsWidth); } diff --git a/js/gallery.js b/js/gallery.js index 56d073c26d..fc9b86a8ac 100644 --- a/js/gallery.js +++ b/js/gallery.js @@ -3,6 +3,7 @@ "use strict"; var Gallery = { currentAlbum: null, + currentEtag: null, config: {}, /** Map of the whole gallery, built as we navigate through folders */ albumMap: {}, @@ -22,7 +23,7 @@ */ refresh: function (path, albumPath) { if (Gallery.currentAlbum !== albumPath) { - Gallery.view.init(albumPath); + Gallery.view.init(albumPath, null); } // If the path is mapped, that means that it's an albumPath @@ -89,11 +90,9 @@ Gallery.config.updateAlbumSorting(Gallery.albumMap[albumInfo.path].sorting); } - }, function () { - // Triggered if we couldn't find a working folder - Gallery.view.element.empty(); - Gallery.showEmpty(); - Gallery.currentAlbum = null; + }, function (xhr) { + var result = xhr.responseJSON; + Gallery.view.init(decodeURIComponent(currentLocation), result.message); }); }, @@ -130,8 +129,9 @@ // Sort the images Gallery.albumMap[Gallery.currentAlbum].images.sort(Gallery.utility.sortBy(sortType, sortOrder)); - Gallery.albumMap[Gallery.currentAlbum].subAlbums.sort(Gallery.utility.sortBy(albumSortType, - albumSortOrder)); + Gallery.albumMap[Gallery.currentAlbum].subAlbums.sort( + Gallery.utility.sortBy(albumSortType, + albumSortOrder)); // Save the new settings var sortConfig = { @@ -182,8 +182,7 @@ var albumPermissions = Gallery.config.albumPermissions; $('a.share').data('path', albumPermissions.path).data('link', true) - .data('possible-permissions', albumPermissions.permissions). - click(); + .data('possible-permissions', albumPermissions.permissions).click(); if (!$('#linkCheckbox').is(':checked')) { $('#linkText').hide(); } @@ -240,58 +239,6 @@ Gallery._saveToOwnCloud(remote, Gallery.token, owner, name, isProtected); }, - /** - * Hide the search button while we wait for core to fix the templates - */ - hideSearch: function () { - $('form.searchbox').hide(); - }, - - /** - * Shows an empty gallery message - */ - showEmpty: function () { - var emptyContentElement = $('#emptycontent'); - var message = ''; - message += '

' + t('gallery', - 'No pictures found') + '

'; - message += '

' + t('gallery', - 'Upload pictures in the files app to display them here') + '

'; - emptyContentElement.html(message); - emptyContentElement.removeClass('hidden'); - $('#controls').addClass('hidden'); - }, - - /** - * Shows an empty gallery message - */ - showEmptyFolder: function () { - var emptyContentElement = $('#emptycontent'); - var message = ''; - message += '

' + t('gallery', - 'Nothing in here') + '

'; - message += '

' + t('gallery', - 'No media files found in this folder') + '

'; - emptyContentElement.html(message); - emptyContentElement.removeClass('hidden'); - }, - - /** - * Shows the infamous loading spinner - */ - showLoading: function () { - $('#emptycontent').addClass('hidden'); - $('#controls').removeClass('hidden'); - }, - - /** - * Shows thumbnails - */ - showNormal: function () { - $('#emptycontent').addClass('hidden'); - $('#controls').removeClass('hidden'); - }, - /** * Creates a new slideshow using the images found in the current folder * @@ -327,7 +274,8 @@ c: image.etag, requesttoken: oc_requesttoken }; - var downloadUrl = Gallery.utility.buildGalleryUrl('files', '/download/' + image.fileId, + var downloadUrl = Gallery.utility.buildGalleryUrl('files', + '/download/' + image.fileId, params); return { @@ -379,6 +327,8 @@ var mimeType = null; var mTime = null; var etag = null; + var size = null; + var sharedWithUser = null; var albumInfo = data.albuminfo; var currentLocation = albumInfo.path; // This adds a new node to the map for each parent album @@ -394,8 +344,13 @@ mimeType = files[i].mimetype; mTime = files[i].mtime; etag = files[i].etag; + size = files[i].size; + sharedWithUser = files[i].sharedWithUser; - image = new GalleryImage(path, path, fileId, mimeType, mTime, etag); + image = + new GalleryImage( + path, path, fileId, mimeType, mTime, etag, size, sharedWithUser + ); // Determines the folder name for the image var dir = OC.dirname(path); @@ -505,15 +460,15 @@ // directive $.get(OC.generateUrl('apps/files_sharing/testremote'), {remote: remote}).then(function (protocol) { - if (protocol !== 'http' && protocol !== 'https') { - OC.dialogs.alert(t('files_sharing', - 'No ownCloud installation (7 or higher) found at {remote}', - {remote: remote}), - t('files_sharing', 'Invalid ownCloud url')); - } else { - OC.redirect(protocol + '://' + url); - } - }); + if (protocol !== 'http' && protocol !== 'https') { + OC.dialogs.alert(t('files_sharing', + 'No ownCloud installation (7 or higher) found at {remote}', + {remote: remote}), + t('files_sharing', 'Invalid ownCloud url')); + } else { + OC.redirect(protocol + '://' + url); + } + }); } } }; diff --git a/js/galleryimage.js b/js/galleryimage.js index fa2ab7a6b8..63ac3b85b0 100644 --- a/js/galleryimage.js +++ b/js/galleryimage.js @@ -14,21 +14,25 @@ /** * Creates a new image object to store information about a media file * - * @param src - * @param path - * @param fileId - * @param mimeType - * @param mTime modification time - * @param etag + * @param {string} src + * @param {string} path + * @param {number} fileId + * @param {string} mimeType + * @param {number} mTime modification time + * @param {string} etag + * @param {number} size + * @param {boolean} sharedWithUser * @constructor */ - var GalleryImage = function (src, path, fileId, mimeType, mTime, etag) { + var GalleryImage = function (src, path, fileId, mimeType, mTime, etag, size, sharedWithUser) { this.src = src; this.path = path; this.fileId = fileId; this.mimeType = mimeType; this.mTime = mTime; this.etag = etag; + this.size = size; + this.sharedWithUser = sharedWithUser; this.thumbnail = null; this.domDef = null; this.spinner = null; diff --git a/js/galleryview.js b/js/galleryview.js index 339d5b444c..b69d05f6aa 100644 --- a/js/galleryview.js +++ b/js/galleryview.js @@ -1,6 +1,9 @@ -/* global Gallery */ +/* global Handlebars, Gallery */ (function ($, _, OC, t, Gallery) { "use strict"; + + var TEMPLATE_ADDBUTTON = '{{addText}}'; + /** * Builds and updates the Gallery view * @@ -9,13 +12,18 @@ var View = function () { this.element = $('#gallery'); this.loadVisibleRows.loading = false; + this._setupUploader(); this.breadcrumb = new Gallery.Breadcrumb(); + this.emptyContentElement = $('#emptycontent'); + this.controlsElement = $('#controls'); }; View.prototype = { element: null, breadcrumb: null, requestId: -1, + emptyContentElement: null, + controlsElement: null, /** * Removes all thumbnails from the view @@ -23,32 +31,23 @@ clear: function () { // We want to keep all the events this.element.children().detach(); - Gallery.showLoading(); + this.showLoading(); }, /** * Populates the view if there are images or albums to show * * @param {string} albumPath + * @param {string|undefined} errorMessage */ - init: function (albumPath) { + init: function (albumPath, errorMessage) { // Only do it when the app is initialised if (this.requestId === -1) { this._initButtons(); this._blankUrl(); } if ($.isEmptyObject(Gallery.imageMap)) { - this.clear(); - if (albumPath === '') { - Gallery.showEmpty(); - } else { - Gallery.showEmptyFolder(); - this.hideButtons(); - Gallery.currentAlbum = albumPath; - var availableWidth = $(window).width() - Gallery.buttonsWidth; - this.breadcrumb.init(albumPath, availableWidth); - Gallery.config.albumDesign = null; - } + Gallery.view.showEmptyFolder(albumPath, errorMessage); } else { this.viewAlbum(albumPath); } @@ -82,9 +81,10 @@ this.clear(); - if (albumPath !== Gallery.currentAlbum) { + if (Gallery.albumMap[albumPath].etag !== Gallery.currentEtag) { this.loadVisibleRows.loading = false; Gallery.currentAlbum = albumPath; + Gallery.currentEtag = Gallery.albumMap[albumPath].etag; this._setupButtons(albumPath); } @@ -170,7 +170,7 @@ // meantime } if (view.element.length === 1) { - Gallery.showNormal(); + view._showNormal(); } if (album.viewedItems < album.subAlbums.length + album.images.length && view.element.height() < targetHeight) { @@ -194,11 +194,101 @@ } }, - hideButtons: function () { - $('#album-info-button').hide(); - $('#share-button').hide(); - $('#sort-name-button').hide(); - $('#sort-date-button').hide(); + /** + * Shows an empty gallery message + * + * @param {string} albumPath + * @param {string|null} errorMessage + */ + showEmptyFolder: function (albumPath, errorMessage) { + var message = ''; + var uploadAllowed = true; + + this.clear(); + + if (!_.isUndefined(errorMessage) && errorMessage !== null) { + message += '

' + t('gallery', + 'Album cannot be shown') + '

'; + message += '

' + errorMessage + '

'; + uploadAllowed = false; + } else { + message += '

' + t('gallery', + 'No media files found') + '

'; + // We can't upload yet on the public side + if (Gallery.token) { + message += '

' + t('gallery', + 'Upload pictures in the files app to display them here') + '

'; + } else { + message += '

' + t('gallery', + 'Upload new files via drag and drop or by using the [+] button above') + + '

'; + } + } + this.emptyContentElement.html(message); + this.emptyContentElement.removeClass('hidden'); + + //Gallery.view.showEmptyFolder(); + this._hideButtons(uploadAllowed); + Gallery.currentAlbum = albumPath; + var availableWidth = $(window).width() - Gallery.buttonsWidth; + this.breadcrumb.init(albumPath, availableWidth); + Gallery.config.albumDesign = null; + }, + + /** + * Shows the infamous loading spinner + */ + showLoading: function () { + this.emptyContentElement.addClass('hidden'); + this.controlsElement.removeClass('hidden'); + }, + + /** + * Shows thumbnails + */ + _showNormal: function () { + this.emptyContentElement.addClass('hidden'); + this.controlsElement.removeClass('hidden'); + }, + + /** + * Sets up our custom handlers for folder uploading operations + * + * We only want it to be called for that specific case as all other file uploading + * operations will call Files.highlightFiles + * + * @see OC.Upload.init/file_upload_param.done() + * + * @private + */ + _setupUploader: function () { + $('#file_upload_start').on('fileuploaddone', function (e, data) { + if (data.files[0] === data.originalFiles[data.originalFiles.length - 1] + && data.files[0].relativePath) { + + //Ask for a refresh of the photowall + Gallery.getFiles(Gallery.currentAlbum).done(function () { + Gallery.view.init(Gallery.currentAlbum); + }); + } + }); + + // Since 9.0 + if (OC.Upload) { + OC.Upload._isReceivedSharedFile = function (file) { + var path = file.name; + var sharedWith = false; + + if (Gallery.currentAlbum !== '' && Gallery.currentAlbum !== '/') { + path = Gallery.currentAlbum + '/' + path; + } + if (Gallery.imageMap[path] && Gallery.imageMap[path].sharedWithUser) { + sharedWith = true; + } + + return sharedWith; + }; + } }, /** @@ -216,6 +306,7 @@ $('#sort-date-button').click(Gallery.sorter); $('#save #save-button').click(Gallery.showSaveForm); $('.save-form').submit(Gallery.saveForm); + this._renderNewButton(); this.requestId = Math.random(); }, @@ -253,6 +344,27 @@ currentSort.order)); Gallery.albumMap[Gallery.currentAlbum].subAlbums.sort(Gallery.utility.sortBy('name', currentSort.albumOrder)); + + $('#save-button').show(); + $('#download').show(); + }, + + /** + * Hide buttons in the controls bar + * + * @param uploadAllowed + */ + _hideButtons: function (uploadAllowed) { + $('#album-info-button').hide(); + $('#share-button').hide(); + $('#sort-name-button').hide(); + $('#sort-date-button').hide(); + $('#save-button').hide(); + $('#download').hide(); + + if (!uploadAllowed) { + $('a.button.new').hide(); + } }, /** @@ -366,6 +478,60 @@ $('#save-button-confirm').prop('disabled', false); } }); + }, + + /** + * Creates the [+] button allowing users who can't drag and drop to upload files + * + * @see core/apps/files/js/filelist.js + * @private + */ + _renderNewButton: function () { + // if no actions container exist, skip + var $actionsContainer = $('.actions'); + if (!$actionsContainer.length) { + return; + } + if (!this._addButtonTemplate) { + this._addButtonTemplate = Handlebars.compile(TEMPLATE_ADDBUTTON); + } + var $newButton = $(this._addButtonTemplate({ + addText: t('gallery', 'New'), + iconUrl: OC.imagePath('core', 'actions/add') + })); + + $actionsContainer.prepend($newButton); + $newButton.tooltip({'placement': 'bottom'}); + + $newButton.click(_.bind(this._onClickNewButton, this)); + this._newButton = $newButton; + }, + + /** + * Creates the click handler for the [+] button + * @param event + * @returns {boolean} + * + * @see core/apps/files/js/filelist.js + * @private + */ + _onClickNewButton: function (event) { + var $target = $(event.target); + if (!$target.hasClass('.button')) { + $target = $target.closest('.button'); + } + this._newButton.tooltip('hide'); + event.preventDefault(); + if ($target.hasClass('disabled')) { + return false; + } + if (!this._newFileMenu) { + this._newFileMenu = new Gallery.NewFileMenu(); + $('body').append(this._newFileMenu.$el); + } + this._newFileMenu.showAt($target); + + return false; } }; diff --git a/js/thumbnail.js b/js/thumbnail.js index f40dffdf75..03da6ff353 100644 --- a/js/thumbnail.js +++ b/js/thumbnail.js @@ -90,7 +90,7 @@ function Thumbnail (fileId, square) { */ loadBatch: function (ids, square) { var map = (square) ? Thumbnails.squareMap : Thumbnails.map; - // Purely here as a precaution + // Prevents re-loading thumbnails when resizing the window ids = ids.filter(function (id) { return !map[id]; }); diff --git a/js/upload-helper.js b/js/upload-helper.js new file mode 100644 index 0000000000..d7ecf789dc --- /dev/null +++ b/js/upload-helper.js @@ -0,0 +1,144 @@ +/* global Gallery, Thumbnails */ +/** + * OCA.FileList methods needed for file uploading + * + * This hack makes it possible to use the Files scripts as is, without having to import and + * maintain them in Gallery + * + * Empty methods are for the "new" button, if we want to implement that one day + * + * @type {{inList: FileList.inList, lastAction: FileList.lastAction, getUniqueName: + * FileList.getUniqueName, getCurrentDirectory: FileList.getCurrentDirectory, add: + * FileList.add, checkName: FileList.checkName}} + */ +var FileList = { + /** + * Makes sure the filename does not exist + * + * Gives an early chance to the user to abort the action, before uploading everything to the + * server. + * Albums are not supported as we don't have a full list of images contained in a sub-album + * + * @param fileName + * @returns {*} + */ + findFile: function (fileName) { + "use strict"; + var path = Gallery.currentAlbum + '/' + fileName; + var galleryImage = Gallery.imageMap[path]; + if (galleryImage) { + var fileInfo = { + name: fileName, + directory: Gallery.currentAlbum, + path: path, + etag: galleryImage.etag, + mtime: galleryImage.mTime * 1000, // Javascript gives the Epoch time in milliseconds + size: galleryImage.size + }; + return fileInfo; + } else { + return null; + } + }, + + /** + * Refreshes the photowall + * + * Called at the end of the uploading process when 1 or multiple files are sent + * Never called with folders on Chrome, unless files are uploaded at the same time as folders + * + * @param fileList + */ + highlightFiles: function (fileList) { + "use strict"; + //Ask for a refresh of the photowall + Gallery.getFiles(Gallery.currentAlbum).done(function () { + var fileId, path; + // Removes the cached thumbnails of files which have been re-uploaded + _(fileList).each(function (fileName) { + path = Gallery.currentAlbum + '/' + fileName; + if (Gallery.imageMap[path]) { + fileId = Gallery.imageMap[path].fileId; + if (Thumbnails.map[fileId]) { + delete Thumbnails.map[fileId]; + } + } + }); + + Gallery.view.init(Gallery.currentAlbum); + }); + }, + + /** + * Retrieves the current album + * + * @returns {string} + */ + getCurrentDirectory: function () { + "use strict"; + + // In Files, dirs start with a / + return '/' + Gallery.currentAlbum; + } +}; + +/** + * OCA.Files methods needed for file uploading + * + * This hack makes it possible to use the Files scripts as is, without having to import and + * maintain them in Gallery + * + * @type {{isFileNameValid: Files.isFileNameValid, generatePreviewUrl: Files.generatePreviewUrl}} + */ +var Files = { + App: {fileList: {}}, + + isFileNameValid: function (name) { + "use strict"; + var trimmedName = name.trim(); + if (trimmedName === '.' || trimmedName === '..') { + throw t('files', '"{name}" is an invalid file name.', {name: name}); + } else if (trimmedName.length === 0) { + throw t('files', 'File name cannot be empty.'); + } + return true; + + }, + + /** + * Generates a preview for the conflict dialogue + * + * Since Gallery uses the fileId and Files uses the path, we have to use the preview endpoint + * of Files + */ + generatePreviewUrl: function (urlSpec) { + "use strict"; + var previewUrl; + var path = urlSpec.file; + + // In Files, root files start with // + if (path.indexOf('//') === 0) { + path = path.substring(2); + } else { + // Directories start with / + path = path.substring(1); + } + + if (Gallery.imageMap[path]) { + var fileId = Gallery.imageMap[path].fileId; + var thumbnail = Thumbnails.map[fileId]; + previewUrl = thumbnail.image.src; + } else { + var previewDimension = 96; + urlSpec.x = Math.ceil(previewDimension * window.devicePixelRatio); + urlSpec.y = Math.ceil(previewDimension * window.devicePixelRatio); + urlSpec.forceIcon = 0; + previewUrl = OC.generateUrl('/core/preview.png?') + $.param(urlSpec); + } + + return previewUrl; + } +}; + +OCA.Files = Files; +OCA.Files.App.fileList = FileList; diff --git a/js/vendor/owncloud/newfilemenu.js b/js/vendor/owncloud/newfilemenu.js new file mode 100644 index 0000000000..a3dbd748f5 --- /dev/null +++ b/js/vendor/owncloud/newfilemenu.js @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2014-2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING file. + * + */ + +/* global Handlebars, Gallery */ +(function ($, Gallery) { + "use strict"; + var TEMPLATE_MENU = + ''; + + /** + * Construct a new NewFileMenu instance + * @constructs NewFileMenu + * + * @memberof Gallery + */ + var NewFileMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'newFileMenu popovermenu bubble hidden open menu', + + events: { + 'click .menuitem': '_onClickAction' + }, + + initialize: function () { + var self = this; + var $uploadEl = $('#file_upload_start'); + if ($uploadEl.length) { + $uploadEl.on('fileuploadstart', function () { + self.trigger('actionPerformed', 'upload'); + }); + } else { + console.warn('Missing upload element "file_upload_start"'); + } + }, + + template: function (data) { + if (!Gallery.NewFileMenu._TEMPLATE) { + Gallery.NewFileMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU); + } + return Gallery.NewFileMenu._TEMPLATE(data); + }, + + /** + * Event handler whenever the upload button has been clicked within the menu + */ + _onClickAction: function () { + // note: clicking the upload label will automatically + // set the focus on the "file_upload_start" hidden field + // which itself triggers the upload dialog. + // Currently the upload logic is still in file-upload.js and filelist.js + OC.hideMenus(); + }, + + /** + * Renders the menu with the currently set items + */ + render: function () { + this.$el.html(this.template({ + uploadMaxHumanFileSize: 'TODO', + uploadLabel: t('gallery', 'Upload') + })); + }, + + /** + * Displays the menu under the given element + * + * @param {Object} $target target element + */ + showAt: function ($target) { + this.render(); + var targetOffset = $target.offset(); + this.$el.css({ + left: targetOffset.left, + top: targetOffset.top + $target.height() + }); + this.$el.removeClass('hidden'); + + OC.showMenu(null, this.$el); + } + }); + + Gallery.NewFileMenu = NewFileMenu; +})(jQuery, Gallery); diff --git a/service/configservice.php b/service/configservice.php index 0ee75200d3..f308a19db1 100644 --- a/service/configservice.php +++ b/service/configservice.php @@ -169,13 +169,16 @@ public function getAlbumInfo($folderNode, $folderPathFromRoot, $features) { list ($albumConfig, $privateAlbum) = $this->getAlbumConfig($folderNode, $this->privacyChecker, $this->configName); if ($privateAlbum) { - throw new ForbiddenServiceException('Album is private or unavailable'); + throw new ForbiddenServiceException( + 'The owner has placed a restriction or the storage location is unavailable' + ); } $albumInfo = [ - 'path' => $folderPathFromRoot, - 'fileid' => $folderNode->getID(), - 'permissions' => $folderNode->getPermissions(), - 'etag' => $folderNode->getEtag() + 'path' => $folderPathFromRoot, + 'fileid' => $folderNode->getId(), + 'permissions' => $folderNode->getPermissions(), + 'etag' => $folderNode->getEtag(), + 'sharedWithUser' => $folderNode->isShared() ]; // There is always an albumInfo, but the albumConfig may be empty $albumConfig = array_merge($albumInfo, $albumConfig); diff --git a/service/searchfolderservice.php b/service/searchfolderservice.php index 6612528862..33b223011b 100644 --- a/service/searchfolderservice.php +++ b/service/searchfolderservice.php @@ -30,7 +30,7 @@ class SearchFolderService extends FilesService { * @var int */ protected $virtualRootLevel = null; - + /** * This returns what we think is the current folder node based on a given path * @@ -126,7 +126,9 @@ private function sendFolder($path, $node, $locationHasChanged) { // Something very wrong has just happened throw new NotFoundServiceException('Oh Nooooes!'); } elseif (!$this->isAllowedAndAvailable($node)) { - throw new ForbiddenServiceException('Album is private or unavailable'); + throw new ForbiddenServiceException( + 'The owner has placed a restriction or the storage location is unavailable' + ); } return [$path, $node, $locationHasChanged]; diff --git a/service/searchmediaservice.php b/service/searchmediaservice.php index 974aa53f3b..c7e82f73ae 100644 --- a/service/searchmediaservice.php +++ b/service/searchmediaservice.php @@ -199,16 +199,20 @@ private function isPreviewAvailable($file) { private function addFileToResults($file) { $imagePath = $this->environment->getPathFromVirtualRoot($file); $imageId = $file->getId(); - $mimeType = $file->getMimetype(); + $mimeType = $file->getMimeType(); $mTime = $file->getMTime(); $etag = $file->getEtag(); + $size = $file->getSize(); + $sharedWithUser = $file->isShared(); $imageData = [ - 'path' => $imagePath, - 'fileid' => $imageId, - 'mimetype' => $mimeType, - 'mtime' => $mTime, - 'etag' => $etag + 'path' => $imagePath, + 'fileid' => $imageId, + 'mimetype' => $mimeType, + 'mtime' => $mTime, + 'etag' => $etag, + 'size' => $size, + 'sharedWithUser' => $sharedWithUser ]; $this->images[] = $imageData; diff --git a/templates/part.content.php b/templates/part.content.php index 1e1ecad4e3..d41f3ac8a1 100644 --- a/templates/part.content.php +++ b/templates/part.content.php @@ -27,7 +27,24 @@ 'vendor/bigshot/bigshot-compressed', 'slideshow', 'slideshowcontrols', - 'slideshowzoomablepreview' + 'slideshowzoomablepreview', + 'upload-helper', + 'vendor/owncloud/newfilemenu' + ] +); +script( + 'files', + [ + 'upload', + 'file-upload', + 'jquery.fileupload', + 'jquery.iframe-transport' + ] +); +style( + 'files', + [ + 'upload' ] ); style( @@ -35,10 +52,11 @@ [ 'styles', 'share', - 'mobile', 'github-markdown', 'slideshow', - 'gallerybutton' + 'gallerybutton', + 'upload', + 'mobile', ] ); ?> @@ -67,6 +85,17 @@ +
+
+
+ +
+
+
@@ -100,3 +129,7 @@ +
+ +
diff --git a/tests/api/GetFilesCest.php b/tests/api/GetFilesCest.php index 45afd3d850..d69282b2b3 100644 --- a/tests/api/GetFilesCest.php +++ b/tests/api/GetFilesCest.php @@ -142,7 +142,10 @@ public function getListOfForbiddenPath(\Step\Api\User $I) { $I->seeResponseCodeIs($statusCode); $I->seeResponseIsJson(); $I->seeResponseContainsJson( - ['message' => 'Album is private or unavailable (' . $statusCode . ')'] + [ + 'message' => 'The owner has placed a restriction or the storage location is unavailable (' + . $statusCode . ')' + ] ); } diff --git a/tests/unit/GalleryUnitTest.php b/tests/unit/GalleryUnitTest.php index 44b759139e..14e27f04b6 100644 --- a/tests/unit/GalleryUnitTest.php +++ b/tests/unit/GalleryUnitTest.php @@ -90,10 +90,15 @@ protected function mockGetResourceFromIdWithBadFile($mockedObject, $fileId, $exc * @param string $storageId * @param bool $isReadable * @param string $path + * @param string $etag + * @param int $size + * @param bool $isShared * * @return \PHPUnit_Framework_MockObject_MockObject */ - protected function mockFile($fileId, $storageId = 'home::user', $isReadable = true, $path = '' + protected function mockFile( + $fileId, $storageId = 'home::user', $isReadable = true, $path = '', + $etag = "8603c11cd6c5d739f2c156c38b8db8c4", $size = 1024, $isShared = false ) { $storage = $this->mockGetStorage($storageId); $file = $this->getMockBuilder('OCP\Files\File') @@ -109,12 +114,21 @@ protected function mockFile($fileId, $storageId = 'home::user', $isReadable = tr ->willReturn($isReadable); $file->method('getPath') ->willReturn($path); + $file->method('getEtag') + ->willReturn($etag); + $file->method('getSize') + ->willReturn($size); + $file->method('isShared') + ->willReturn($isShared); return $file; } - protected function mockJpgFile($fileId) { - $file = $this->mockFile($fileId); + protected function mockJpgFile( + $fileId, $storageId = 'home::user', $isReadable = true, $path = '', + $etag = "8603c11cd6c5d739f2c156c38b8db8c4", $size = 1024, $isShared = false + ) { + $file = $this->mockFile($fileId, $storageId, $isReadable, $path, $etag, $size, $isShared); $this->mockJpgFileMethods($file); return $file; diff --git a/tests/unit/controller/FilesControllerTest.php b/tests/unit/controller/FilesControllerTest.php index 9076427a39..8b7ed65035 100644 --- a/tests/unit/controller/FilesControllerTest.php +++ b/tests/unit/controller/FilesControllerTest.php @@ -169,6 +169,7 @@ public function testGetFilesWithWorkingSetup() { $folderId = 9876; $folderPermissions = 31; $folderEtag = 9999888877776666; + $folderIsShared = false; $files = [ ['path' => $folderPathFromRoot . '/deep/path.png'], ['path' => $folderPathFromRoot . '/testimage.png'] @@ -177,7 +178,8 @@ public function testGetFilesWithWorkingSetup() { 'path' => $folderPathFromRoot, 'fileid' => $folderId, 'permissions' => $folderPermissions, - 'etag' => $folderEtag + 'etag' => $folderEtag, + 'shared' => $folderIsShared ]; $locationHasChanged = false; $result = [ @@ -185,7 +187,9 @@ public function testGetFilesWithWorkingSetup() { 'albuminfo' => $albumInfo, 'locationhaschanged' => $locationHasChanged ]; - $folder = $this->mockGetFolder($folderId, $files, $folderPermissions, $folderEtag); + $folder = $this->mockGetFolder( + $folderId, $files, $folderPermissions, $folderEtag, $folderIsShared + ); $this->mockGetCurrentFolder( $location, $folderPathFromRoot, [$features], $locationHasChanged, $folder @@ -351,7 +355,7 @@ private function mockGetCurrentFolder( ->willReturn($answer); } - private function mockGetFolder($nodeId, $files, $permissions, $etag) { + private function mockGetFolder($nodeId, $files, $permissions, $etag, $isShared) { $folder = $this->getMockBuilder('OCP\Files\Folder') ->disableOriginalConstructor() ->getMock(); @@ -365,6 +369,8 @@ private function mockGetFolder($nodeId, $files, $permissions, $etag) { ->willReturn($permissions); $folder->method('getEtag') ->willReturn($etag); + $folder->method('isShared') + ->willReturn($isShared); return $folder; } diff --git a/tests/unit/controller/PageControllerTest.php b/tests/unit/controller/PageControllerTest.php index 5eb0e6b0dc..d46120042c 100644 --- a/tests/unit/controller/PageControllerTest.php +++ b/tests/unit/controller/PageControllerTest.php @@ -69,7 +69,12 @@ protected function setUp() { public function testIndex() { - $params = ['appName' => $this->appName]; + $url = 'http://owncloud/ajax/upload.php'; + $this->mockUrlToUploadEndpoint($url); + $params = [ + 'appName' => $this->appName, + 'uploadUrl' => $url + ]; $template = new TemplateResponse($this->appName, 'index', $params); $response = $this->controller->index(); @@ -239,4 +244,10 @@ private function mockCookieGet($key, $value) { ->willReturn($value); } + private function mockUrlToUploadEndpoint($url) { + $this->urlGenerator->expects($this->once()) + ->method('linkTo') + ->with('files', 'ajax/upload.php') + ->willReturn($url); + } } diff --git a/tests/unit/service/SearchFolderServiceTest.php b/tests/unit/service/SearchFolderServiceTest.php index dc0a5b0674..6f942ff5e7 100644 --- a/tests/unit/service/SearchFolderServiceTest.php +++ b/tests/unit/service/SearchFolderServiceTest.php @@ -105,7 +105,9 @@ public function providesSendExternalFolderData() { */ public function testSendExternalFolder($storageId) { $expectedException = - new ForbiddenServiceException('Album is private or unavailable'); + new ForbiddenServiceException( + 'The owner has placed a restriction or the storage location is unavailable' + ); $path = ''; $nodeId = 94875; $files = []; diff --git a/tests/unit/service/SearchMediaServiceTest.php b/tests/unit/service/SearchMediaServiceTest.php index d22e3695aa..9a6b5ffbef 100644 --- a/tests/unit/service/SearchMediaServiceTest.php +++ b/tests/unit/service/SearchMediaServiceTest.php @@ -12,6 +12,8 @@ namespace OCA\Gallery\Service; +use OCP\Files\Folder; + /** * Class SearchMediaServiceTest * @@ -189,7 +191,7 @@ public function providesTopFolderData() { /** * @dataProvider providesTopFolderData * - * @param array $topFolder + * @param Folder $topFolder * @param int $result */ public function testGetMediaFiles($topFolder, $result) { @@ -205,6 +207,94 @@ public function testGetMediaFiles($topFolder, $result) { $this->assertSame($result, sizeof($response)); } + public function providesFolderWithFilesData() { + $isReadable = true; + $mounted = false; + $mount = null; + $query = '.nomedia'; + $queryResult = false; + + $file1 = [ + 'fileid' => 11111, + 'storageId' => 'home::user', + 'isReadable' => true, + 'path' => null, + 'etag' => "8603c11cd6c5d739f2c156c38b8db8c4", + 'size' => 1024, + 'sharedWithUser' => false, + 'mimetype' => 'image/jpeg' + ]; + + $file2 = [ + 'fileid' => 22222, + 'storageId' => 'webdav::user@domain.com/dav', + 'isReadable' => true, + 'path' => null, + 'etag' => "739f2c156c38b88603c11cd6c5ddb8c4", + 'size' => 102410241024, + 'sharedWithUser' => true, + 'mimetype' => 'image/jpeg' + ]; + + + $folder1 = $this->mockFolder( + 'home::user', 545454, [ + $this->mockJpgFile( + $file1['fileid'], $file1['storageId'], $file1['isReadable'], $file1['path'], + $file1['etag'], $file1['size'], $file1['sharedWithUser'] + ), + $this->mockJpgFile( + $file2['fileid'], $file2['storageId'], $file2['isReadable'], $file2['path'], + $file2['etag'], $file2['size'], $file2['sharedWithUser'] + ) + ], $isReadable, $mounted, $mount, $query, $queryResult + ); + + return [ + [ + $folder1, [ + [ + 'path' => $file1['path'], + 'fileid' => $file1['fileid'], + 'mimetype' => $file1['mimetype'], + 'mtime' => null, + 'etag' => $file1['etag'], + 'size' => $file1['size'], + 'sharedWithUser' => $file1['sharedWithUser'] + ], + [ + 'path' => $file2['path'], + 'fileid' => $file2['fileid'], + 'mimetype' => $file2['mimetype'], + 'mtime' => null, + 'etag' => $file2['etag'], + 'size' => $file2['size'], + 'sharedWithUser' => $file2['sharedWithUser'] + ] + ] + ] + ]; + } + + /** + * @dataProvider providesFolderWithFilesData + * + * @param Folder $topFolder + * @param array $result + */ + public function testPropertiesOfGetMediaFiles($topFolder, $result) { + $supportedMediaTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif' + ]; + $features = []; + + $response = $this->service->getMediaFiles($topFolder, $supportedMediaTypes, $features); + + $this->assertSame($result, $response); + } + /** * @expectedException \OCA\Gallery\Service\NotFoundServiceException */