1// Copyright (c) 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * WallpaperManager constructor. 7 * 8 * WallpaperManager objects encapsulate the functionality of the wallpaper 9 * manager extension. 10 * 11 * @constructor 12 * @param {HTMLElement} dialogDom The DOM node containing the prototypical 13 * extension UI. 14 */ 15 16function WallpaperManager(dialogDom) { 17 this.dialogDom_ = dialogDom; 18 this.document_ = dialogDom.ownerDocument; 19 this.enableOnlineWallpaper_ = loadTimeData.valueExists('manifestBaseURL'); 20 this.selectedCategory = null; 21 this.selectedItem_ = null; 22 this.progressManager_ = new ProgressManager(); 23 this.customWallpaperData_ = null; 24 this.currentWallpaper_ = null; 25 this.wallpaperRequest_ = null; 26 this.wallpaperDirs_ = WallpaperDirectories.getInstance(); 27 this.fetchManifest_(); 28} 29 30// Anonymous 'namespace'. 31// TODO(bshe): Get rid of anonymous namespace. 32(function() { 33 34 /** 35 * URL of the learn more page for wallpaper picker. 36 */ 37 /** @const */ var LearnMoreURL = 38 'https://support.google.com/chromeos/?p=wallpaper_fileerror&hl=' + 39 navigator.language; 40 41 /** 42 * Index of the All category. It is the first category in wallpaper picker. 43 */ 44 /** @const */ var AllCategoryIndex = 0; 45 46 /** 47 * Index offset of categories parsed from manifest. The All category is added 48 * before them. So the offset is 1. 49 */ 50 /** @const */ var OnlineCategoriesOffset = 1; 51 52 /** 53 * Returns a translated string. 54 * 55 * Wrapper function to make dealing with translated strings more concise. 56 * Equivilant to localStrings.getString(id). 57 * 58 * @param {string} id The id of the string to return. 59 * @return {string} The translated string. 60 */ 61 function str(id) { 62 return loadTimeData.getString(id); 63 } 64 65 /** 66 * Retruns the current selected layout. 67 * @return {string} The selected layout. 68 */ 69 function getSelectedLayout() { 70 var setWallpaperLayout = $('set-wallpaper-layout'); 71 return setWallpaperLayout.options[setWallpaperLayout.selectedIndex].value; 72 } 73 74 /** 75 * Loads translated strings. 76 */ 77 WallpaperManager.initStrings = function(callback) { 78 chrome.wallpaperPrivate.getStrings(function(strings) { 79 loadTimeData.data = strings; 80 if (callback) 81 callback(); 82 }); 83 }; 84 85 /** 86 * Requests wallpaper manifest file from server. 87 */ 88 WallpaperManager.prototype.fetchManifest_ = function() { 89 var locale = navigator.language; 90 if (!this.enableOnlineWallpaper_) { 91 this.initDom_(); 92 return; 93 } 94 95 var urls = [ 96 str('manifestBaseURL') + locale + '.json', 97 // Fallback url. Use 'en' locale by default. 98 str('manifestBaseURL') + 'en.json']; 99 100 var asyncFetchManifestFromUrls = function(urls, func, successCallback, 101 failureCallback) { 102 var index = 0; 103 var loop = { 104 next: function() { 105 if (index < urls.length) { 106 func(loop, urls[index]); 107 index++; 108 } else { 109 failureCallback(); 110 } 111 }, 112 113 success: function(response) { 114 successCallback(response); 115 }, 116 117 failure: function() { 118 failureCallback(); 119 } 120 }; 121 loop.next(); 122 }; 123 124 var fetchManifestAsync = function(loop, url) { 125 var xhr = new XMLHttpRequest(); 126 try { 127 xhr.addEventListener('loadend', function(e) { 128 if (this.status == 200 && this.responseText != null) { 129 try { 130 var manifest = JSON.parse(this.responseText); 131 loop.success(manifest); 132 } catch (e) { 133 loop.failure(); 134 } 135 } else { 136 loop.next(); 137 } 138 }); 139 xhr.open('GET', url, true); 140 xhr.send(null); 141 } catch (e) { 142 loop.failure(); 143 } 144 }; 145 146 if (navigator.onLine) { 147 asyncFetchManifestFromUrls(urls, fetchManifestAsync, 148 this.onLoadManifestSuccess_.bind(this), 149 this.onLoadManifestFailed_.bind(this)); 150 } else { 151 // If device is offline, fetches manifest from local storage. 152 // TODO(bshe): Always loading the offline manifest first and replacing 153 // with the online one when available. 154 this.onLoadManifestFailed_(); 155 } 156 }; 157 158 /** 159 * Shows error message in a centered dialog. 160 * @private 161 * @param {string} errroMessage The string to show in the error dialog. 162 */ 163 WallpaperManager.prototype.showError_ = function(errorMessage) { 164 document.querySelector('.error-message').textContent = errorMessage; 165 $('error-container').hidden = false; 166 }; 167 168 /** 169 * Sets manifest loaded from server. Called after manifest is successfully 170 * loaded. 171 * @param {object} manifest The parsed manifest file. 172 */ 173 WallpaperManager.prototype.onLoadManifestSuccess_ = function(manifest) { 174 this.manifest_ = manifest; 175 WallpaperUtil.saveToStorage(Constants.AccessManifestKey, manifest, false); 176 this.initDom_(); 177 }; 178 179 // Sets manifest to previously saved object if any and shows connection error. 180 // Called after manifest failed to load. 181 WallpaperManager.prototype.onLoadManifestFailed_ = function() { 182 var accessManifestKey = Constants.AccessManifestKey; 183 var self = this; 184 Constants.WallpaperLocalStorage.get(accessManifestKey, function(items) { 185 self.manifest_ = items[accessManifestKey] ? items[accessManifestKey] : {}; 186 self.showError_(str('connectionFailed')); 187 self.initDom_(); 188 $('wallpaper-grid').classList.add('image-picker-offline'); 189 }); 190 }; 191 192 /** 193 * Toggle surprise me feature of wallpaper picker. It fires an storage 194 * onChanged event. Event handler for that event is in event_page.js. 195 * @private 196 */ 197 WallpaperManager.prototype.toggleSurpriseMe_ = function() { 198 var checkbox = $('surprise-me').querySelector('#checkbox'); 199 var shouldEnable = !checkbox.classList.contains('checked'); 200 WallpaperUtil.saveToStorage(Constants.AccessSurpriseMeEnabledKey, 201 shouldEnable, false, function() { 202 if (chrome.runtime.lastError == null) { 203 if (shouldEnable) { 204 checkbox.classList.add('checked'); 205 } else { 206 checkbox.classList.remove('checked'); 207 } 208 $('categories-list').disabled = shouldEnable; 209 $('wallpaper-grid').disabled = shouldEnable; 210 } else { 211 // TODO(bshe): show error message to user. 212 console.error('Failed to save surprise me option to chrome storage.'); 213 } 214 }); 215 }; 216 217 /** 218 * One-time initialization of various DOM nodes. 219 */ 220 WallpaperManager.prototype.initDom_ = function() { 221 i18nTemplate.process(this.document_, loadTimeData); 222 this.initCategoriesList_(); 223 this.initThumbnailsGrid_(); 224 this.presetCategory_(); 225 226 $('file-selector').addEventListener( 227 'change', this.onFileSelectorChanged_.bind(this)); 228 $('set-wallpaper-layout').addEventListener( 229 'change', this.onWallpaperLayoutChanged_.bind(this)); 230 231 if (this.enableOnlineWallpaper_) { 232 var self = this; 233 $('surprise-me').hidden = false; 234 $('surprise-me').addEventListener('click', 235 this.toggleSurpriseMe_.bind(this)); 236 Constants.WallpaperLocalStorage.get(Constants.AccessSurpriseMeEnabledKey, 237 function(items) { 238 if (items[Constants.AccessSurpriseMeEnabledKey]) { 239 $('surprise-me').querySelector('#checkbox').classList.add('checked'); 240 $('categories-list').disabled = true; 241 $('wallpaper-grid').disabled = true; 242 } 243 }); 244 245 window.addEventListener('offline', function() { 246 chrome.wallpaperPrivate.getOfflineWallpaperList( 247 Constants.WallpaperSourceEnum.Online, function(lists) { 248 if (!self.downloadedListMap_) 249 self.downloadedListMap_ = {}; 250 for (var i = 0; i < lists.length; i++) { 251 self.downloadedListMap_[lists[i]] = true; 252 } 253 var thumbnails = self.document_.querySelectorAll('.thumbnail'); 254 for (var i = 0; i < thumbnails.length; i++) { 255 var thumbnail = thumbnails[i]; 256 var url = self.wallpaperGrid_.dataModel.item(i).baseURL; 257 var fileName = url.substring(url.lastIndexOf('/') + 1) + 258 Constants.HighResolutionSuffix; 259 if (self.downloadedListMap_ && 260 self.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) { 261 thumbnail.offline = true; 262 } 263 } 264 }); 265 $('wallpaper-grid').classList.add('image-picker-offline'); 266 }); 267 window.addEventListener('online', function() { 268 self.downloadedListMap_ = null; 269 $('wallpaper-grid').classList.remove('image-picker-offline'); 270 }); 271 } 272 $('window-close-button').addEventListener('click', function() { 273 window.close(); 274 }); 275 this.document_.defaultView.addEventListener( 276 'resize', this.onResize_.bind(this)); 277 $('learn-more').href = LearnMoreURL; 278 $('close-error').addEventListener('click', function() { 279 $('error-container').hidden = true; 280 }); 281 $('close-wallpaper-selection').addEventListener('click', function() { 282 $('wallpaper-selection-container').hidden = true; 283 $('set-wallpaper-layout').disabled = true; 284 }); 285 286 this.onResize_(); 287 this.initContextMenuAndCommand_(); 288 }; 289 290 /** 291 * One-time initialization of context menu and command. 292 */ 293 WallpaperManager.prototype.initContextMenuAndCommand_ = function() { 294 this.wallpaperContextMenu_ = $('wallpaper-context-menu'); 295 cr.ui.Menu.decorate(this.wallpaperContextMenu_); 296 cr.ui.contextMenuHandler.setContextMenu(this.wallpaperGrid_, 297 this.wallpaperContextMenu_); 298 var commands = this.dialogDom_.querySelectorAll('command'); 299 for (var i = 0; i < commands.length; i++) 300 cr.ui.Command.decorate(commands[i]); 301 302 var doc = this.document_; 303 doc.addEventListener('command', this.onCommand_.bind(this)); 304 doc.addEventListener('canExecute', this.onCommandCanExecute_.bind(this)); 305 }; 306 307 /** 308 * Handles a command being executed. 309 * @param {Event} event A command event. 310 */ 311 WallpaperManager.prototype.onCommand_ = function(event) { 312 if (event.command.id == 'delete') { 313 var wallpaperGrid = this.wallpaperGrid_; 314 var selectedIndex = wallpaperGrid.selectionModel.selectedIndex; 315 var item = wallpaperGrid.dataModel.item(selectedIndex); 316 if (!item || item.source != Constants.WallpaperSourceEnum.Custom) 317 return; 318 this.removeCustomWallpaper(item.baseURL); 319 wallpaperGrid.dataModel.splice(selectedIndex, 1); 320 // Calculate the number of remaining custom wallpapers. The add new button 321 // in data model needs to be excluded. 322 var customWallpaperCount = wallpaperGrid.dataModel.length - 1; 323 if (customWallpaperCount == 0) { 324 // Active custom wallpaper is also copied in chronos data dir. It needs 325 // to be deleted. 326 chrome.wallpaperPrivate.resetWallpaper(); 327 } else { 328 selectedIndex = Math.min(selectedIndex, customWallpaperCount - 1); 329 wallpaperGrid.selectionModel.selectedIndex = selectedIndex; 330 } 331 event.cancelBubble = true; 332 } 333 }; 334 335 /** 336 * Decides if a command can be executed on current target. 337 * @param {Event} event A command event. 338 */ 339 WallpaperManager.prototype.onCommandCanExecute_ = function(event) { 340 switch (event.command.id) { 341 case 'delete': 342 var wallpaperGrid = this.wallpaperGrid_; 343 var selectedIndex = wallpaperGrid.selectionModel.selectedIndex; 344 var item = wallpaperGrid.dataModel.item(selectedIndex); 345 if (selectedIndex != this.wallpaperGrid_.dataModel.length - 1 && 346 item && item.source == Constants.WallpaperSourceEnum.Custom) { 347 event.canExecute = true; 348 break; 349 } 350 default: 351 event.canExecute = false; 352 } 353 }; 354 355 /** 356 * Preset to the category which contains current wallpaper. 357 */ 358 WallpaperManager.prototype.presetCategory_ = function() { 359 this.currentWallpaper_ = str('currentWallpaper'); 360 // The currentWallpaper_ is either a url contains HightResolutionSuffix or a 361 // custom wallpaper file name converted from an integer value represent 362 // time (e.g., 13006377367586070). 363 if (!this.enableOnlineWallpaper_ || (this.currentWallpaper_ && 364 this.currentWallpaper_.indexOf(Constants.HighResolutionSuffix) == -1)) { 365 // Custom is the last one in the categories list. 366 this.categoriesList_.selectionModel.selectedIndex = 367 this.categoriesList_.dataModel.length - 1; 368 return; 369 } 370 var self = this; 371 var presetCategoryInner_ = function() { 372 // Selects the first category in the categories list of current 373 // wallpaper as the default selected category when showing wallpaper 374 // picker UI. 375 var presetCategory = AllCategoryIndex; 376 if (self.currentWallpaper_) { 377 for (var key in self.manifest_.wallpaper_list) { 378 var url = self.manifest_.wallpaper_list[key].base_url + 379 Constants.HighResolutionSuffix; 380 if (url.indexOf(self.currentWallpaper_) != -1 && 381 self.manifest_.wallpaper_list[key].categories.length > 0) { 382 presetCategory = self.manifest_.wallpaper_list[key].categories[0] + 383 OnlineCategoriesOffset; 384 break; 385 } 386 } 387 } 388 self.categoriesList_.selectionModel.selectedIndex = presetCategory; 389 }; 390 if (navigator.onLine) { 391 presetCategoryInner_(); 392 } else { 393 // If device is offline, gets the available offline wallpaper list first. 394 // Wallpapers which are not in the list will display a grayscaled 395 // thumbnail. 396 chrome.wallpaperPrivate.getOfflineWallpaperList( 397 Constants.WallpaperSourceEnum.Online, function(lists) { 398 if (!self.downloadedListMap_) 399 self.downloadedListMap_ = {}; 400 for (var i = 0; i < lists.length; i++) 401 self.downloadedListMap_[lists[i]] = true; 402 presetCategoryInner_(); 403 }); 404 } 405 }; 406 407 /** 408 * Constructs the thumbnails grid. 409 */ 410 WallpaperManager.prototype.initThumbnailsGrid_ = function() { 411 this.wallpaperGrid_ = $('wallpaper-grid'); 412 wallpapers.WallpaperThumbnailsGrid.decorate(this.wallpaperGrid_); 413 this.wallpaperGrid_.autoExpands = true; 414 415 this.wallpaperGrid_.addEventListener('change', this.onChange_.bind(this)); 416 this.wallpaperGrid_.addEventListener('dblclick', this.onClose_.bind(this)); 417 }; 418 419 /** 420 * Handles change event dispatched by wallpaper grid. 421 */ 422 WallpaperManager.prototype.onChange_ = function() { 423 // splice may dispatch a change event because the position of selected 424 // element changing. But the actual selected element may not change after 425 // splice. Check if the new selected element equals to the previous selected 426 // element before continuing. Otherwise, wallpaper may reset to previous one 427 // as described in http://crbug.com/229036. 428 if (this.selectedItem_ == this.wallpaperGrid_.selectedItem) 429 return; 430 this.selectedItem_ = this.wallpaperGrid_.selectedItem; 431 this.onSelectedItemChanged_(); 432 }; 433 434 /** 435 * Closes window if no pending wallpaper request. 436 */ 437 WallpaperManager.prototype.onClose_ = function() { 438 if (this.wallpaperRequest_) { 439 this.wallpaperRequest_.addEventListener('loadend', function() { 440 // Close window on wallpaper loading finished. 441 window.close(); 442 }); 443 } else { 444 window.close(); 445 } 446 }; 447 448 /** 449 * Sets wallpaper to the corresponding wallpaper of selected thumbnail. 450 * @param {{baseURL: string, layout: string, source: string, 451 * availableOffline: boolean, opt_dynamicURL: string, 452 * opt_author: string, opt_authorWebsite: string}} 453 * selectedItem the selected item in WallpaperThumbnailsGrid's data 454 * model. 455 */ 456 WallpaperManager.prototype.setSelectedWallpaper_ = function(selectedItem) { 457 var self = this; 458 switch (selectedItem.source) { 459 case Constants.WallpaperSourceEnum.Custom: 460 var errorHandler = this.onFileSystemError_.bind(this); 461 var setActive = function() { 462 self.wallpaperGrid_.activeItem = selectedItem; 463 self.currentWallpaper_ = selectedItem.baseURL; 464 }; 465 var success = function(dirEntry) { 466 dirEntry.getFile(selectedItem.baseURL, {create: false}, 467 function(fileEntry) { 468 fileEntry.file(function(file) { 469 var reader = new FileReader(); 470 reader.readAsArrayBuffer(file); 471 reader.addEventListener('error', errorHandler); 472 reader.addEventListener('load', function(e) { 473 self.setCustomWallpaper(e.target.result, 474 selectedItem.layout, 475 false, selectedItem.baseURL, 476 setActive, errorHandler); 477 }); 478 }, errorHandler); 479 }, errorHandler); 480 } 481 this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, 482 success, errorHandler); 483 break; 484 case Constants.WallpaperSourceEnum.Online: 485 var wallpaperURL = selectedItem.baseURL + 486 Constants.HighResolutionSuffix; 487 var selectedGridItem = this.wallpaperGrid_.getListItem(selectedItem); 488 489 chrome.wallpaperPrivate.setWallpaperIfExists(wallpaperURL, 490 selectedItem.layout, 491 selectedItem.source, 492 function(exists) { 493 if (exists) { 494 self.currentWallpaper_ = wallpaperURL; 495 self.wallpaperGrid_.activeItem = selectedItem; 496 WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout, 497 selectedItem.source); 498 return; 499 } 500 501 // Falls back to request wallpaper from server. 502 if (self.wallpaperRequest_) 503 self.wallpaperRequest_.abort(); 504 505 self.wallpaperRequest_ = new XMLHttpRequest(); 506 self.progressManager_.reset(self.wallpaperRequest_, selectedGridItem); 507 508 var onSuccess = function(xhr) { 509 var image = xhr.response; 510 chrome.wallpaperPrivate.setWallpaper(image, selectedItem.layout, 511 wallpaperURL, 512 self.onFinished_.bind(self, selectedGridItem, selectedItem)); 513 self.currentWallpaper_ = wallpaperURL; 514 WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout, 515 selectedItem.source); 516 self.wallpaperRequest_ = null; 517 }; 518 var onFailure = function() { 519 self.progressManager_.hideProgressBar(selectedGridItem); 520 self.showError_(str('downloadFailed')); 521 self.wallpaperRequest_ = null; 522 }; 523 WallpaperUtil.fetchURL(wallpaperURL, 'arraybuffer', onSuccess, 524 onFailure, self.wallpaperRequest_); 525 }); 526 break; 527 default: 528 console.error('Unsupported wallpaper source.'); 529 } 530 }; 531 532 /* 533 * Removes the oldest custom wallpaper. If the oldest one is set as current 534 * wallpaper, removes the second oldest one to free some space. This should 535 * only be called when exceeding wallpaper quota. 536 */ 537 WallpaperManager.prototype.removeOldestWallpaper_ = function() { 538 // Custom wallpapers should already sorted when put to the data model. The 539 // last element is the add new button, need to exclude it as well. 540 var oldestIndex = this.wallpaperGrid_.dataModel.length - 2; 541 var item = this.wallpaperGrid_.dataModel.item(oldestIndex); 542 if (!item || item.source != Constants.WallpaperSourceEnum.Custom) 543 return; 544 if (item.baseURL == this.currentWallpaper_) 545 item = this.wallpaperGrid_.dataModel.item(--oldestIndex); 546 if (item) { 547 this.removeCustomWallpaper(item.baseURL); 548 this.wallpaperGrid_.dataModel.splice(oldestIndex, 1); 549 } 550 }; 551 552 /* 553 * Shows an error message to user and log the failed reason in console. 554 */ 555 WallpaperManager.prototype.onFileSystemError_ = function(e) { 556 var msg = ''; 557 switch (e.code) { 558 case FileError.QUOTA_EXCEEDED_ERR: 559 msg = 'QUOTA_EXCEEDED_ERR'; 560 // Instead of simply remove oldest wallpaper, we should consider a 561 // better way to handle this situation. See crbug.com/180890. 562 this.removeOldestWallpaper_(); 563 break; 564 case FileError.NOT_FOUND_ERR: 565 msg = 'NOT_FOUND_ERR'; 566 break; 567 case FileError.SECURITY_ERR: 568 msg = 'SECURITY_ERR'; 569 break; 570 case FileError.INVALID_MODIFICATION_ERR: 571 msg = 'INVALID_MODIFICATION_ERR'; 572 break; 573 case FileError.INVALID_STATE_ERR: 574 msg = 'INVALID_STATE_ERR'; 575 break; 576 default: 577 msg = 'Unknown Error'; 578 break; 579 } 580 console.error('Error: ' + msg); 581 this.showError_(str('accessFileFailure')); 582 }; 583 584 /** 585 * Handles changing of selectedItem in wallpaper manager. 586 */ 587 WallpaperManager.prototype.onSelectedItemChanged_ = function() { 588 this.setWallpaperAttribution_(this.selectedItem_); 589 590 if (!this.selectedItem_ || this.selectedItem_.source == 'ADDNEW') 591 return; 592 593 if (this.selectedItem_.baseURL && !this.wallpaperGrid_.inProgramSelection) { 594 if (this.selectedItem_.source == Constants.WallpaperSourceEnum.Custom) { 595 var items = {}; 596 var key = this.selectedItem_.baseURL; 597 var self = this; 598 Constants.WallpaperLocalStorage.get(key, function(items) { 599 self.selectedItem_.layout = 600 items[key] ? items[key] : 'CENTER_CROPPED'; 601 self.setSelectedWallpaper_(self.selectedItem_); 602 }); 603 } else { 604 this.setSelectedWallpaper_(this.selectedItem_); 605 } 606 } 607 }; 608 609 /** 610 * Set attributions of wallpaper with given URL. If URL is not valid, clear 611 * the attributions. 612 * @param {{baseURL: string, dynamicURL: string, layout: string, 613 * author: string, authorWebsite: string, availableOffline: boolean}} 614 * selectedItem selected wallpaper item in grid. 615 * @private 616 */ 617 WallpaperManager.prototype.setWallpaperAttribution_ = function(selectedItem) { 618 if (selectedItem) { 619 $('author-name').textContent = selectedItem.author; 620 $('author-website').textContent = $('author-website').href = 621 selectedItem.authorWebsite; 622 chrome.wallpaperPrivate.getThumbnail(selectedItem.baseURL, 623 selectedItem.source, 624 function(data) { 625 var img = $('attribute-image'); 626 if (data) { 627 var blob = new Blob([new Int8Array(data)], {'type' : 'image\/png'}); 628 img.src = window.URL.createObjectURL(blob); 629 img.addEventListener('load', function(e) { 630 window.URL.revokeObjectURL(this.src); 631 }); 632 } else { 633 img.src = ''; 634 } 635 }); 636 $('wallpaper-attribute').hidden = false; 637 $('attribute-image').hidden = false; 638 return; 639 } 640 $('wallpaper-attribute').hidden = true; 641 $('attribute-image').hidden = true; 642 $('author-name').textContent = ''; 643 $('author-website').textContent = $('author-website').href = ''; 644 $('attribute-image').src = ''; 645 }; 646 647 /** 648 * Resize thumbnails grid and categories list to fit the new window size. 649 */ 650 WallpaperManager.prototype.onResize_ = function() { 651 this.wallpaperGrid_.redraw(); 652 this.categoriesList_.redraw(); 653 }; 654 655 /** 656 * Constructs the categories list. 657 */ 658 WallpaperManager.prototype.initCategoriesList_ = function() { 659 this.categoriesList_ = $('categories-list'); 660 cr.ui.List.decorate(this.categoriesList_); 661 // cr.ui.list calculates items in view port based on client height and item 662 // height. However, categories list is displayed horizontally. So we should 663 // not calculate visible items here. Sets autoExpands to true to show every 664 // item in the list. 665 // TODO(bshe): Use ul to replace cr.ui.list for category list. 666 this.categoriesList_.autoExpands = true; 667 668 var self = this; 669 this.categoriesList_.itemConstructor = function(entry) { 670 return self.renderCategory_(entry); 671 }; 672 673 this.categoriesList_.selectionModel = new cr.ui.ListSingleSelectionModel(); 674 this.categoriesList_.selectionModel.addEventListener( 675 'change', this.onCategoriesChange_.bind(this)); 676 677 var categoriesDataModel = new cr.ui.ArrayDataModel([]); 678 if (this.enableOnlineWallpaper_) { 679 // Adds all category as first category. 680 categoriesDataModel.push(str('allCategoryLabel')); 681 for (var key in this.manifest_.categories) { 682 categoriesDataModel.push(this.manifest_.categories[key]); 683 } 684 } 685 // Adds custom category as last category. 686 categoriesDataModel.push(str('customCategoryLabel')); 687 this.categoriesList_.dataModel = categoriesDataModel; 688 }; 689 690 /** 691 * Constructs the element in categories list. 692 * @param {string} entry Text content of a category. 693 */ 694 WallpaperManager.prototype.renderCategory_ = function(entry) { 695 var li = this.document_.createElement('li'); 696 cr.defineProperty(li, 'custom', cr.PropertyKind.BOOL_ATTR); 697 li.custom = (entry == str('customCategoryLabel')); 698 cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR); 699 cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR); 700 var div = this.document_.createElement('div'); 701 div.textContent = entry; 702 li.appendChild(div); 703 return li; 704 }; 705 706 /** 707 * Handles the custom wallpaper which user selected from file manager. Called 708 * when users select a file. 709 */ 710 WallpaperManager.prototype.onFileSelectorChanged_ = function() { 711 var files = $('file-selector').files; 712 if (files.length != 1) 713 console.error('More than one files are selected or no file selected'); 714 if (!files[0].type.match('image/jpeg') && 715 !files[0].type.match('image/png')) { 716 this.showError_(str('invalidWallpaper')); 717 return; 718 } 719 var layout = getSelectedLayout(); 720 var self = this; 721 var errorHandler = this.onFileSystemError_.bind(this); 722 var setSelectedFile = function(file, layout, fileName) { 723 var saveThumbnail = function(thumbnail) { 724 var success = function(dirEntry) { 725 dirEntry.getFile(fileName, {create: true}, function(fileEntry) { 726 fileEntry.createWriter(function(fileWriter) { 727 fileWriter.onwriteend = function(e) { 728 $('set-wallpaper-layout').disabled = false; 729 var wallpaperInfo = { 730 baseURL: fileName, 731 layout: layout, 732 source: Constants.WallpaperSourceEnum.Custom, 733 availableOffline: true 734 }; 735 self.wallpaperGrid_.dataModel.splice(0, 0, wallpaperInfo); 736 self.wallpaperGrid_.selectedItem = wallpaperInfo; 737 self.wallpaperGrid_.activeItem = wallpaperInfo; 738 self.currentWallpaper_ = fileName; 739 WallpaperUtil.saveToStorage(self.currentWallpaper_, layout, 740 false); 741 }; 742 743 fileWriter.onerror = errorHandler; 744 745 var blob = new Blob([new Int8Array(thumbnail)], 746 {'type' : 'image\/jpeg'}); 747 fileWriter.write(blob); 748 }, errorHandler); 749 }, errorHandler); 750 }; 751 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, 752 success, errorHandler); 753 }; 754 755 var success = function(dirEntry) { 756 dirEntry.getFile(fileName, {create: true}, function(fileEntry) { 757 fileEntry.createWriter(function(fileWriter) { 758 fileWriter.addEventListener('writeend', function(e) { 759 var reader = new FileReader(); 760 reader.readAsArrayBuffer(file); 761 reader.addEventListener('error', errorHandler); 762 reader.addEventListener('load', function(e) { 763 self.setCustomWallpaper(e.target.result, layout, true, fileName, 764 saveThumbnail, function() { 765 self.removeCustomWallpaper(fileName); 766 errorHandler(); 767 }); 768 }); 769 }); 770 771 fileWriter.addEventListener('error', errorHandler); 772 fileWriter.write(file); 773 }, errorHandler); 774 }, errorHandler); 775 }; 776 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success, 777 errorHandler); 778 }; 779 setSelectedFile(files[0], layout, new Date().getTime().toString()); 780 }; 781 782 /** 783 * Removes wallpaper and thumbnail with fileName from FileSystem. 784 * @param {string} fileName The file name of wallpaper and thumbnail to be 785 * removed. 786 */ 787 WallpaperManager.prototype.removeCustomWallpaper = function(fileName) { 788 var errorHandler = this.onFileSystemError_.bind(this); 789 var self = this; 790 var removeFile = function(fileName) { 791 var success = function(dirEntry) { 792 dirEntry.getFile(fileName, {create: false}, function(fileEntry) { 793 fileEntry.remove(function() { 794 }, errorHandler); 795 }, errorHandler); 796 } 797 798 // Removes copy of original. 799 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success, 800 errorHandler); 801 802 // Removes generated thumbnail. 803 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, success, 804 errorHandler); 805 }; 806 removeFile(fileName); 807 }; 808 809 /** 810 * Sets current wallpaper and generate thumbnail if generateThumbnail is true. 811 * @param {ArrayBuffer} wallpaper The binary representation of wallpaper. 812 * @param {string} layout The user selected wallpaper layout. 813 * @param {boolean} generateThumbnail True if need to generate thumbnail. 814 * @param {string} fileName The unique file name of wallpaper. 815 * @param {function(thumbnail):void} success Success callback. If 816 * generateThumbnail is true, the callback parameter should have the 817 * generated thumbnail. 818 * @param {function(e):void} failure Failure callback. Called when there is an 819 * error from FileSystem. 820 */ 821 WallpaperManager.prototype.setCustomWallpaper = function(wallpaper, 822 layout, 823 generateThumbnail, 824 fileName, 825 success, 826 failure) { 827 var self = this; 828 var onFinished = function(opt_thumbnail) { 829 if (chrome.runtime.lastError != undefined) { 830 self.showError_(chrome.runtime.lastError.message); 831 $('set-wallpaper-layout').disabled = true; 832 failure(); 833 } else { 834 success(opt_thumbnail); 835 // Custom wallpapers are not synced yet. If login on a different 836 // computer after set a custom wallpaper, wallpaper wont change by sync. 837 WallpaperUtil.saveWallpaperInfo(fileName, layout, 838 Constants.WallpaperSourceEnum.Custom); 839 } 840 }; 841 842 chrome.wallpaperPrivate.setCustomWallpaper(wallpaper, layout, 843 generateThumbnail, 844 fileName, onFinished); 845 }; 846 847 /** 848 * Sets wallpaper finished. Displays error message if any. 849 * @param {WallpaperThumbnailsGridItem=} opt_selectedGridItem The wallpaper 850 * thumbnail grid item. It extends from cr.ui.ListItem. 851 * @param {{baseURL: string, layout: string, source: string, 852 * availableOffline: boolean, opt_dynamicURL: string, 853 * opt_author: string, opt_authorWebsite: string}=} 854 * opt_selectedItem the selected item in WallpaperThumbnailsGrid's data 855 * model. 856 */ 857 WallpaperManager.prototype.onFinished_ = function(opt_selectedGridItem, 858 opt_selectedItem) { 859 if (opt_selectedGridItem) 860 this.progressManager_.hideProgressBar(opt_selectedGridItem); 861 862 if (chrome.runtime.lastError != undefined) { 863 this.showError_(chrome.runtime.lastError.message); 864 } else if (opt_selectedItem) { 865 this.wallpaperGrid_.activeItem = opt_selectedItem; 866 } 867 }; 868 869 /** 870 * Handles the layout setting change of custom wallpaper. 871 */ 872 WallpaperManager.prototype.onWallpaperLayoutChanged_ = function() { 873 var layout = getSelectedLayout(); 874 var self = this; 875 chrome.wallpaperPrivate.setCustomWallpaperLayout(layout, function() { 876 if (chrome.runtime.lastError != undefined) { 877 self.showError_(chrome.runtime.lastError.message); 878 self.removeCustomWallpaper(fileName); 879 $('set-wallpaper-layout').disabled = true; 880 } else { 881 WallpaperUtil.saveToStorage(self.currentWallpaper_, layout, false); 882 } 883 }); 884 }; 885 886 /** 887 * Handles user clicking on a different category. 888 */ 889 WallpaperManager.prototype.onCategoriesChange_ = function() { 890 var categoriesList = this.categoriesList_; 891 var selectedIndex = categoriesList.selectionModel.selectedIndex; 892 if (selectedIndex == -1) 893 return; 894 var selectedListItem = categoriesList.getListItemByIndex(selectedIndex); 895 var bar = $('bar'); 896 bar.style.left = selectedListItem.offsetLeft + 'px'; 897 bar.style.width = selectedListItem.offsetWidth + 'px'; 898 899 var wallpapersDataModel = new cr.ui.ArrayDataModel([]); 900 var selectedItem; 901 if (selectedListItem.custom) { 902 this.document_.body.setAttribute('custom', ''); 903 var errorHandler = this.onFileSystemError_.bind(this); 904 var toArray = function(list) { 905 return Array.prototype.slice.call(list || [], 0); 906 } 907 908 var self = this; 909 var processResults = function(entries) { 910 for (var i = 0; i < entries.length; i++) { 911 var entry = entries[i]; 912 var wallpaperInfo = { 913 baseURL: entry.name, 914 // The layout will be replaced by the actual value saved in 915 // local storage when requested later. Layout is not important 916 // for constructing thumbnails grid, we use CENTER_CROPPED here 917 // to speed up the process of constructing. So we do not need to 918 // wait for fetching correct layout. 919 layout: 'CENTER_CROPPED', 920 source: Constants.WallpaperSourceEnum.Custom, 921 availableOffline: true 922 }; 923 if (self.currentWallpaper_ == entry.name) 924 selectedItem = wallpaperInfo; 925 wallpapersDataModel.push(wallpaperInfo); 926 } 927 var lastElement = { 928 baseURL: '', 929 layout: '', 930 source: 'ADDNEW', 931 availableOffline: true 932 }; 933 wallpapersDataModel.push(lastElement); 934 self.wallpaperGrid_.dataModel = wallpapersDataModel; 935 self.wallpaperGrid_.selectedItem = selectedItem; 936 self.wallpaperGrid_.activeItem = selectedItem; 937 } 938 939 var success = function(dirEntry) { 940 var dirReader = dirEntry.createReader(); 941 var entries = []; 942 // All of a directory's entries are not guaranteed to return in a single 943 // call. 944 var readEntries = function() { 945 dirReader.readEntries(function(results) { 946 if (!results.length) { 947 processResults(entries.sort()); 948 } else { 949 entries = entries.concat(toArray(results)); 950 readEntries(); 951 } 952 }, errorHandler); 953 }; 954 readEntries(); // Start reading dirs. 955 } 956 this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, 957 success, errorHandler); 958 } else { 959 this.document_.body.removeAttribute('custom'); 960 for (var key in this.manifest_.wallpaper_list) { 961 if (selectedIndex == AllCategoryIndex || 962 this.manifest_.wallpaper_list[key].categories.indexOf( 963 selectedIndex - OnlineCategoriesOffset) != -1) { 964 var wallpaperInfo = { 965 baseURL: this.manifest_.wallpaper_list[key].base_url, 966 layout: this.manifest_.wallpaper_list[key].default_layout, 967 source: Constants.WallpaperSourceEnum.Online, 968 availableOffline: false, 969 author: this.manifest_.wallpaper_list[key].author, 970 authorWebsite: this.manifest_.wallpaper_list[key].author_website, 971 dynamicURL: this.manifest_.wallpaper_list[key].dynamic_url 972 }; 973 var startIndex = wallpaperInfo.baseURL.lastIndexOf('/') + 1; 974 var fileName = wallpaperInfo.baseURL.substring(startIndex) + 975 Constants.HighResolutionSuffix; 976 if (this.downloadedListMap_ && 977 this.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) { 978 wallpaperInfo.availableOffline = true; 979 } 980 wallpapersDataModel.push(wallpaperInfo); 981 var url = this.manifest_.wallpaper_list[key].base_url + 982 Constants.HighResolutionSuffix; 983 if (url == this.currentWallpaper_) { 984 selectedItem = wallpaperInfo; 985 } 986 } 987 } 988 this.wallpaperGrid_.dataModel = wallpapersDataModel; 989 this.wallpaperGrid_.selectedItem = selectedItem; 990 this.wallpaperGrid_.activeItem = selectedItem; 991 } 992 }; 993 994})(); 995