js/galleryview.js   F
last analyzed

Complexity

Total Complexity 78
Complexity/F 2

Size

Lines of Code 605
Function Count 39

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 308
dl 0
loc 605
rs 2.16
c 0
b 0
f 0
wmc 78
mnd 39
bc 39
fnc 39
bpm 1
cpm 2
noi 4

How to fix   Complexity   

Complexity

Complex classes like js/galleryview.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
186
				}
187
188
				// Prevents creating rows which are no longer required. I.e when changing album
189
				if (view.requestId !== album.requestId) {
190
					return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
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;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
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
Bug introduced by
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