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/gallery.js b/js/gallery.js index ff4f5af4b2..8788286c5d 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: {}, @@ -259,21 +260,6 @@ $('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 */ @@ -281,9 +267,9 @@ var emptyContentElement = $('#emptycontent'); var message = ''; message += '' + t('gallery', - 'No media files found in this folder') + '
'; + 'Upload new files via drag and drop or by using the [+] button above') + ''; emptyContentElement.html(message); emptyContentElement.removeClass('hidden'); }, @@ -391,6 +377,7 @@ var mimeType = null; var mTime = null; var etag = null; + var size = null; var albumInfo = data.albuminfo; var currentLocation = albumInfo.path; // This adds a new node to the map for each parent album @@ -406,8 +393,9 @@ mimeType = files[i].mimetype; mTime = files[i].mtime; etag = files[i].etag; + size = files[i].size; - image = new GalleryImage(path, path, fileId, mimeType, mTime, etag); + image = new GalleryImage(path, path, fileId, mimeType, mTime, etag, size); // Determines the folder name for the image var dir = OC.dirname(path); diff --git a/js/galleryimage.js b/js/galleryimage.js index fa2ab7a6b8..7b7c73bf29 100644 --- a/js/galleryimage.js +++ b/js/galleryimage.js @@ -14,21 +14,23 @@ /** * 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 * @constructor */ - var GalleryImage = function (src, path, fileId, mimeType, mTime, etag) { + var GalleryImage = function (src, path, fileId, mimeType, mTime, etag, size) { this.src = src; this.path = path; this.fileId = fileId; this.mimeType = mimeType; this.mTime = mTime; this.etag = etag; + this.size = size; this.thumbnail = null; this.domDef = null; this.spinner = null; diff --git a/js/galleryview.js b/js/galleryview.js index 6ba7f7f99b..abe9cc2f3e 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,6 +12,7 @@ var View = function () { this.element = $('#gallery'); this.loadVisibleRows.loading = false; + this._setupUploader(); this.breadcrumb = new Gallery.Breadcrumb(); }; @@ -38,16 +42,12 @@ } 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.showEmptyFolder(); + this.hideButtons(); + Gallery.currentAlbum = albumPath; + var availableWidth = $(window).width() - Gallery.buttonsWidth; + this.breadcrumb.init(albumPath, availableWidth); + Gallery.config.albumDesign = null; } else { this.viewAlbum(albumPath); } @@ -81,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); } @@ -200,6 +201,29 @@ $('#sort-date-button').hide(); }, + /** + * 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); + }); + } + }); + }, + /** * Adds all the click handlers to buttons the first time they appear in the interface * @@ -215,6 +239,7 @@ $('#sort-date-button').click(Gallery.sorter); $('#save #save-button').click(Gallery.showSaveForm); $('.save-form').submit(Gallery.saveForm); + this._renderNewButton(); this.requestId = Math.random(); }, @@ -340,6 +365,60 @@ $(this).removeClass('hover'); }); } + }, + + /** + * 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..d0640c2fc1 --- /dev/null +++ b/js/upload-helper.js @@ -0,0 +1,164 @@ +/* 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; + }, + inList: function (filename) { + "use strict"; + + }, + lastAction: function () { + "use strict"; + + }, + getUniqueName: function (newname) { + "use strict"; + + }, + add: function (fileData, options) { + "use strict"; + + }, + checkName: function (name, newname, bool) { + "use strict"; + + } +}; + +/** + * 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 = + '