Issues (263)

js/galleryview.js (1 issue)

1
/**
2
 * Nextcloud - Gallery
3
 *
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Olivier Paroz <[email protected]>
9
 *
10
 * @copyright Olivier Paroz 2017
11
 */
12
/* global Handlebars, Gallery, Thumbnails */
13
(function ($, _, OC, t, Gallery) {
14
	"use strict";
15
16
	var TEMPLATE_ADDBUTTON = '<a href="#" class="button new"><span class="icon icon-add"></span><span class="hidden-visually">New</span></a>';
17
18
	/**
19
	 * Builds and updates the Gallery view
20
	 *
21
	 * @constructor
22
	 */
23
	var View = function () {
24
		this.element = $('#gallery');
25
		this.loadVisibleRows.loading = false;
26
		this._setupUploader();
27
		this.breadcrumb = new Gallery.Breadcrumb();
28
		this.emptyContentElement = $('#emptycontent');
29
		this.controlsElement = $('#controls');
30
	};
31
32
	View.prototype = {
33
		element: null,
34
		breadcrumb: null,
35
		requestId: -1,
36
		emptyContentElement: null,
37
		controlsElement: null,
38
39
		/**
40
		 * Removes all thumbnails from the view
41
		 */
42
		clear: function () {
43
			this.loadVisibleRows.processing = false;
44
			this.loadVisibleRows.loading = null;
45
			// We want to keep all the events
46
			this.element.children().detach();
47
			this.showLoading();
48
		},
49
50
		/**
51
		 * @param {string} path
52
		 * @returns {boolean}
53
		 */
54
		_isValidPath: function(path) {
55
			var sections = path.split('/');
56
			for (var i = 0; i < sections.length; i++) {
57
				if (sections[i] === '..') {
58
					return false;
59
				}
60
			}
61
62
			return path.toLowerCase().indexOf(decodeURI('%0a')) === -1 &&
63
				path.toLowerCase().indexOf(decodeURI('%00')) === -1;
64
		},
65
66
		/**
67
		 * Populates the view if there are images or albums to show
68
		 *
69
		 * @param {string} albumPath
70
		 * @param {string|undefined} errorMessage
71
		 */
72
		init: function (albumPath, errorMessage) {
73
			// Set path to an empty value if not a valid one
74
			if(!this._isValidPath(albumPath)) {
75
				albumPath = '';
76
			}
77
78
			// Only do it when the app is initialised
79
			if (this.requestId === -1) {
80
				this._initButtons();
81
				this._blankUrl();
82
			}
83
			if ($.isEmptyObject(Gallery.imageMap)) {
84
				Gallery.view.showEmptyFolder(albumPath, errorMessage);
85
			} else {
86
				this.viewAlbum(albumPath);
87
			}
88
89
			this._setBackgroundColour();
90
		},
91
92
		/**
93
		 * Starts the slideshow
94
		 *
95
		 * @param {string} path
96
		 * @param {string} albumPath
97
		 */
98
		startSlideshow: function (path, albumPath) {
99
			var album = Gallery.albumMap[albumPath];
100
			var images = album.images;
101
			var startImage = Gallery.imageMap[path];
102
			Gallery.slideShow(images, startImage, false);
103
		},
104
105
		/**
106
		 * Sets up the controls and starts loading the gallery rows
107
		 *
108
		 * @param {string|null} albumPath
109
		 */
110
		viewAlbum: function (albumPath) {
111
			albumPath = albumPath || '';
112
			if (!Gallery.albumMap[albumPath]) {
113
				return;
114
			}
115
116
			this.clear();
117
118
			if (albumPath !== Gallery.currentAlbum
119
				|| (albumPath === Gallery.currentAlbum &&
120
				Gallery.albumMap[albumPath].etag !== Gallery.currentEtag)) {
121
				Gallery.currentAlbum = albumPath;
122
				Gallery.currentEtag = Gallery.albumMap[albumPath].etag;
123
				this._setupButtons(albumPath);
124
			}
125
126
			Gallery.albumMap[albumPath].viewedItems = 0;
127
			Gallery.albumMap[albumPath].preloadOffset = 0;
128
129
			// Each request has a unique ID, so that we can track which request a row belongs to
130
			this.requestId = Math.random();
131
			Gallery.albumMap[Gallery.currentAlbum].requestId = this.requestId;
132
133
			// Loading rows without blocking the execution of the rest of the script
134
			setTimeout(function () {
135
				this.loadVisibleRows.activeIndex = 0;
136
				this.loadVisibleRows(Gallery.albumMap[Gallery.currentAlbum]);
137
			}.bind(this), 0);
138
		},
139
140
		/**
141
		 * Manages the sorting interface
142
		 *
143
		 * @param {string} sortType name or date
144
		 * @param {string} sortOrder asc or des
145
		 */
146
		sortControlsSetup: function (sortType, sortOrder) {
147
			var reverseSortType = 'date';
148
			if (sortType === 'date') {
149
				reverseSortType = 'name';
150
			}
151
			this._setSortButton(sortType, sortOrder, true);
152
			this._setSortButton(reverseSortType, 'asc', false); // default icon
153
		},
154
155
		/**
156
		 * Loads and displays gallery rows on screen
157
		 *
158
		 * view.loadVisibleRows.loading holds the Promise of a row
159
		 *
160
		 * @param {Album} album
161
		 */
162
		loadVisibleRows: function (album) {
163
			var view = this;
164
			// Wait for the previous request to be completed
165
			if (this.loadVisibleRows.processing) {
166
				return;
167
			}
168
169
			/**
170
			 * At this stage, there is no loading taking place, so we can look for new rows
171
			 */
172
173
			var scroll = $(window).scrollTop() + $(window).scrollTop();
174
			// 2 windows worth of rows is the limit from which we need to start loading new rows.
175
			// As we scroll down, it grows
176
			var targetHeight = ($(window).height() * 2) + scroll;
177
			// We throttle rows in order to try and not generate too many CSS resizing events at
178
			// the same time
179
			var showRows = _.throttle(function (album) {
180
181
				// If we've reached the end of the album, we kill the loader
182
				if (!(album.viewedItems < album.subAlbums.length + album.images.length)) {
183
					view.loadVisibleRows.processing = false;
184
					view.loadVisibleRows.loading = null;
185
					return;
186
				}
187
188
				// Prevents creating rows which are no longer required. I.e when changing album
189
				if (view.requestId !== album.requestId) {
190
					return;
191
				}
192
193
				// We can now safely create a new row
194
				var row = album.getRow($(window).width());
195
				var rowDom = row.getDom();
196
				view.element.append(rowDom);
197
198
				return album.fillNextRow(row).then(function () {
199
					if (album.viewedItems < album.subAlbums.length + album.images.length &&
200
						view.element.height() < targetHeight) {
201
						return showRows(album);
202
					}
203
					// No more rows to load at the moment
204
					view.loadVisibleRows.processing = false;
205
					view.loadVisibleRows.loading = null;
206
				}, function () {
207
					// Something went wrong, so kill the loader
208
					view.loadVisibleRows.processing = false;
209
					view.loadVisibleRows.loading = null;
210
				});
211
			}, 100);
212
			if (this.element.height() < targetHeight) {
213
				this._showNormal();
214
				this.loadVisibleRows.processing = true;
215
				album.requestId = view.requestId;
216
				this.loadVisibleRows.loading = showRows(album);
217
			}
218
		},
219
220
		/**
221
		 * Shows an empty gallery message
222
		 *
223
		 * @param {string} albumPath
224
		 * @param {string|null} errorMessage
225
		 */
226
		showEmptyFolder: function (albumPath, errorMessage) {
227
			var message = '<div class="icon-gallery"></div>';
228
			var uploadAllowed = true;
229
230
			this.element.children().detach();
231
			this.removeLoading();
232
233
			if (!_.isUndefined(errorMessage) && errorMessage !== null) {
234
				message += '<h2>' + t('gallery',
235
						'Album cannot be shown') + '</h2>';
236
				message += '<p>' + escapeHTML(errorMessage) + '</p>';
237
				uploadAllowed = false;
238
			} else {
239
				message += '<h2>' + t('gallery',
240
						'No media files found') + '</h2>';
241
				// We can't upload yet on the public side
242
				if (Gallery.token) {
243
					message += '<p>' + t('gallery',
244
							'Upload pictures in the Files app to display them here') + '</p>';
245
				} else {
246
					message += '<p>' + t('gallery',
247
							'Upload new files via drag and drop or by using the [+] button above') +
248
						'</p>';
249
				}
250
			}
251
			this.emptyContentElement.html(message);
252
			this.emptyContentElement.removeClass('hidden');
253
254
			this._hideButtons(uploadAllowed);
255
			Gallery.currentAlbum = albumPath;
256
			var availableWidth = $(window).width() - Gallery.buttonsWidth;
257
			this.breadcrumb.init(albumPath, availableWidth);
258
			Gallery.config.albumDesign = null;
259
		},
260
261
		/**
262
		 * Dims the controls bar when retrieving new content. Matches the effect in Files
263
		 */
264
		dimControls: function () {
265
			// Use the existing mask if its already there
266
			var $mask = this.controlsElement.find('.mask');
267
			if ($mask.exists()) {
268
				return;
269
			}
270
			$mask = $('<div class="mask transparent"></div>');
271
			this.controlsElement.append($mask);
272
			$mask.removeClass('transparent');
273
		},
274
275
		/**
276
		 * Shows the infamous loading spinner
277
		 */
278
		showLoading: function () {
279
			this.emptyContentElement.addClass('hidden');
280
			this.controlsElement.removeClass('hidden');
281
			$('#content').addClass('icon-loading');
282
			this.dimControls();
283
		},
284
285
		/**
286
		 * Removes the spinner in the main area and restore normal visibility of the controls bar
287
		 */
288
		removeLoading: function () {
289
			$('#content').removeClass('icon-loading');
290
			this.controlsElement.find('.mask').remove();
291
		},
292
293
		/**
294
		 * Shows thumbnails
295
		 */
296
		_showNormal: function () {
297
			this.emptyContentElement.addClass('hidden');
298
			this.controlsElement.removeClass('hidden');
299
			this.removeLoading();
300
		},
301
302
		/**
303
		 * Sets up our custom handlers for folder uploading operations
304
		 *
305
		 * @see OC.Upload.init/file_upload_param.done()
306
		 *
307
		 * @private
308
		 */
309
		_setupUploader: function () {
310
			var $uploadEl = $('#file_upload_start');
311
			if (!$uploadEl.exists()) {
312
				return;
313
			}
314
315
			this._operationProgressBar = new OCA.Files.OperationProgressBar();
316
			this._operationProgressBar.render();
317
			$('#content').find('#uploadprogresswrapper').replaceWith(this._operationProgressBar.$el);
318
319
			this._uploader = new OC.Uploader($uploadEl, {
320
				fileList: FileList,
0 ignored issues
show
The variable FileList seems to be never declared. If this is a global, consider adding a /** global: FileList */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
321
				dropZone: $('#content'),
322
				progressBar: this._operationProgressBar
323
			});
324
			this._uploader.on('add', function (e, data) {
325
				data.targetDir = '/' + Gallery.currentAlbum;
326
			});
327
			this._uploader.on('done', function (e, upload) {
328
				var data = upload.data;
329
330
				// is that the last upload ?
331
				if (data.files[0] === data.originalFiles[data.originalFiles.length - 1]) {
332
					var fileList = data.originalFiles;
333
					//Ask for a refresh of the photowall
334
					Gallery.getFiles(Gallery.currentAlbum).done(function () {
335
						var fileId, path;
336
						// Removes the cached thumbnails of files which have been re-uploaded
337
						_(fileList).each(function (fileName) {
338
							path = Gallery.currentAlbum + '/' + fileName;
339
							if (Gallery.imageMap[path]) {
340
								fileId = Gallery.imageMap[path].fileId;
341
								if (Thumbnails.map[fileId]) {
342
									delete Thumbnails.map[fileId];
343
								}
344
							}
345
						});
346
347
						Gallery.view.init(Gallery.currentAlbum);
348
					});
349
				}
350
			});
351
352
			// Since Nextcloud 9.0
353
			if (OC.Uploader) {
354
				OC.Uploader.prototype._isReceivedSharedFile = function (file) {
355
					var path = file.name;
356
					var sharedWith = false;
357
358
					if (Gallery.currentAlbum !== '' && Gallery.currentAlbum !== '/') {
359
						path = Gallery.currentAlbum + '/' + path;
360
					}
361
					if (Gallery.imageMap[path] && Gallery.imageMap[path].sharedWithUser) {
362
						sharedWith = true;
363
					}
364
365
					return sharedWith;
366
				};
367
			}
368
		},
369
370
		/**
371
		 * Adds all the click handlers to buttons the first time they appear in the interface
372
		 *
373
		 * @private
374
		 */
375
		_initButtons: function () {
376
			this.element.on("contextmenu", function(e) { e.preventDefault(); });
377
			$('#filelist-button').click(Gallery.switchToFilesView);
378
			$('#download').click(Gallery.download);
379
			$('#shared-button').click(Gallery.share);
380
			Gallery.infoBox = new Gallery.InfoBox();
381
			$('#album-info-button').click(Gallery.showInfo);
382
			$('#sort-name-button').click(Gallery.sorter);
383
			$('#sort-date-button').click(Gallery.sorter);
384
			$('.save-form').submit(Gallery.saveForm);
385
			this._renderNewButton();
386
			// Trigger cancelling of file upload
387
			$('#uploadprogresswrapper .stop').on('click', function () {
388
				OC.Upload.cancelUploads();
389
			});
390
			this.requestId = Math.random();
391
		},
392
393
		/**
394
		 * Sets up all the buttons of the interface and the breadcrumbs
395
		 *
396
		 * @param {string} albumPath
397
		 * @private
398
		 */
399
		_setupButtons: function (albumPath) {
400
			this._shareButtonSetup(albumPath);
401
			this._infoButtonSetup();
402
403
			var availableWidth = $(window).width() - Gallery.buttonsWidth;
404
			this.breadcrumb.init(albumPath, availableWidth);
405
			var album = Gallery.albumMap[albumPath];
406
407
			var sum = album.images.length + album.subAlbums.length;
408
			//If sum of the number of images and subalbums exceeds 1 then show the buttons.
409
			if(sum > 1)
410
			{
411
				$('#sort-name-button').show();
412
				$('#sort-date-button').show();
413
			}
414
			else
415
			{
416
				$('#sort-name-button').hide();
417
				$('#sort-date-button').hide();
418
			}
419
			var currentSort = Gallery.config.albumSorting;
420
			this.sortControlsSetup(currentSort.type, currentSort.order);
421
			Gallery.albumMap[Gallery.currentAlbum].images.sort(
422
				Gallery.utility.sortBy(currentSort.type,
423
					currentSort.order));
424
			Gallery.albumMap[Gallery.currentAlbum].subAlbums.sort(Gallery.utility.sortBy('name',
425
				currentSort.albumOrder));
426
427
			$('#save-button').show();
428
			$('#download').show();
429
			$('a.button.new').show();
430
		},
431
432
		/**
433
		 * Hide buttons in the controls bar
434
		 *
435
		 * @param uploadAllowed
436
		 */
437
		_hideButtons: function (uploadAllowed) {
438
			$('#album-info-button').hide();
439
			$('#shared-button').hide();
440
			$('#sort-name-button').hide();
441
			$('#sort-date-button').hide();
442
			$('#save-button').hide();
443
			$('#download').hide();
444
445
			if (!uploadAllowed) {
446
				$('a.button.new').hide();
447
			}
448
		},
449
450
		/**
451
		 * Shows or hides the share button depending on if we're in a public gallery or not
452
		 *
453
		 * @param {string} albumPath
454
		 * @private
455
		 */
456
		_shareButtonSetup: function (albumPath) {
457
			var shareButton = $('#shared-button');
458
			if (albumPath === '' || Gallery.token) {
459
				shareButton.hide();
460
			} else {
461
				shareButton.show();
462
			}
463
		},
464
465
		/**
466
		 * Shows or hides the info button based on the information we've received from the server
467
		 *
468
		 * @private
469
		 */
470
		_infoButtonSetup: function () {
471
			var infoButton = $('#album-info-button');
472
			infoButton.find('span').hide();
473
			var infoContentContainer = $('.album-info-container');
474
			infoContentContainer.slideUp();
475
			infoContentContainer.css('max-height',
476
				$(window).height() - Gallery.browserToolbarHeight);
477
			var albumInfo = Gallery.config.albumInfo;
478
			if (Gallery.config.albumError) {
479
				infoButton.hide();
480
				var text = '<strong>' + t('gallery', 'Configuration error') + '</strong></br>' +
481
					Gallery.config.albumError.message + '</br></br>';
482
				Gallery.utility.showHtmlNotification(text, 7);
483
			} else if ($.isEmptyObject(albumInfo)) {
484
				infoButton.hide();
485
			} else {
486
				infoButton.show();
487
				if (albumInfo.inherit !== 'yes' || albumInfo.level === 0) {
488
					infoButton.find('span').delay(1000).slideDown();
489
				}
490
			}
491
		},
492
493
		/**
494
		 * Sets the background colour of the photowall
495
		 *
496
		 * @private
497
		 */
498
		_setBackgroundColour: function () {
499
			var wrapper = $('#app-content');
500
			var albumDesign = Gallery.config.albumDesign;
501
			if (!$.isEmptyObject(albumDesign) && albumDesign.background) {
502
				wrapper.css('background-color', albumDesign.background);
503
			} else {
504
				wrapper.css('background-color', '#fff');
505
			}
506
		},
507
508
		/**
509
		 * Picks the image which matches the sort order
510
		 *
511
		 * @param {string} sortType name or date
512
		 * @param {string} sortOrder asc or des
513
		 * @param {boolean} active determines if we're setting up the active sort button
514
		 * @private
515
		 */
516
		_setSortButton: function (sortType, sortOrder, active) {
517
			var button = $('#sort-' + sortType + '-button');
518
			// Removing all the classes which control the image in the button
519
			button.removeClass('active');
520
			button.find('img').removeClass('front');
521
			button.find('img').removeClass('back');
522
523
			// We need to determine the reverse order in order to send that image to the back
524
			var reverseSortOrder = 'des';
525
			if (sortOrder === 'des') {
526
				reverseSortOrder = 'asc';
527
			}
528
529
			// We assign the proper order to the button images
530
			button.find('img.' + sortOrder).addClass('front');
531
			button.find('img.' + reverseSortOrder).addClass('back');
532
533
			// The active button needs a hover action for the flip effect
534
			if (active) {
535
				button.addClass('active');
536
				if (button.is(":hover")) {
537
					button.removeClass('hover');
538
				}
539
				// We can't use a toggle here
540
				button.hover(function () {
541
						$(this).addClass('hover');
542
					},
543
					function () {
544
						$(this).removeClass('hover');
545
					});
546
			}
547
		},
548
549
		/**
550
		 * If no url is entered then do not show the error box.
551
		 *
552
		 */
553
		_blankUrl: function() {
554
			$('#remote_address').on("change keyup paste", function() {
555
 				if ($(this).val() === '') {
556
 					$('#save-button-confirm').prop('disabled', true);
557
 				} else {
558
 					$('#save-button-confirm').prop('disabled', false);
559
 				}
560
			});
561
		},
562
563
		/**
564
		 * Creates the [+] button allowing users who can't drag and drop to upload files
565
		 *
566
		 * @see core/apps/files/js/filelist.js
567
		 * @private
568
		 */
569
		_renderNewButton: function () {
570
			// if no actions container exist, skip
571
			var $actionsContainer = $('.actions');
572
			if (!$actionsContainer.length) {
573
				return;
574
			}
575
576
			var $newButton = $(TEMPLATE_ADDBUTTON);
577
578
			$actionsContainer.prepend($newButton);
579
			$newButton.tooltip({'placement': 'bottom'});
580
581
			$newButton.click(_.bind(this._onClickNewButton, this));
582
			this._newButton = $newButton;
583
		},
584
585
		/**
586
		 * Creates the click handler for the [+] button
587
		 * @param event
588
		 * @returns {boolean}
589
		 *
590
		 * @see core/apps/files/js/filelist.js
591
		 * @private
592
		 */
593
		_onClickNewButton: function (event) {
594
			var $target = $(event.target);
595
			if (!$target.hasClass('.button')) {
596
				$target = $target.closest('.button');
597
			}
598
			this._newButton.tooltip('hide');
599
			event.preventDefault();
600
			if ($target.hasClass('disabled')) {
601
				return false;
602
			}
603
			if (!this._newFileMenu) {
604
				this._newFileMenu = new Gallery.NewFileMenu();
605
				$('.actions').append(this._newFileMenu.$el);
606
			}
607
			this._newFileMenu.showAt($target);
608
609
			if (Gallery.currentAlbum === '') {
610
				$('.menuitem[data-action="hideAlbum"]').parent().hide();
611
			}
612
			return false;
613
		}
614
	};
615
616
	Gallery.View = View;
617
})(jQuery, _, OC, t, Gallery);
618