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', - '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', - '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 = ''; + /** * 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 += '' + errorMessage + '
'; + uploadAllowed = false; + } else { + 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 = + '