js/galleryalbum.js   A
last analyzed

Complexity

Total Complexity 37
Complexity/F 1.85

Size

Lines of Code 392
Function Count 20

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 183
dl 0
loc 392
rs 9.44
c 0
b 0
f 0
wmc 37
mnd 17
bc 17
fnc 20
bpm 0.85
cpm 1.85
noi 2
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, GalleryImage */
13
(function ($, Gallery) {
14
	"use strict";
15
16
	/**
17
	 * Creates a new album object to store information about an album
18
	 *
19
	 * @param {string} path
20
	 * @param {Array<Album|GalleryImage>} subAlbums
21
	 * @param {Array<Album|GalleryImage>} images
22
	 * @param {string} name
23
	 * @param {number} fileId
24
	 * @param {number} mTime
25
	 * @param {string} etag
26
	 * @param {number} size
27
	 * @param {Boolean} sharedWithUser
28
	 * @param {string} owner
29
	 * @param {number} freeSpace
30
	 * @param {number} permissions
31
	 * @constructor
32
	 */
33
	var Album = function (path, subAlbums, images, name, fileId, mTime, etag, size, sharedWithUser,
34
						  owner, freeSpace, permissions) {
35
		this.path = path;
36
		this.subAlbums = subAlbums;
37
		this.images = images;
38
		this.viewedItems = 0;
39
		this.name = name;
40
		this.fileId = fileId;
41
		this.mTime = mTime;
42
		this.etag = etag;
43
		this.size = size;
44
		this.sharedWithUser = sharedWithUser;
45
		this.owner = owner;
46
		this.freeSpace = freeSpace;
47
		this.permissions = permissions;
48
		this.domDef = null;
49
		this.loader = null;
50
		this.preloadOffset = 0;
51
	};
52
53
	Album.prototype = {
54
		requestId: null,
55
		droppableOptions: {
56
			accept: '#gallery > .row > a',
57
			activeClass: 'album-droppable',
58
			hoverClass: 'album-droppable-hover',
59
			tolerance: 'pointer'
60
		},
61
62
		/**
63
		 * Processes UI elements dropped on the album
64
		 *
65
		 * @param event
66
		 * @param ui
67
		 */
68
		onDrop: function (event, ui) {
69
			var $item = ui.draggable;
70
			var $clone = ui.helper;
71
			var $target = $(event.target);
72
			var targetPath = $target.data('dir').toString();
73
			var filePath = $item.data('path').toString();
74
			var fileName = OC.basename(filePath);
75
76
			this.loader.show();
77
78
			$clone.fadeOut("normal", function () {
79
				Gallery.move($item, fileName, filePath, $target, targetPath);
80
			});
81
		},
82
83
		/**
84
		 * Returns a new album row
85
		 *
86
		 * @param {number} width
87
		 *
88
		 * @returns {Gallery.Row}
89
		 */
90
		getRow: function (width) {
91
			return new Gallery.Row(width);
92
		},
93
94
		/**
95
		 * Creates the DOM element for the album and return it immediately so as to not block the
96
		 * rendering of the rest of the interface
97
		 *
98
		 *    * Each album also contains a link to open that folder
99
		 *    * An album has a natural size of 200x200 and is comprised of 4 thumbnails which have a
100
		 *        natural size of 200x200
101
		 *    * Thumbnails are checked first in order to make sure that we have something to show
102
		 *
103
		 * @param {number} targetHeight Each row has a specific height
104
		 *
105
		 * @return {$} The album to be placed on the row
106
		 */
107
		getDom: function (targetHeight) {
108
			if (this.domDef === null) {
109
				var albumElement = Gallery.Templates.galleryalbum({
110
					targetHeight: targetHeight,
111
					targetWidth: targetHeight,
112
					dir: this.path,
113
					path: this.path,
114
					permissions: this.permissions,
115
					freeSpace: this.freeSpace,
116
					label: this.name,
117
					targetPath: '#' + encodeURIComponent(this.path)
118
				});
119
				this.domDef = $(albumElement);
120
				this.loader = this.domDef.children('.album-loader');
121
				this.loader.hide();
122
				this.domDef.click(this._openAlbum.bind(this));
123
124
				this.droppableOptions.drop = this.onDrop.bind(this);
125
				this.domDef.droppable(this.droppableOptions);
126
127
				// Define a if you don't want to set the style in the template
128
				//a.width(targetHeight);
129
				//a.height(targetHeight);
130
131
				this._fillSubAlbum(targetHeight);
132
			} else {
133
				this.loader.hide();
134
			}
135
136
			return this.domDef;
137
		},
138
139
		/**
140
		 * Fills the row with albums and images
141
		 *
142
		 * @param {Gallery.Row} row The row to append elements to
143
		 *
144
		 * @returns {$.Deferred<Gallery.Row>}
145
		 */
146
		fillNextRow: function (row) {
147
			var def = new $.Deferred();
148
			var numberOfThumbnailsToPreload = 6;
149
			var buffer = 5;
150
151
			/**
152
			 * Add images to the row until it's full
153
			 *
154
			 * @todo The number of images to preload should be a user setting
155
			 *
156
			 * @param {Album} album
157
			 * @param {Row} row
158
			 * @param {Array<Album|GalleryImage>} images
159
			 *
160
			 * @returns {$.Deferred<Gallery.Row>}
161
			 */
162
			var addRowElements = function (album, row, images) {
163
				if ((album.viewedItems + buffer) > album.preloadOffset &&
164
					(album.preloadOffset < images.length)) {
165
					album._preload(numberOfThumbnailsToPreload);
166
				}
167
168
				var image = images[album.viewedItems];
169
				return row.addElement(image).then(function (more) {
170
					album.viewedItems++;
171
					if (more && album.viewedItems < images.length) {
172
						return addRowElements(album, row, images);
173
					}
174
					row.fit();
175
					def.resolve(row);
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...
176
				});
177
			};
178
			var items = this.subAlbums.concat(this.images);
179
			addRowElements(this, row, items);
180
			return def.promise();
181
		},
182
183
		/**
184
		 * Returns IDs of thumbnails belonging to the album
185
		 *
186
		 * @param {number} count
187
		 *
188
		 * @return number[]
189
		 */
190
		getThumbnailIds: function (count) {
191
			var ids = [];
192
			var items = this.images.concat(this.subAlbums);
193
			for (var i = 0; i < items.length && i < count; i++) {
194
				ids = ids.concat(items[i].getThumbnailIds(count));
195
			}
196
197
			return ids;
198
		},
199
200
		/**
201
		 * Call when the album is clicked on.
202
		 *
203
		 * @param event
204
		 * @private
205
		 */
206
		_openAlbum: function (event) {
207
			event.stopPropagation();
208
			// show loading animation
209
			this.loader.show();
210
			if(!_.isUndefined(Gallery.Share)){
211
				Gallery.Share.hideDropDown();
212
			}
213
		},
214
215
		/**
216
		 * Retrieves a thumbnail and adds it to the album representation
217
		 *
218
		 * Only attaches valid thumbnails to the album
219
		 *
220
		 * @param {GalleryImage} image
221
		 * @param {number} targetHeight Each row has a specific height
222
		 * @param {number} calcWidth Album width
223
		 * @param {jQuery} imageHolder
224
		 *
225
		 * @returns {$.Deferred<Thumbnail>}
226
		 * @private
227
		 */
228
		_getOneImage: function (image, targetHeight, calcWidth, imageHolder) {
229
			var backgroundHeight, backgroundWidth;
230
231
			backgroundHeight = (targetHeight / 2);
232
			backgroundWidth = calcWidth - 2.01;
233
234
			// Adjust the size because of the margins around pictures
235
			backgroundHeight -= 2;
236
237
			imageHolder.css("height", backgroundHeight)
238
				.css("width", backgroundWidth);
239
			var spinner = $('<div class="icon-loading">');
240
			imageHolder.append(spinner);
241
242
			// img is a Thumbnail.image, true means square thumbnails
243
			return image.getThumbnail(true).then(function (img) {
244
				if (image.thumbnail.valid) {
245
					img.alt = '';
246
					spinner.remove();
247
					imageHolder.css("background-image", "url('" + img.src + "')")
248
						.css('opacity', 1);
249
				}
250
			});
251
		},
252
253
		/**
254
		 * Builds the album representation by placing 1 to 4 images on a grid
255
		 *
256
		 * @param {Array<GalleryImage>} images
257
		 * @param {number} targetHeight Each row has a specific height
258
		 * @param {object} a
259
		 *
260
		 * @returns {$.Deferred<Array>}
261
		 * @private
262
		 */
263
		_getFourImages: function (images, targetHeight, a) {
264
			var calcWidth = targetHeight;
265
			var targetWidth;
266
			var imagesCount = images.length;
267
			var def = new $.Deferred();
268
			var validImages = [];
269
			var fail = false;
270
			var thumbsArray = [];
271
272
			for (var i = 0; i < imagesCount; i++) {
273
				targetWidth = calcWidth;
274
				// One picture filling the album
275
				if (imagesCount === 1) {
276
					targetHeight = 2 * targetHeight;
277
				}
278
				// 2 bottom pictures out of 3, or 4 pictures have the size of a quarter of the album
279
				if ((imagesCount === 3 && i !== 0) || imagesCount === 4) {
280
					targetWidth = calcWidth / 2;
281
				}
282
283
				// Append the div first in order to not lose the order of images
284
				var imageHolder = $('<div class="cropped">');
285
				a.append(imageHolder);
286
				thumbsArray.push(
287
					this._getOneImage(images[i], targetHeight, targetWidth, imageHolder));
288
			}
289
290
			// This technique allows us to wait for all objects to be resolved before making a
291
			// decision
292
			$.when.apply($, thumbsArray).done(function () {
293
				for (var i = 0; i < imagesCount; i++) {
294
					// Collect all valid images, just in case
295
					if (images[i].thumbnail.valid) {
296
						validImages.push(images[i]);
297
					} else {
298
						fail = true;
299
					}
300
				}
301
302
				// At least one thumbnail could not be retrieved
303
				if (fail) {
304
					// Clean up the album
305
					a.children().remove();
306
					// Send back the list of images which have thumbnails
307
					def.reject(validImages);
308
				}
309
			});
310
311
			return def.promise();
312
		},
313
314
		/**
315
		 * Fills the album representation with images we've received
316
		 *
317
		 *    * Each album includes between 1 and 4 images
318
		 *    * Each album is also a link to open that folder
319
		 *    * An album has a natural size of 200x200 and is comprised of 4 thumbnails which have a
320
		 * natural size of 200x200 The whole thing gets resized to match the targetHeight
321
		 *
322
		 * @param {number} targetHeight
323
		 * @private
324
		 */
325
		_fillSubAlbum: function (targetHeight) {
326
			var album = this;
327
			var subAlbum = this.domDef.children('.album');
328
329
			if (this.images.length >= 1) {
330
				this._getFourImages(this.images, targetHeight, subAlbum).fail(
331
					function (validImages) {
332
						album.images = validImages;
333
						album._fillSubAlbum(targetHeight, subAlbum);
334
					});
335
			} else {
336
				var imageHolder = $('<div class="cropped">');
337
				subAlbum.append(imageHolder);
338
				this._showFolder(targetHeight, imageHolder);
339
			}
340
		},
341
342
		/**
343
		 * Shows a folder icon in the album since we couldn't get any proper thumbnail
344
		 *
345
		 * @param {number} targetHeight
346
		 * @param imageHolder
347
		 * @private
348
		 */
349
		_showFolder: function (targetHeight, imageHolder) {
350
			var image = new GalleryImage('Generic folder', 'Generic folder', -1, 'image/svg+xml',
351
				null, null);
352
			var thumb = Thumbnails.getStandardIcon(-1);
353
			image.thumbnail = thumb;
354
			this.images.push(image);
355
			thumb.loadingDeferred.done(function (img) {
356
				img.height = (targetHeight - 2);
357
				img.width = (targetHeight) - 2;
358
				imageHolder.append(img);
359
				imageHolder.css('opacity', 1);
360
			});
361
		},
362
363
		/**
364
		 * Preloads the first $count thumbnails
365
		 *
366
		 * @param {number} count
367
		 * @private
368
		 */
369
		_preload: function (count) {
370
			var items = this.subAlbums.concat(this.images);
371
			var realCounter = 0;
372
			var maxThumbs = 0;
373
			var fileIds = [];
374
			var squareFileIds = [];
375
			for (var i = this.preloadOffset; i < this.preloadOffset + count &&
376
			i < items.length; i++) {
377
				if (items[i].subAlbums) {
378
					maxThumbs = 4;
379
					var imagesLength = items[i].images.length;
380
					if (imagesLength > 0 && imagesLength < 4) {
381
						maxThumbs = imagesLength;
382
					}
383
					var squareFileId = items[i].getThumbnailIds(maxThumbs);
384
					squareFileIds = squareFileIds.concat(squareFileId);
385
					realCounter = realCounter + maxThumbs;
386
				} else {
387
					var fileId = items[i].getThumbnailIds();
388
					fileIds = fileIds.concat(fileId);
389
					realCounter++;
390
				}
391
				if (realCounter >= count) {
392
					i++;
0 ignored issues
show
Complexity Coding Style introduced by
You seem to be assigning a new value to the loop variable i here. Please check if this was indeed your intention. Even if it was, consider using another kind of loop instead.
Loading history...
393
					break;
394
				}
395
			}
396
397
			this.preloadOffset = i;
398
			Thumbnails.loadBatch(fileIds, false);
399
			Thumbnails.loadBatch(squareFileIds, true);
400
		}
401
	};
402
403
	window.Album = Album;
404
})(jQuery, Gallery);
405