Completed
Pull Request — master (#155)
by Lukas
13:41
created

View._isValidPath   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 11
rs 9.4285
1
/* global Handlebars, Gallery */
2
(function ($, _, OC, t, Gallery) {
3
	"use strict";
4
5
	var TEMPLATE_ADDBUTTON = '<a href="#" class="button new"><img src="{{iconUrl}}" alt="{{addText}}"></img></a>';
6
7
	/**
8
	 * Builds and updates the Gallery view
9
	 *
10
	 * @constructor
11
	 */
12
	var View = function () {
13
		this.element = $('#gallery');
14
		this.loadVisibleRows.loading = false;
15
		this._setupUploader();
16
		this.breadcrumb = new Gallery.Breadcrumb();
17
		this.emptyContentElement = $('#emptycontent');
18
		this.controlsElement = $('#controls');
19
	};
20
21
	View.prototype = {
22
		element: null,
23
		breadcrumb: null,
24
		requestId: -1,
25
		emptyContentElement: null,
26
		controlsElement: null,
27
28
		/**
29
		 * Removes all thumbnails from the view
30
		 */
31
		clear: function () {
32
			this.loadVisibleRows.processing = false;
33
			this.loadVisibleRows.loading = null;
34
			// We want to keep all the events
35
			this.element.children().detach();
36
			this.showLoading();
37
		},
38
39
		/**
40
		 * @param {string} path
41
		 * @returns {boolean}
42
		 */
43
		_isValidPath: function(path) {
44
			var sections = path.split('/');
45
			for (var i = 0; i < sections.length; i++) {
46
				if (sections[i] === '..') {
47
					return false;
48
				}
49
			}
50
51
			return path.toLowerCase().indexOf(decodeURI('%0a')) === -1 &&
52
				path.toLowerCase().indexOf(decodeURI('%00')) === -1;
53
		},
54
55
		/**
56
		 * Populates the view if there are images or albums to show
57
		 *
58
		 * @param {string} albumPath
59
		 * @param {string|undefined} errorMessage
60
		 */
61
		init: function (albumPath, errorMessage) {
62
			// Set path to an empty value if not a valid one
63
			if(!this._isValidPath(albumPath)) {
64
				albumPath = '';
65
			}
66
67
			// Only do it when the app is initialised
68
			if (this.requestId === -1) {
69
				this._initButtons();
70
				this._blankUrl();
71
			}
72
			if ($.isEmptyObject(Gallery.imageMap)) {
73
				Gallery.view.showEmptyFolder(albumPath, errorMessage);
74
			} else {
75
				this.viewAlbum(albumPath);
76
			}
77
78
			this._setBackgroundColour();
79
		},
80
81
		/**
82
		 * Starts the slideshow
83
		 *
84
		 * @param {string} path
85
		 * @param {string} albumPath
86
		 */
87
		startSlideshow: function (path, albumPath) {
88
			var album = Gallery.albumMap[albumPath];
89
			var images = album.images;
90
			var startImage = Gallery.imageMap[path];
91
			Gallery.slideShow(images, startImage, false);
92
		},
93
94
		/**
95
		 * Sets up the controls and starts loading the gallery rows
96
		 *
97
		 * @param {string|null} albumPath
98
		 */
99
		viewAlbum: function (albumPath) {
100
			albumPath = albumPath || '';
101
			if (!Gallery.albumMap[albumPath]) {
102
				return;
103
			}
104
105
			this.clear();
106
107
			if (albumPath !== Gallery.currentAlbum
108
				|| (albumPath === Gallery.currentAlbum &&
109
				Gallery.albumMap[albumPath].etag !== Gallery.currentEtag)) {
110
				Gallery.currentAlbum = albumPath;
111
				Gallery.currentEtag = Gallery.albumMap[albumPath].etag;
112
				this._setupButtons(albumPath);
113
			}
114
115
			Gallery.albumMap[albumPath].viewedItems = 0;
116
			Gallery.albumMap[albumPath].preloadOffset = 0;
117
118
			// Each request has a unique ID, so that we can track which request a row belongs to
119
			this.requestId = Math.random();
120
			Gallery.albumMap[Gallery.currentAlbum].requestId = this.requestId;
121
122
			// Loading rows without blocking the execution of the rest of the script
123
			setTimeout(function () {
124
				this.loadVisibleRows.activeIndex = 0;
125
				this.loadVisibleRows(Gallery.albumMap[Gallery.currentAlbum]);
126
			}.bind(this), 0);
127
		},
128
129
		/**
130
		 * Manages the sorting interface
131
		 *
132
		 * @param {string} sortType name or date
133
		 * @param {string} sortOrder asc or des
134
		 */
135
		sortControlsSetup: function (sortType, sortOrder) {
136
			var reverseSortType = 'date';
137
			if (sortType === 'date') {
138
				reverseSortType = 'name';
139
			}
140
			this._setSortButton(sortType, sortOrder, true);
141
			this._setSortButton(reverseSortType, 'asc', false); // default icon
142
		},
143
144
		/**
145
		 * Loads and displays gallery rows on screen
146
		 *
147
		 * view.loadVisibleRows.loading holds the Promise of a row
148
		 *
149
		 * @param {Album} album
150
		 */
151
		loadVisibleRows: function (album) {
152
			var view = this;
153
			// Wait for the previous request to be completed
154
			if (this.loadVisibleRows.processing) {
155
				return;
156
			}
157
158
			/**
159
			 * At this stage, there is no loading taking place, so we can look for new rows
160
			 */
161
162
			var scroll = $('#content-wrapper').scrollTop() + $(window).scrollTop();
163
			// 2 windows worth of rows is the limit from which we need to start loading new rows.
164
			// As we scroll down, it grows
165
			var targetHeight = ($(window).height() * 2) + scroll;
166
			// We throttle rows in order to try and not generate too many CSS resizing events at
167
			// the same time
168
			var showRows = _.throttle(function (album) {
169
170
				// If we've reached the end of the album, we kill the loader
171
				if (!(album.viewedItems < album.subAlbums.length + album.images.length)) {
0 ignored issues
show
Coding Style introduced by
The usage of ! looks confusing here.

The following shows a case which JSHint considers confusing and its respective non-confusing counterpart:

! (str.indexOf(i) > -1) // Bad
str.indexOf(i) === -1 // Good
Loading history...
172
					view.loadVisibleRows.processing = false;
173
					view.loadVisibleRows.loading = null;
174
					return;
175
				}
176
177
				// Prevents creating rows which are no longer required. I.e when changing album
178
				if (view.requestId !== album.requestId) {
179
					return;
180
				}
181
182
				// We can now safely create a new row
183
				var row = album.getRow($(window).width());
184
				var rowDom = row.getDom();
185
				view.element.append(rowDom);
186
187
				return album.fillNextRow(row).then(function () {
188
					if (album.viewedItems < album.subAlbums.length + album.images.length &&
189
						view.element.height() < targetHeight) {
190
						return showRows(album);
191
					}
192
					// No more rows to load at the moment
193
					view.loadVisibleRows.processing = false;
194
					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...
195
				}, function () {
196
					// Something went wrong, so kill the loader
197
					view.loadVisibleRows.processing = false;
198
					view.loadVisibleRows.loading = null;
199
				});
200
			}, 100);
201
			if (this.element.height() < targetHeight) {
202
				this._showNormal();
203
				this.loadVisibleRows.processing = true;
204
				album.requestId = view.requestId;
205
				this.loadVisibleRows.loading = showRows(album);
206
			}
207
		},
208
209
		/**
210
		 * Shows an empty gallery message
211
		 *
212
		 * @param {string} albumPath
213
		 * @param {string|null} errorMessage
214
		 */
215
		showEmptyFolder: function (albumPath, errorMessage) {
216
			var message = '<div class="icon-gallery"></div>';
217
			var uploadAllowed = true;
218
219
			this.element.children().detach();
220
			this.removeLoading();
221
222
			if (!_.isUndefined(errorMessage) && errorMessage !== null) {
223
				message += '<h2>' + t('gallery',
224
						'Album cannot be shown') + '</h2>';
225
				message += '<p>' + escapeHTML(errorMessage) + '</p>';
0 ignored issues
show
Bug introduced by
escapeHTML does not seem to be defined.
Loading history...
226
				uploadAllowed = false;
227
			} else {
228
				message += '<h2>' + t('gallery',
229
						'No media files found') + '</h2>';
230
				// We can't upload yet on the public side
231
				if (Gallery.token) {
232
					message += '<p>' + t('gallery',
233
							'Upload pictures in the files app to display them here') + '</p>';
234
				} else {
235
					message += '<p>' + t('gallery',
236
							'Upload new files via drag and drop or by using the [+] button above') +
237
						'</p>';
238
				}
239
			}
240
			this.emptyContentElement.html(message);
241
			this.emptyContentElement.removeClass('hidden');
242
243
			this._hideButtons(uploadAllowed);
244
			Gallery.currentAlbum = albumPath;
245
			var availableWidth = $(window).width() - Gallery.buttonsWidth;
246
			this.breadcrumb.init(albumPath, availableWidth);
247
			Gallery.config.albumDesign = null;
248
		},
249
250
		/**
251
		 * Dims the controls bar when retrieving new content. Matches the effect in Files
252
		 */
253
		dimControls: function () {
254
			// Use the existing mask if its already there
255
			var $mask = this.controlsElement.find('.mask');
256
			if ($mask.exists()) {
257
				return;
258
			}
259
			$mask = $('<div class="mask transparent"></div>');
260
			this.controlsElement.append($mask);
261
			$mask.removeClass('transparent');
262
		},
263
264
		/**
265
		 * Shows the infamous loading spinner
266
		 */
267
		showLoading: function () {
268
			this.emptyContentElement.addClass('hidden');
269
			this.controlsElement.removeClass('hidden');
270
			$('#content').addClass('icon-loading');
271
			this.dimControls();
272
		},
273
274
		/**
275
		 * Removes the spinner in the main area and restore normal visibility of the controls bar
276
		 */
277
		removeLoading: function () {
278
			$('#content').removeClass('icon-loading');
279
			this.controlsElement.find('.mask').remove();
280
		},
281
282
		/**
283
		 * Shows thumbnails
284
		 */
285
		_showNormal: function () {
286
			this.emptyContentElement.addClass('hidden');
287
			this.controlsElement.removeClass('hidden');
288
			this.removeLoading();
289
		},
290
291
		/**
292
		 * Sets up our custom handlers for folder uploading operations
293
		 *
294
		 * We only want it to be called for that specific case as all other file uploading
295
		 * operations will call Files.highlightFiles
296
		 *
297
		 * @see OC.Upload.init/file_upload_param.done()
298
		 *
299
		 * @private
300
		 */
301
		_setupUploader: function () {
302
			$('#file_upload_start').on('fileuploaddone', function (e, data) {
303
				if (data.files[0] === data.originalFiles[data.originalFiles.length - 1]
304
					&& data.files[0].relativePath) {
305
306
					//Ask for a refresh of the photowall
307
					Gallery.getFiles(Gallery.currentAlbum).done(function () {
308
						Gallery.view.init(Gallery.currentAlbum);
309
					});
310
				}
311
			});
312
313
			// Since 9.0
314
			if (OC.Upload) {
315
				OC.Upload._isReceivedSharedFile = function (file) {
316
					var path = file.name;
317
					var sharedWith = false;
318
319
					if (Gallery.currentAlbum !== '' && Gallery.currentAlbum !== '/') {
320
						path = Gallery.currentAlbum + '/' + path;
321
					}
322
					if (Gallery.imageMap[path] && Gallery.imageMap[path].sharedWithUser) {
323
						sharedWith = true;
324
					}
325
326
					return sharedWith;
327
				};
328
			}
329
		},
330
331
		/**
332
		 * Adds all the click handlers to buttons the first time they appear in the interface
333
		 *
334
		 * @private
335
		 */
336
		_initButtons: function () {
337
			this.element.on("contextmenu", function(e) { e.preventDefault(); });
338
			$('#filelist-button').click(Gallery.switchToFilesView);
339
			$('#download').click(Gallery.download);
340
			$('#share-button').click(Gallery.share);
341
			Gallery.infoBox = new Gallery.InfoBox();
342
			$('#album-info-button').click(Gallery.showInfo);
343
			$('#sort-name-button').click(Gallery.sorter);
344
			$('#sort-date-button').click(Gallery.sorter);
345
			$('#save #save-button').click(Gallery.showSaveForm);
346
			$('.save-form').submit(Gallery.saveForm);
347
			this._renderNewButton();
348
			// Trigger cancelling of file upload
349
			$('#uploadprogresswrapper .stop').on('click', function () {
350
				OC.Upload.cancelUploads();
351
			});
352
			this.requestId = Math.random();
353
		},
354
355
		/**
356
		 * Sets up all the buttons of the interface and the breadcrumbs
357
		 *
358
		 * @param {string} albumPath
359
		 * @private
360
		 */
361
		_setupButtons: function (albumPath) {
362
			this._shareButtonSetup(albumPath);
363
			this._infoButtonSetup();
364
365
			var availableWidth = $(window).width() - Gallery.buttonsWidth;
366
			this.breadcrumb.init(albumPath, availableWidth);
367
			var album = Gallery.albumMap[albumPath];
368
			
369
			var sum = album.images.length + album.subAlbums.length;
370
			//If sum of the number of images and subalbums exceeds 1 then show the buttons.
371
			if(sum > 1)
372
			{
373
				$('#sort-name-button').show();
374
				$('#sort-date-button').show();
375
			}
376
			else
377
			{
378
				$('#sort-name-button').hide();
379
				$('#sort-date-button').hide();
380
			}
381
			var currentSort = Gallery.config.albumSorting;
382
			this.sortControlsSetup(currentSort.type, currentSort.order);
383
			Gallery.albumMap[Gallery.currentAlbum].images.sort(
384
				Gallery.utility.sortBy(currentSort.type,
385
					currentSort.order));
386
			Gallery.albumMap[Gallery.currentAlbum].subAlbums.sort(Gallery.utility.sortBy('name',
387
				currentSort.albumOrder));
388
389
			$('#save-button').show();
390
			$('#download').show();
391
			$('a.button.new').show();
392
		},
393
394
		/**
395
		 * Hide buttons in the controls bar
396
		 *
397
		 * @param uploadAllowed
398
		 */
399
		_hideButtons: function (uploadAllowed) {
400
			$('#album-info-button').hide();
401
			$('#share-button').hide();
402
			$('#sort-name-button').hide();
403
			$('#sort-date-button').hide();
404
			$('#save-button').hide();
405
			$('#download').hide();
406
407
			if (!uploadAllowed) {
408
				$('a.button.new').hide();
409
			}
410
		},
411
412
		/**
413
		 * Shows or hides the share button depending on if we're in a public gallery or not
414
		 *
415
		 * @param {string} albumPath
416
		 * @private
417
		 */
418
		_shareButtonSetup: function (albumPath) {
419
			var shareButton = $('#share-button');
420
			if (albumPath === '' || Gallery.token) {
421
				shareButton.hide();
422
			} else {
423
				shareButton.show();
424
			}
425
		},
426
427
		/**
428
		 * Shows or hides the info button based on the information we've received from the server
429
		 *
430
		 * @private
431
		 */
432
		_infoButtonSetup: function () {
433
			var infoButton = $('#album-info-button');
434
			infoButton.find('span').hide();
435
			var infoContentContainer = $('.album-info-container');
436
			infoContentContainer.slideUp();
437
			infoContentContainer.css('max-height',
438
				$(window).height() - Gallery.browserToolbarHeight);
439
			var albumInfo = Gallery.config.albumInfo;
440
			if (Gallery.config.albumError) {
441
				infoButton.hide();
442
				var text = '<strong>' + t('gallery', 'Configuration error') + '</strong></br>' +
443
					Gallery.config.albumError.message + '</br></br>';
444
				Gallery.utility.showHtmlNotification(text, 7);
445
			} else if ($.isEmptyObject(albumInfo)) {
446
				infoButton.hide();
447
			} else {
448
				infoButton.show();
449
				if (albumInfo.inherit !== 'yes' || albumInfo.level === 0) {
450
					infoButton.find('span').delay(1000).slideDown();
451
				}
452
			}
453
		},
454
455
		/**
456
		 * Sets the background colour of the photowall
457
		 *
458
		 * @private
459
		 */
460
		_setBackgroundColour: function () {
461
			var wrapper = $('#content-wrapper');
462
			var albumDesign = Gallery.config.albumDesign;
463
			if (!$.isEmptyObject(albumDesign) && albumDesign.background) {
464
				wrapper.css('background-color', albumDesign.background);
465
			} else {
466
				wrapper.css('background-color', '#fff');
467
			}
468
		},
469
470
		/**
471
		 * Picks the image which matches the sort order
472
		 *
473
		 * @param {string} sortType name or date
474
		 * @param {string} sortOrder asc or des
475
		 * @param {boolean} active determines if we're setting up the active sort button
476
		 * @private
477
		 */
478
		_setSortButton: function (sortType, sortOrder, active) {
479
			var button = $('#sort-' + sortType + '-button');
480
			// Removing all the classes which control the image in the button
481
			button.removeClass('active-button');
482
			button.find('img').removeClass('front');
483
			button.find('img').removeClass('back');
484
485
			// We need to determine the reverse order in order to send that image to the back
486
			var reverseSortOrder = 'des';
487
			if (sortOrder === 'des') {
488
				reverseSortOrder = 'asc';
489
			}
490
491
			// We assign the proper order to the button images
492
			button.find('img.' + sortOrder).addClass('front');
493
			button.find('img.' + reverseSortOrder).addClass('back');
494
495
			// The active button needs a hover action for the flip effect
496
			if (active) {
497
				button.addClass('active-button');
498
				if (button.is(":hover")) {
499
					button.removeClass('hover');
500
				}
501
				// We can't use a toggle here
502
				button.hover(function () {
503
						$(this).addClass('hover');
504
					},
505
					function () {
506
						$(this).removeClass('hover');
507
					});
508
			}
509
		},
510
		
511
		/**
512
		 * If no url is entered then do not show the error box.
513
		 *
514
		 */
515
		_blankUrl: function() {
516
			$('#remote_address').on("change keyup paste", function() {
517
 				if ($(this).val() === '') {
518
 					$('#save-button-confirm').prop('disabled', true);
519
 				} else {
520
 					$('#save-button-confirm').prop('disabled', false);
521
 				}
522
			});
523
		},
524
		
525
		/**
526
		 * Creates the [+] button allowing users who can't drag and drop to upload files
527
		 *
528
		 * @see core/apps/files/js/filelist.js
529
		 * @private
530
		 */
531
		_renderNewButton: function () {
532
			// if no actions container exist, skip
533
			var $actionsContainer = $('.actions');
534
			if (!$actionsContainer.length) {
535
				return;
536
			}
537
			if (!this._addButtonTemplate) {
538
				this._addButtonTemplate = Handlebars.compile(TEMPLATE_ADDBUTTON);
539
			}
540
			var $newButton = $(this._addButtonTemplate({
541
				addText: t('gallery', 'New'),
542
				iconUrl: OC.imagePath('core', 'actions/add')
543
			}));
544
545
			$actionsContainer.prepend($newButton);
546
			$newButton.tooltip({'placement': 'bottom'});
547
548
			$newButton.click(_.bind(this._onClickNewButton, this));
549
			this._newButton = $newButton;
550
		},
551
552
		/**
553
		 * Creates the click handler for the [+] button
554
		 * @param event
555
		 * @returns {boolean}
556
		 *
557
		 * @see core/apps/files/js/filelist.js
558
		 * @private
559
		 */
560
		_onClickNewButton: function (event) {
561
			var $target = $(event.target);
562
			if (!$target.hasClass('.button')) {
563
				$target = $target.closest('.button');
564
			}
565
			this._newButton.tooltip('hide');
566
			event.preventDefault();
567
			if ($target.hasClass('disabled')) {
568
				return false;
569
			}
570
			if (!this._newFileMenu) {
571
				this._newFileMenu = new Gallery.NewFileMenu();
572
				$('body').append(this._newFileMenu.$el);
573
			}
574
			this._newFileMenu.showAt($target);
575
576
			if (Gallery.currentAlbum === '') {
577
				$('.menuitem[data-action="hideAlbum"]').parent().hide();
578
			}
579
			return false;
580
		}
581
	};
582
583
	Gallery.View = View;
584
})(jQuery, _, OC, t, Gallery);
585