js/gallery.js   F
last analyzed

Complexity

Total Complexity 66
Complexity/F 2.64

Size

Lines of Code 597
Function Count 25

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 317
dl 0
loc 597
rs 3.12
c 0
b 0
f 0
wmc 66
mnd 41
bc 41
fnc 25
bpm 1.64
cpm 2.64
noi 11

How to fix   Complexity   

Complexity

Complex classes like js/gallery.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 Album, GalleryImage */
13
(function ($, OC, t) {
14
	"use strict";
15
	var Gallery = {
16
		currentAlbum: null,
17
		currentEtag: null,
18
		config: {},
19
		/** Map of the whole gallery, built as we navigate through folders */
20
		albumMap: {},
21
		/** Used to pick an image based on the URL */
22
		imageMap: {},
23
		appName: 'gallery',
24
		token: undefined,
25
		activeSlideShow: null,
26
		buttonsWidth: 600,
27
		browserToolbarHeight: 150,
28
		filesClient: null,
29
30
		/**
31
		 * Refreshes the view and starts the slideshow if required
32
		 *
33
		 * @param {string} path
34
		 * @param {string} albumPath
35
		 */
36
		refresh: function (path, albumPath) {
37
			if (Gallery.currentAlbum !== albumPath) {
38
				Gallery.view.init(albumPath, null);
39
			}
40
41
			// If the path is mapped, that means that it's an albumPath
42
			if (Gallery.albumMap[path]) {
43
				if (Gallery.activeSlideShow) {
44
					Gallery.activeSlideShow.stop();
45
				}
46
			} else if (Gallery.imageMap[path] && Gallery.activeSlideShow.active === false) {
47
				Gallery.view.startSlideshow(path, albumPath);
48
			}
49
		},
50
51
		/**
52
		 * Retrieves information about all the images and albums located in the current folder
53
		 *
54
		 * @param {string} currentLocation
55
		 *
56
		 * @returns {*}
57
		 */
58
		getFiles: function (currentLocation) {
59
			// Cache the sorting order of the current album before loading new files
60
			if (!$.isEmptyObject(Gallery.albumMap)) {
61
				Gallery.albumMap[Gallery.currentAlbum].sorting = Gallery.config.albumSorting;
62
			}
63
			// Checks if we've visited this location before ands saves the etag to use for
64
			// comparison later
65
			var albumEtag;
66
			var albumCache = Gallery.albumMap[decodeURIComponent(currentLocation)];
67
			if (!$.isEmptyObject(albumCache)) {
68
				albumEtag = albumCache.etag;
69
			}
70
71
			// Sends the request to the server
72
			var params = {
73
				location: currentLocation,
74
				mediatypes: Gallery.config.getMediaTypes(),
75
				features: Gallery.config.getFeatures(),
76
				etag: albumEtag
0 ignored issues
show
Bug introduced by
The variable albumEtag does not seem to be initialized in case !$.isEmptyObject(albumCache) on line 67 is false. Are you sure this can never be the case?
Loading history...
77
			};
78
			// Only use the folder as a GET parameter and not as part of the URL
79
			var url = Gallery.utility.buildGalleryUrl('files', '/list', params);
80
			return $.getJSON(url).then(
81
				function (/**@type{{
82
					* files:Array,
83
					* albums:Array,
84
					* albumconfig:Object,
85
					* albumpath:String,
86
					* updated:Boolean}}*/
87
						  data) {
88
					var albumpath = data.albumpath;
89
					var updated = data.updated;
90
					// FIXME albumConfig should be cached as well
91
					/**@type {{design,information,sorting,error: string}}*/
92
					var albumConfig = data.albumconfig;
93
					//Gallery.config.setAlbumPermissions(currentAlbum);
94
					Gallery.config.setAlbumConfig(albumConfig, albumpath);
95
					// Both the folder and the etag have to match
96
					if ((decodeURIComponent(currentLocation) === albumpath) &&
97
						(updated === false)) {
98
						Gallery.imageMap = albumCache.imageMap;
99
					} else {
100
						Gallery._mapFiles(data);
101
					}
102
103
					// Restore the previous sorting order for this album
104
					if (!$.isEmptyObject(Gallery.albumMap[albumpath].sorting)) {
105
						Gallery.config.updateAlbumSorting(
106
							Gallery.albumMap[albumpath].sorting);
107
					}
108
109
				}, function (xhr) {
110
					var result = xhr.responseJSON;
111
					var albumPath = decodeURIComponent(currentLocation);
112
					var message;
113
					if (result === null) {
114
						message = t('gallery', 'There was a problem reading files from this album');
115
					} else {
116
						message = result.message;
117
					}
118
					Gallery.view.init(albumPath, message);
119
					Gallery._mapStructure(albumPath);
120
				});
121
		},
122
123
		/**
124
		 * Sorts albums and images based on user preferences
125
		 */
126
		sorter: function () {
127
			var sortType = 'name';
128
			var sortOrder = 'asc';
129
			var albumSortType = 'name';
130
			var albumSortOrder = 'asc';
131
			if (this.id === 'sort-date-button') {
132
				sortType = 'date';
133
134
			}
135
			var currentSort = Gallery.config.albumSorting;
136
			if (currentSort.type === sortType && currentSort.order === sortOrder) {
137
				sortOrder = 'des';
138
			}
139
140
			// Update the controls
141
			Gallery.view.sortControlsSetup(sortType, sortOrder);
142
143
			// We can't currently sort by album creation time
144
			if (sortType === 'name') {
145
				albumSortOrder = sortOrder;
146
			}
147
148
			// FIXME Rendering is still happening while we're sorting...
149
150
			// Clear before sorting
151
			Gallery.view.clear();
152
153
			// Sort the images
154
			Gallery.albumMap[Gallery.currentAlbum].images.sort(Gallery.utility.sortBy(sortType,
155
				sortOrder));
156
			Gallery.albumMap[Gallery.currentAlbum].subAlbums.sort(
157
				Gallery.utility.sortBy(albumSortType,
158
					albumSortOrder));
159
160
			// Save the new settings
161
			var sortConfig = {
162
				type: sortType,
163
				order: sortOrder,
164
				albumOrder: albumSortOrder
165
			};
166
			Gallery.config.updateAlbumSorting(sortConfig);
167
168
			// Refresh the view
169
			Gallery.view.viewAlbum(Gallery.currentAlbum);
170
		},
171
172
		/**
173
		 * Switches to the Files view
174
		 *
175
		 * @param event
176
		 */
177
		switchToFilesView: function (event) {
178
			event.stopPropagation();
179
180
			var subUrl = '';
181
			var params = {path: '/' + Gallery.currentAlbum};
182
			if (Gallery.token) {
183
				params.token = Gallery.token;
184
				subUrl = 's/{token}?path={path}';
185
			} else {
186
				subUrl = 'apps/files?dir={path}';
187
			}
188
189
			var button = $('#filelist-button');
190
			button.children('img').addClass('hidden');
191
			button.children('#button-loading').removeClass('hidden').addClass('icon-loading-small');
192
			OC.redirect(OC.generateUrl(subUrl, params));
193
		},
194
195
		/**
196
		 * Populates the share dialog with the needed information
197
		 *
198
		 * @param event
199
		 */
200
		share: function (event) {
201
			// Clicking on share button does not trigger automatic slide-up
202
			$('.album-info-container').slideUp();
203
204
			if (!Gallery.Share.droppedDown) {
205
				event.preventDefault();
206
				event.stopPropagation();
207
208
				var currentAlbum = Gallery.albumMap[Gallery.currentAlbum];
209
				$('#controls a.share').data('path', currentAlbum.path)
210
					.data('link', true)
211
					.data('item-source', currentAlbum.fileId)
212
					.data('possible-permissions', currentAlbum.permissions)
213
					.click();
214
			}
215
		},
216
217
		/**
218
		 * Sends an archive of the current folder to the browser
219
		 *
220
		 * @param event
221
		 */
222
		download: function (event) {
223
			event.preventDefault();
224
225
			var path = $('#app-content').data('albumname');
226
			var files = Gallery.currentAlbum;
227
			var downloadUrl = Gallery.utility.buildFilesUrl(path, files);
228
229
			OC.redirect(downloadUrl);
230
		},
231
232
		/**
233
		 * Shows an information box to the user
234
		 *
235
		 * @param event
236
		 */
237
		showInfo: function (event) {
238
			event.stopPropagation();
239
			Gallery.infoBox.showInfo();
240
		},
241
242
		/**
243
		 * Sends the shared files to the viewer's cloud
244
		 *
245
		 * @param event
246
		 */
247
		saveForm: function (event) {
248
			event.preventDefault();
249
250
			var saveElement = $('#save-external-share');
251
			var remote = $(this).find('input[type="text"]').val();
252
			var owner = saveElement.data('owner');
253
			var name = saveElement.data('name');
254
			var isProtected = saveElement.data('protected');
255
			Gallery._saveToServer(remote, Gallery.token, owner, name, isProtected);
256
		},
257
258
		/**
259
		 * Creates a new slideshow using the images found in the current folder
260
		 *
261
		 * @param {Array} images
262
		 * @param {string} startImage
263
		 * @param {boolean} autoPlay
264
		 *
265
		 * @returns {boolean}
266
		 */
267
		slideShow: function (images, startImage, autoPlay) {
268
			if (startImage === undefined) {
269
				OC.Notification.showTemporary(t('gallery',
270
					'Aborting preview. Could not find the file'));
271
				return false;
272
			}
273
			var start = images.indexOf(startImage);
274
			images = images.filter(function (image, index) {
275
				// If the slideshow is loaded before we get a thumbnail, we have to accept all
276
				// images
277
				if (!image.thumbnail) {
278
					return image;
279
				} else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
280
					if (image.thumbnail.valid) {
281
						return image;
282
					} else if (index < images.indexOf(startImage)) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if index < images.indexOf(startImage) is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
283
						start--;
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...
284
					}
285
				}
286
			}).map(function (image) {
287
				var name = OC.basename(image.path);
288
				var previewUrl = Gallery.utility.getPreviewUrl(image.fileId, image.etag);
289
				var params = {
290
					c: image.etag,
291
					requesttoken: oc_requesttoken
0 ignored issues
show
Bug introduced by
The variable oc_requesttoken seems to be never declared. If this is a global, consider adding a /** global: oc_requesttoken */ 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...
292
				};
293
				var downloadUrl = Gallery.utility.buildGalleryUrl('files',
294
					'/download/' + image.fileId,
295
					params);
296
297
				return {
298
					name: name,
299
					path: image.path,
300
					file: image.fileId,
301
					mimeType: image.mimeType,
302
					permissions: image.permissions,
303
					url: previewUrl,
304
					downloadUrl: downloadUrl
305
				};
306
			});
307
			Gallery.activeSlideShow.setImages(images, autoPlay);
308
			Gallery.activeSlideShow.onStop = function () {
309
				$('#content').show();
310
				Gallery.view.removeLoading();
311
				if (Gallery.currentAlbum !== '') {
312
					// Only modern browsers can manipulate history
313
					if (history && history.replaceState) {
314
						history.replaceState('', '',
315
							'#' + encodeURIComponent(Gallery.currentAlbum));
316
					} else {
317
						location.hash = '#' + encodeURIComponent(Gallery.currentAlbum);
318
					}
319
				} else {
320
					// Only modern browsers can manipulate history
321
					if (history && history.replaceState) {
322
						history.replaceState('', '', '#');
323
					} else {
324
						location.hash = '#';
325
					}
326
				}
327
			};
328
			Gallery.activeSlideShow.show(start);
329
			if(!_.isUndefined(Gallery.Share)){
330
				Gallery.Share.hideDropDown();
331
			}
332
			$('.album-info-container').slideUp();
333
			// Resets the last focused element
334
			document.activeElement.blur();
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...
335
		},
336
337
		/**
338
		 * Moves files and albums to a new location
339
		 *
340
		 * @param {jQuery} $item
341
		 * @param {string} fileName
342
		 * @param {string} filePath
343
		 * @param {jQuery} $target
344
		 * @param {string} targetPath
345
		 */
346
		move: function ($item, fileName, filePath, $target, targetPath) {
347
			var self = this;
348
			var dir = Gallery.currentAlbum;
349
350
			if (targetPath.charAt(targetPath.length - 1) !== '/') {
351
				// make sure we move the files into the target dir,
352
				// not overwrite it
353
				targetPath = targetPath + '/';
354
			}
355
			self.filesClient.move(dir + '/' + fileName, targetPath + fileName)
356
				.done(function () {
357
					self._removeElement(dir, filePath, $item);
358
				})
359
				.fail(function (status) {
360
					if (status === 412) {
361
						// TODO: some day here we should invoke the conflict dialog
362
						OC.Notification.showTemporary(
363
							t('gallery', 'Could not move "{file}", target exists', {file: fileName})
364
						);
365
					} else {
366
						OC.Notification.showTemporary(
367
							t('gallery', 'Could not move "{file}"', {file: fileName})
368
						);
369
					}
370
					$item.fadeTo("normal", 1);
371
					$target.children('.album-loader').hide();
372
				})
373
				.always(function () {
374
					// Nothing?
375
				});
376
		},
377
378
		/**
379
		 * Builds the album's model
380
		 *
381
		 * @param {{
382
		 * 	files:Array,
383
		 * 	albums:Array,
384
		 * 	albumconfig:Object,
385
		 * 	albumpath:String,
386
		 *	updated:Boolean
387
		 * 	}} data
388
		 * @private
389
		 */
390
		_mapFiles: function (data) {
391
			Gallery.imageMap = {};
392
			var image = null;
0 ignored issues
show
Unused Code introduced by
The assignment to image seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
393
			var path = null;
394
			var fileId = null;
0 ignored issues
show
Unused Code introduced by
The assignment to fileId seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
395
			var mimeType = null;
0 ignored issues
show
Unused Code introduced by
The assignment to mimeType seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
396
			var mTime = null;
0 ignored issues
show
Unused Code introduced by
The assignment to mTime seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
397
			var etag = null;
398
			var size = null;
399
			var sharedWithUser = null;
0 ignored issues
show
Unused Code introduced by
The assignment to sharedWithUser seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
400
			var owner = null;
401
			var permissions = 0;
402
			var currentLocation = data.albumpath;
403
			// This adds a new node to the map for each parent album
404
			Gallery._mapStructure(currentLocation);
405
			var files = data.files;
406
			if (files.length > 0) {
407
				var subAlbumCache = {};
408
				var albumCache = Gallery.albumMap[currentLocation]
409
					= new Album(
410
					currentLocation,
411
					[],
412
					[],
413
					OC.basename(currentLocation),
414
					data.albums[currentLocation].nodeid,
415
					data.albums[currentLocation].mtime,
416
					data.albums[currentLocation].etag,
417
					data.albums[currentLocation].size,
418
					data.albums[currentLocation].sharedwithuser,
419
					data.albums[currentLocation].owner,
420
					data.albums[currentLocation].freespace,
421
					data.albums[currentLocation].permissions
422
				);
423
				for (var i = 0; i < files.length; i++) {
424
					path = files[i].path;
425
					fileId = files[i].nodeid;
426
					mimeType = files[i].mimetype;
427
					mTime = files[i].mtime;
428
					etag = files[i].etag;
429
					size = files[i].size;
430
					sharedWithUser = files[i].sharedwithuser;
431
					owner = files[i].owner;
432
					permissions = files[i].permissions;
433
434
					image =
435
						new GalleryImage(
436
							path, path, fileId, mimeType, mTime, etag, size, sharedWithUser, owner, permissions
437
						);
438
439
					// Determines the folder name for the image
440
					var dir = OC.dirname(path);
441
					if (dir === path) {
442
						dir = '';
443
					}
444
					if (dir === currentLocation) {
445
						// The image belongs to the current album, so we can add it directly
446
						albumCache.images.push(image);
447
					} else {
448
						// The image belongs to a sub-album, so we create a sub-album cache if it
449
						// doesn't exist and add images to it
450
						if (!subAlbumCache[dir]) {
451
							subAlbumCache[dir] = new Album(
452
								dir,
453
								[],
454
								[],
455
								OC.basename(dir),
456
								data.albums[dir].nodeid,
457
								data.albums[dir].mtime,
458
								data.albums[dir].etag,
459
								data.albums[dir].size,
460
								data.albums[dir].sharedwithuser,
461
								data.albums[currentLocation].owner,
462
								data.albums[currentLocation].freespace,
463
								data.albums[dir].permissions);
464
						}
465
						subAlbumCache[dir].images.push(image);
466
						// The sub-album also has to be added to the global map
467
						if (!Gallery.albumMap[dir]) {
468
							Gallery.albumMap[dir] = {};
469
						}
470
					}
471
					Gallery.imageMap[image.path] = image;
472
				}
473
				// Adds the sub-albums to the current album
474
				Gallery._mapAlbums(albumCache, subAlbumCache);
475
476
				// Caches the information which is not already cached
477
				albumCache.etag = data.albums[currentLocation].etag;
478
				albumCache.imageMap = Gallery.imageMap;
479
			}
480
		},
481
482
		/**
483
		 * Adds every album leading to the current folder to a global album map
484
		 *
485
		 * Per example, if you have Root/Folder1/Folder2/CurrentFolder then the map will contain:
486
		 *    * Root
487
		 *    * Folder1
488
		 *    * Folder2
489
		 *    * CurrentFolder
490
		 *
491
		 *  Every time a new location is loaded, the map is completed
492
		 *
493
		 *
494
		 * @param {string} path
495
		 *
496
		 * @returns {Album}
497
		 * @private
498
		 */
499
		_mapStructure: function (path) {
500
			if (!Gallery.albumMap[path]) {
501
				Gallery.albumMap[path] = {};
502
				// Builds relationships between albums
503
				if (path !== '') {
504
					var parent = OC.dirname(path);
505
					if (parent === path) {
506
						parent = '';
507
					}
508
					Gallery._mapStructure(parent);
509
				}
510
			}
511
			return Gallery.albumMap[path];
512
		},
513
514
		/**
515
		 * Adds the sub-albums to the current album
516
		 *
517
		 * @param {Album} albumCache
518
		 * @param {{Album}} subAlbumCache
519
		 * @private
520
		 */
521
		_mapAlbums: function (albumCache, subAlbumCache) {
522
			for (var j = 0, keys = Object.keys(subAlbumCache); j <
523
			keys.length; j++) {
524
				albumCache.subAlbums.push(subAlbumCache[keys[j]]);
525
			}
526
		},
527
528
		/**
529
		 * Saves the folder to a remote server
530
		 *
531
		 * Our location is the remote for the other server
532
		 *
533
		 * @param {string} remote
534
		 * @param {string}token
535
		 * @param {string}owner
536
		 * @param {string}name
537
		 * @param {boolean} isProtected
538
		 * @private
539
		 */
540
		_saveToServer: function (remote, token, owner, name, isProtected) {
541
			var location = window.location.protocol + '//' + window.location.host + OC.webroot;
542
			var isProtectedInt = (isProtected) ? 1 : 0;
543
			var url = remote + '/index.php/apps/files#' + 'remote=' + encodeURIComponent(location)
544
				+ "&token=" + encodeURIComponent(token) + "&owner=" + encodeURIComponent(owner) +
545
				"&name=" +
546
				encodeURIComponent(name) + "&protected=" + isProtectedInt;
547
548
			if (remote.indexOf('://') > 0) {
549
				OC.redirect(url);
550
			} else {
551
				// if no protocol is specified, we automatically detect it by testing https and
552
				// http
553
				// this check needs to happen on the server due to the Content Security Policy
554
				// directive
555
				$.get(OC.generateUrl('apps/files_sharing/testremote'),
556
					{remote: remote}).then(function (protocol) {
557
					if (protocol !== 'http' && protocol !== 'https') {
558
						OC.dialogs.alert(t('files_sharing',
559
							'No compatible server found at {remote}',
560
							{remote: remote}),
561
							t('files_sharing', 'Invalid server url'));
562
					} else {
563
						OC.redirect(protocol + '://' + url);
564
					}
565
				});
566
			}
567
		},
568
569
		/**
570
		 * Removes the moved element from the UI and refreshes the view
571
		 *
572
		 * @param {string} dir
573
		 * @param {string}filePath
574
		 * @param {jQuery} $item
575
		 * @private
576
		 */
577
		_removeElement: function (dir, filePath, $item) {
578
			var images = Gallery.albumMap[Gallery.currentAlbum].images;
579
			var albums = Gallery.albumMap[Gallery.currentAlbum].subAlbums;
580
			// if still viewing the same directory
581
			if (Gallery.currentAlbum === dir) {
582
				var removed = false;
583
				// We try to see if an image was removed
584
				var movedImage = _(images).findIndex({path: filePath});
585
				if (movedImage >= 0) {
586
					images.splice(movedImage, 1);
587
					removed = true;
588
				} else {
589
					// It wasn't an image, so try to remove an album
590
					var movedAlbum = _(albums).findIndex({path: filePath});
591
					if (movedAlbum >= 0) {
592
						albums.splice(movedAlbum, 1);
593
						removed = true;
594
					}
595
				}
596
597
				if (removed) {
598
					$item.remove();
599
					// Refresh the photowall without checking if new files have arrived in the
600
					// current album
601
					// TODO On the next visit this album is going to be reloaded, unless we can get
602
					// an etag back from the move endpoint
603
					Gallery.view.init(Gallery.currentAlbum);
604
				}
605
			}
606
		}
607
	};
608
	window.Gallery = Gallery;
609
})(jQuery, OC, t);
610