js/slideshow.js   F
last analyzed

Complexity

Total Complexity 64
Complexity/F 1.73

Size

Lines of Code 579
Function Count 37

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 303
dl 0
loc 579
rs 3.28
c 0
b 0
f 0
wmc 64
mnd 27
bc 27
fnc 37
bpm 0.7297
cpm 1.7297
noi 2

How to fix   Complexity   

Complexity

Complex classes like js/slideshow.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 Gallery, Thumbnails, DOMPurify */
13
(function ($, OC, OCA, t) {
14
	"use strict";
15
	/**
16
	 * Slideshow featuring zooming
17
	 *
18
	 * @constructor
19
	 */
20
	var SlideShow = function () {
21
	};
22
23
	SlideShow.prototype = {
24
		slideshowTemplate: null,
25
		container: null,
26
		zoomablePreviewContainer: null,
27
		controls: null,
28
		imageCache: {},
29
		/** {Image} */
30
		currentImage: null,
31
		errorLoadingImage: false,
32
		onStop: null,
33
		zoomablePreview: null,
34
		active: false,
35
		backgroundToggle: false,
36
		// We need 6 hexas for comparison reasons
37
		darkBackgroundColour: '#000000',
38
		lightBackgroundColour: '#ffffff',
39
40
		/**
41
		 * Initialises the slideshow
42
		 *
43
		 * @param {boolean} autoPlay
44
		 * @param {number} interval
45
		 * @param {Array} features
46
		 */
47
		init: function (autoPlay, interval, features) {
48
			if (features.indexOf('background_colour_toggle') > -1) {
49
				this.backgroundToggle = true;
50
			}
51
52
			return $.when(this._getSlideshowTemplate()).then(function ($tmpl) {
53
				// Move the slideshow outside the content so we can hide the content
54
				$('body').append($tmpl);
55
				this.container = $('#slideshow');
56
				this.zoomablePreviewContainer = this.container.find('.bigshotContainer');
57
				this.zoomablePreview = new SlideShow.ZoomablePreview(this.container);
58
				this.controls =
59
					new SlideShow.Controls(
60
						this,
61
						this.container,
62
						this.zoomablePreview,
63
						interval,
64
						features,
65
						this.restoreContent.bind(this));
66
				this.controls.init();
67
68
				this._initControlsAutoFader();
69
70
				// Only modern browsers can manipulate history
71
				if (history && history.pushState) {
72
					// Stop the slideshow when backing out.
73
					$(window).bind('popstate.slideshow', function () {
74
						if (this.active === true) {
75
							this.active = false;
76
							this.controls.stop();
77
						}
78
					}.bind(this));
79
				}
80
			}.bind(this)).fail(function () {
81
				OC.Notification.show(t('gallery', 'Error loading slideshow template'));
82
			});
83
		},
84
85
		/**
86
		 * Refreshes the slideshow's data
87
		 *
88
		 * @param {{name:string, url: string, path: string, fallBack: string}[]} images
89
		 * @param {boolean} autoPlay
90
		 */
91
		setImages: function (images, autoPlay) {
92
			this._hideImage();
93
			this.images = images;
94
			this.controls.update(images, autoPlay);
95
		},
96
97
		/**
98
		 * Hides the content behind the slideshow
99
		 *
100
		 * This should be called when the slideshow is shown.
101
		 *
102
		 * It hides the content (and, in the public share page, also the footer)
103
		 * to ensure that the body size is just the slideshow size and thus no
104
		 * scroll bars are shown.
105
		 */
106
		hideContent: function () {
107
			this._savedScrollPosition = $(window).scrollTop();
108
109
			$('#content').hide();
110
			$('footer').hide();
111
		},
112
113
		/**
114
		 * Shows again the content behind the slideshow
115
		 *
116
		 * This should be called when the slideshow is hidden.
117
		 *
118
		 * It restores the content hidden when calling "hideContent", including
119
		 * the vertical scrolling position.
120
		 */
121
		restoreContent: function () {
122
			$('#content').show();
123
			$('footer').show();
124
125
			if (this._savedScrollPosition) {
126
				$(window).scrollTop(this._savedScrollPosition);
127
			}
128
		},
129
130
		/**
131
		 * Launches the slideshow
132
		 *
133
		 * @param {number} index
134
		 *
135
		 * @returns {*}
136
		 */
137
		show: function (index) {
138
			this.hideErrorNotification();
139
			this.active = true;
140
			this.container.show();
141
			this.hideContent();
142
			this.container.css('background-position', 'center');
143
			this._hideImage();
144
			this.container.find('.icon-loading-dark').show();
145
			var currentImageId = index;
146
			return this.loadImage(this.images[index]).then(function (img) {
147
				this.container.css('background-position', '-10000px 0');
148
149
				// check if we moved along while we were loading
150
				if (currentImageId === index) {
151
					var image = this.images[index];
152
					var transparent = this._isTransparent(image.mimeType);
153
					this.controls.showActionButtons(transparent, Gallery.token, image.permissions);
154
					this.errorLoadingImage = false;
155
					this.currentImage = img;
156
					img.setAttribute('alt', image.name);
157
					$(img).css('position', 'absolute');
158
					$(img).css('background-color', image.backgroundColour);
159
					if (transparent && this.backgroundToggle === true) {
160
						var $border = 30 / window.devicePixelRatio;
161
						$(img).css('outline', $border + 'px solid ' + image.backgroundColour);
162
					}
163
164
					this.zoomablePreview.startBigshot(img, this.currentImage, image.mimeType);
165
166
					this._setUrl(image.path);
167
					this.controls.show(currentImageId);
168
					this.container.find('.icon-loading-dark').hide();
169
				}
170
			}.bind(this), function () {
171
				// Don't do anything if the user has moved along while we were loading as it would
172
				// mess up the index
173
				if (currentImageId === index) {
174
					this.errorLoadingImage = true;
175
					this.showErrorNotification(null);
176
					this._setUrl(this.images[index].path);
177
					this.images.splice(index, 1);
178
					this.controls.updateControls(this.images, this.errorLoadingImage);
179
				}
180
			}.bind(this));
181
		},
182
183
		/**
184
		 * Loads the image to show in the slideshow and preloads the next one
185
		 *
186
		 * @param {Object} preview
187
		 *
188
		 * @returns {*}
189
		 */
190
		loadImage: function (preview) {
191
			var url = preview.url;
192
			var mimeType = preview.mimeType;
193
194
			if (!this.imageCache[url]) {
195
				this.imageCache[url] = new $.Deferred();
196
				var image = new Image();
0 ignored issues
show
Bug introduced by
The variable Image seems to be never declared. If this is a global, consider adding a /** global: Image */ 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...
197
198
				image.onload = function () {
199
					preview.backgroundColour = this._getBackgroundColour(image, mimeType);
200
					if (this.imageCache[url]) {
201
						this.imageCache[url].resolve(image);
202
					}
203
				}.bind(this);
204
				image.onerror = function () {
205
					if (this.imageCache[url]) {
206
						this.imageCache[url].reject(url);
207
					}
208
				}.bind(this);
209
				if (mimeType === 'image/svg+xml') {
210
					image.src = this._getSVG(url);
211
				} else {
212
					image.src = url;
213
				}
214
			}
215
			return this.imageCache[url];
216
		},
217
218
		/**
219
		 * Shows a new image in the slideshow and preloads the next in the list
220
		 *
221
		 * @param {number} current
222
		 * @param {Object} next
223
		 */
224
		next: function (current, next) {
225
			this.show(current).then(function () {
226
				// Preloads the next image in the list
227
				this.loadImage(next);
228
			}.bind(this));
229
		},
230
231
		/**
232
		 * Determines which colour to use for the background
233
		 *
234
		 * @param {*} image
235
		 * @param {string} mimeType
236
		 *
237
		 * @returns {string}
238
		 * @private
239
		 */
240
		_getBackgroundColour: function (image, mimeType) {
241
			var backgroundColour = this.darkBackgroundColour;
242
			if (this._isTransparent(mimeType) && this._isMainlyDark(image)) {
243
				backgroundColour = this.lightBackgroundColour;
244
			}
245
			return backgroundColour;
246
		},
247
248
		/**
249
		 * Calculates the luminance of an image to determine if an image is mainly dark
250
		 *
251
		 * @param {*} image
252
		 *
253
		 * @returns {boolean}
254
		 * @private
255
		 */
256
		_isMainlyDark: function (image) {
257
			var isMainlyDark = false;
258
			var numberOfSamples = 1000; // Seems to be the sweet spot
259
			// The name has to be 'canvas'
260
			var lumiCanvas = document.createElement('canvas');
261
262
			var imgArea = image.width * image.height;
263
			var canArea = numberOfSamples;
264
			var factor = Math.sqrt(canArea / imgArea);
265
266
			var scaledWidth = factor * image.width;
267
			var scaledHeight = factor * image.height;
268
			lumiCanvas.width = scaledWidth;
269
			lumiCanvas.height = scaledHeight;
270
			var lumiCtx = lumiCanvas.getContext('2d');
271
			lumiCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
272
			var imgData = lumiCtx.getImageData(0, 0, lumiCanvas.width, lumiCanvas.height);
273
			var pix = imgData.data; // pix.length will be approximately 4*numberOfSamples (for RGBA)
274
			var pixelArraySize = pix.length;
275
			var totalLuminance = 0;
276
			var sampleNumber = 1;
277
			var averageLuminance;
278
			var totalAlpha = 0;
279
			var alphaLevel;
280
			var red = 0;
281
			var green = 0;
282
			var blue = 0;
283
			var alpha = 0;
284
			var lum = 0;
285
			var alphaThreshold = 0.1;
286
287
			var sampleCounter = 0;
288
			var itemsPerPixel = 4; // red, green, blue, alpha
289
			// i += 4 because 4 colours for every pixel
290
			for (var i = 0, n = pixelArraySize; i < n; i += itemsPerPixel) {
291
				sampleCounter++;
292
				alpha = pix[i + 3] / 255;
293
				totalAlpha += alpha;
294
				if (Math.ceil(alpha * 100) / 100 > alphaThreshold) {
295
					red = pix[i];
296
					green = pix[i + 1];
297
					blue = pix[i + 2];
298
					// Luminance formula from
299
					// http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
300
					lum = (red + red + green + green + green + blue) / 6;
301
					//lum = (red * 0.299 + green * 0.587 + blue * 0.114 );
302
					totalLuminance += lum * alpha;
303
					sampleNumber++;
304
				}
305
			}
306
307
			// Deletes the canvas
308
			lumiCanvas = null;
0 ignored issues
show
Unused Code introduced by
The assignment to lumiCanvas 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...
309
310
			// Calculate the optimum background colour for this image
311
			averageLuminance = Math.ceil((totalLuminance / sampleNumber) * 100) / 100;
312
			alphaLevel = Math.ceil((totalAlpha / numberOfSamples) * 100);
313
314
			if (averageLuminance < 60 && alphaLevel < 90) {
315
				isMainlyDark = true;
316
			}
317
318
			return isMainlyDark;
319
		},
320
321
		/**
322
		 * Stops the slideshow
323
		 */
324
		stop: function () {
325
			this.active = false;
326
			this.images = null;
327
			this._hideImage();
328
			if (this.onStop) {
329
				this.onStop();
330
			}
331
		},
332
333
		/**
334
		 * Sends the current image as a download
335
		 *
336
		 * @param {string} downloadUrl
337
		 *
338
		 * @returns {boolean}
339
		 */
340
		getImageDownload: function (downloadUrl) {
341
			OC.redirect(downloadUrl);
342
			return false;
343
		},
344
345
		/**
346
		 * Changes the colour of the background of the image
347
		 */
348
		toggleBackground: function () {
349
			var toHex = function (x) {
350
				return ("0" + parseInt(x).toString(16)).slice(-2);
351
			};
352
			var container = this.zoomablePreviewContainer.children('img');
353
			var rgb = container.css('background-color').match(/\d+/g);
354
			var hex = "#" + toHex(rgb[0]) + toHex(rgb[1]) + toHex(rgb[2]);
355
			var $border = 30 / window.devicePixelRatio;
356
			var newBackgroundColor;
357
358
			// Grey #363636
359
			if (hex === this.darkBackgroundColour) {
360
				newBackgroundColor = this.lightBackgroundColour;
361
			} else {
362
				newBackgroundColor = this.darkBackgroundColour;
363
			}
364
365
			container.css('background-color', newBackgroundColor);
366
			if (this.backgroundToggle === true) {
367
				container.css('outline', $border + 'px solid ' + newBackgroundColor);
368
			}
369
		},
370
371
		/**
372
		 * Shows an error notification
373
		 *
374
		 * @param {string} message
375
		 */
376
		showErrorNotification: function (message) {
377
			if ($.isEmptyObject(message)) {
378
				message = t('gallery',
379
					'<strong>Error!</strong> Could not generate a preview of this file.<br>' +
380
					'Please go to the next slide while we remove this image from the slideshow');
381
			}
382
			this.container.find('.notification').html(message);
383
			this.container.find('.notification').show();
384
			this.controls.hideButton('.changeBackground');
385
		},
386
387
		/**
388
		 * Hides the error notification
389
		 */
390
		hideErrorNotification: function () {
391
			this.container.find('.notification').hide();
392
			this.container.find('.notification').html('');
393
		},
394
395
		/**
396
		 * Removes a specific button from the interface
397
		 *
398
		 * @param button
399
		 */
400
		removeButton: function (button) {
401
			this.controls.removeButton(button);
402
		},
403
404
		/**
405
		 * Deletes an image from the slideshow
406
		 *
407
		 * @param {object} image
408
		 * @param {number} currentIndex
409
		 */
410
		deleteImage: function (image, currentIndex) {
411
			// These are Gallery specific commands to be replaced
412
			// which should sit somewhere else
413
			if (!window.galleryFileAction) {
414
				delete Gallery.imageMap[image.path];
415
				delete Thumbnails.map[image.file];
416
				Gallery.albumMap[Gallery.currentAlbum].images.splice(currentIndex, 1);
417
				Gallery.view.init(Gallery.currentAlbum);
418
			}
419
		},
420
421
		/**
422
		 * Automatically fades the controls after 3 seconds
423
		 *
424
		 * @private
425
		 */
426
		_initControlsAutoFader: function () {
427
			var inactiveCallback = function () {
428
				this.container.addClass('inactive');
429
			}.bind(this);
430
			var inactiveTimeout = setTimeout(inactiveCallback, 3000);
431
432
			this.container.on('mousemove touchstart', function () {
433
				this.container.removeClass('inactive');
434
				clearTimeout(inactiveTimeout);
435
				inactiveTimeout = setTimeout(inactiveCallback, 3000);
436
			}.bind(this));
437
		},
438
439
		/**
440
		 * Simplest way to detect if image is transparent.
441
		 *
442
		 * That's very inaccurate since it doesn't include images which support transparency
443
		 *
444
		 * @param mimeType
445
		 * @returns {boolean}
446
		 * @private
447
		 */
448
		_isTransparent: function (mimeType) {
449
			return !(mimeType === 'image/jpeg'
450
				|| mimeType === 'image/x-dcraw'
451
				|| mimeType === 'application/font-sfnt'
452
				|| mimeType === 'application/x-font'
453
			);
454
		},
455
456
		/**
457
		 * Changes the browser Url, based on the current image
458
		 *
459
		 * @param {string} path
460
		 * @private
461
		 */
462
		_setUrl: function (path) {
463
			if (history && history.replaceState) {
464
				history.replaceState('', '', '#' + encodeURI(path));
465
			}
466
		},
467
468
		/**
469
		 * Hides the current image (before loading the next)
470
		 *
471
		 * @private
472
		 */
473
		_hideImage: function () {
474
			this.zoomablePreviewContainer.empty();
475
			this.controls.hideActionButtons();
476
		},
477
478
		/**
479
		 * Retrieves an SVG
480
		 *
481
		 * An SVG can't be simply attached to a src attribute like a bitmap image
482
		 *
483
		 * @param {string} source
484
		 *
485
		 * @returns {*}
486
		 * @private
487
		 */
488
		_getSVG: function (source) {
489
			var svgPreview = null;
490
			// DOMPurify only works with IE10+ and we load SVGs in the IMG tag
491
			if (window.btoa &&
492
				document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image",
493
					"1.1")) {
494
				var xmlHttp = new XMLHttpRequest();
495
				xmlHttp.open("GET", source, false);
496
				xmlHttp.send(null);
497
				if (xmlHttp.status === 200) {
498
					var pureSvg = DOMPurify.sanitize(xmlHttp.responseText, {ADD_TAGS: ['filter']});
499
					// Remove XML comment garbage left in the purified data
500
					var badTag = pureSvg.indexOf(']&gt;');
501
					var fixedPureSvg = pureSvg.substring(badTag < 0 ? 0 : 5, pureSvg.length);
502
					svgPreview = "data:image/svg+xml;base64," + window.btoa(fixedPureSvg);
503
				}
504
			}
505
506
			return svgPreview;
507
		},
508
509
		/**
510
		 * Retrieves the slideshow's template
511
		 *
512
		 * @returns {*}
513
		 * @private
514
		 */
515
		_getSlideshowTemplate: function () {
516
			var defer = $.Deferred();
517
			if (!this.$slideshowTemplate) {
518
				var self = this;
519
				var url = OC.generateUrl('apps/gallery/slideshow', null);
520
				$.get(url, function (tmpl) {
521
						var template = $(tmpl);
522
						var tmplButton;
523
						var buttonsArray = [
524
							{
525
								el: '.next',
526
								trans: t('gallery', 'Next')
527
							},
528
							{
529
								el: '.play',
530
								trans: t('gallery', 'Play'),
531
								toolTip: true
532
							},
533
							{
534
								el: '.pause',
535
								trans: t('gallery', 'Pause'),
536
								toolTip: true
537
							},
538
							{
539
								el: '.previous',
540
								trans: t('gallery', 'Previous')
541
							},
542
							{
543
								el: '.exit',
544
								trans: t('gallery', 'Close'),
545
								toolTip: true
546
							},
547
							{
548
								el: '.downloadImage',
549
								trans: t('gallery', 'Download'),
550
								toolTip: true
551
							},
552
							{
553
								el: '.changeBackground',
554
								trans: t('gallery', 'Toggle background'),
555
								toolTip: true
556
							},
557
							{
558
								el: '.deleteImage',
559
								trans: t('gallery', 'Delete'),
560
								toolTip: true
561
							},
562
							{
563
								el: '.shareImage',
564
								trans: t('gallery', 'Share'),
565
								toolTip: true
566
							}
567
						];
568
						for (var i = 0; i < buttonsArray.length; i++) {
569
							var button = buttonsArray[i];
570
571
							tmplButton = template.find(button.el);
572
							tmplButton.val(button.trans);
573
							if (button.toolTip) {
574
								tmplButton.attr("title", button.trans);
575
							}
576
						}
577
						self.$slideshowTemplate = template;
578
						defer.resolve(self.$slideshowTemplate);
579
					})
580
					.fail(function () {
581
						defer.reject();
582
					});
583
			} else {
584
				defer.resolve(this.$slideshowTemplate);
585
			}
586
			return defer.promise();
587
		}
588
	};
589
590
	window.SlideShow = SlideShow;
591
})(jQuery, OC, OCA, t);
592