wallpaper_manager.js revision 8bcbed890bc3ce4d7a057a8f32cab53fa534672e
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(function(lists) { 247 if (!self.downloadedListMap_) 248 self.downloadedListMap_ = {}; 249 for (var i = 0; i < lists.length; i++) { 250 self.downloadedListMap_[lists[i]] = true; 251 } 252 var thumbnails = self.document_.querySelectorAll('.thumbnail'); 253 for (var i = 0; i < thumbnails.length; i++) { 254 var thumbnail = thumbnails[i]; 255 var url = self.wallpaperGrid_.dataModel.item(i).baseURL; 256 var fileName = url.substring(url.lastIndexOf('/') + 1) + 257 Constants.HighResolutionSuffix; 258 if (self.downloadedListMap_ && 259 self.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) { 260 thumbnail.offline = true; 261 } 262 } 263 }); 264 $('wallpaper-grid').classList.add('image-picker-offline'); 265 }); 266 window.addEventListener('online', function() { 267 self.downloadedListMap_ = null; 268 $('wallpaper-grid').classList.remove('image-picker-offline'); 269 }); 270 } 271 $('window-close-button').addEventListener('click', function() { 272 window.close(); 273 }); 274 this.document_.defaultView.addEventListener( 275 'resize', this.onResize_.bind(this)); 276 this.document_.defaultView.addEventListener( 277 'keydown', this.onKeyDown_.bind(this)); 278 $('learn-more').href = LearnMoreURL; 279 $('close-error').addEventListener('click', function() { 280 $('error-container').hidden = true; 281 }); 282 $('close-wallpaper-selection').addEventListener('click', function() { 283 $('wallpaper-selection-container').hidden = true; 284 $('set-wallpaper-layout').disabled = true; 285 }); 286 287 this.onResize_(); 288 this.initContextMenuAndCommand_(); 289 }; 290 291 /** 292 * One-time initialization of context menu and command. 293 */ 294 WallpaperManager.prototype.initContextMenuAndCommand_ = function() { 295 this.wallpaperContextMenu_ = $('wallpaper-context-menu'); 296 cr.ui.Menu.decorate(this.wallpaperContextMenu_); 297 cr.ui.contextMenuHandler.setContextMenu(this.wallpaperGrid_, 298 this.wallpaperContextMenu_); 299 var commands = this.dialogDom_.querySelectorAll('command'); 300 for (var i = 0; i < commands.length; i++) 301 cr.ui.Command.decorate(commands[i]); 302 303 var doc = this.document_; 304 doc.addEventListener('command', this.onCommand_.bind(this)); 305 doc.addEventListener('canExecute', this.onCommandCanExecute_.bind(this)); 306 }; 307 308 /** 309 * Handles a command being executed. 310 * @param {Event} event A command event. 311 */ 312 WallpaperManager.prototype.onCommand_ = function(event) { 313 if (event.command.id == 'delete') { 314 var wallpaperGrid = this.wallpaperGrid_; 315 var selectedIndex = wallpaperGrid.selectionModel.selectedIndex; 316 var item = wallpaperGrid.dataModel.item(selectedIndex); 317 if (!item || item.source != Constants.WallpaperSourceEnum.Custom) 318 return; 319 this.removeCustomWallpaper(item.baseURL); 320 wallpaperGrid.dataModel.splice(selectedIndex, 1); 321 // Calculate the number of remaining custom wallpapers. The add new button 322 // in data model needs to be excluded. 323 var customWallpaperCount = wallpaperGrid.dataModel.length - 1; 324 if (customWallpaperCount == 0) { 325 // Active custom wallpaper is also copied in chronos data dir. It needs 326 // to be deleted. 327 chrome.wallpaperPrivate.resetWallpaper(); 328 } else { 329 selectedIndex = Math.min(selectedIndex, customWallpaperCount - 1); 330 wallpaperGrid.selectionModel.selectedIndex = selectedIndex; 331 } 332 event.cancelBubble = true; 333 } 334 }; 335 336 /** 337 * Decides if a command can be executed on current target. 338 * @param {Event} event A command event. 339 */ 340 WallpaperManager.prototype.onCommandCanExecute_ = function(event) { 341 switch (event.command.id) { 342 case 'delete': 343 var wallpaperGrid = this.wallpaperGrid_; 344 var selectedIndex = wallpaperGrid.selectionModel.selectedIndex; 345 var item = wallpaperGrid.dataModel.item(selectedIndex); 346 if (selectedIndex != this.wallpaperGrid_.dataModel.length - 1 && 347 item && item.source == Constants.WallpaperSourceEnum.Custom) { 348 event.canExecute = true; 349 break; 350 } 351 default: 352 event.canExecute = false; 353 } 354 }; 355 356 /** 357 * Preset to the category which contains current wallpaper. 358 */ 359 WallpaperManager.prototype.presetCategory_ = function() { 360 this.currentWallpaper_ = str('currentWallpaper'); 361 // The currentWallpaper_ is either a url contains HightResolutionSuffix or a 362 // custom wallpaper file name converted from an integer value represent 363 // time (e.g., 13006377367586070). 364 if (!this.enableOnlineWallpaper_ || (this.currentWallpaper_ && 365 this.currentWallpaper_.indexOf(Constants.HighResolutionSuffix) == -1)) { 366 // Custom is the last one in the categories list. 367 this.categoriesList_.selectionModel.selectedIndex = 368 this.categoriesList_.dataModel.length - 1; 369 return; 370 } 371 var self = this; 372 var presetCategoryInner_ = function() { 373 // Selects the first category in the categories list of current 374 // wallpaper as the default selected category when showing wallpaper 375 // picker UI. 376 var presetCategory = AllCategoryIndex; 377 if (self.currentWallpaper_) { 378 for (var key in self.manifest_.wallpaper_list) { 379 var url = self.manifest_.wallpaper_list[key].base_url + 380 Constants.HighResolutionSuffix; 381 if (url.indexOf(self.currentWallpaper_) != -1 && 382 self.manifest_.wallpaper_list[key].categories.length > 0) { 383 presetCategory = self.manifest_.wallpaper_list[key].categories[0] + 384 OnlineCategoriesOffset; 385 break; 386 } 387 } 388 } 389 self.categoriesList_.selectionModel.selectedIndex = presetCategory; 390 }; 391 if (navigator.onLine) { 392 presetCategoryInner_(); 393 } else { 394 // If device is offline, gets the available offline wallpaper list first. 395 // Wallpapers which are not in the list will display a grayscaled 396 // thumbnail. 397 chrome.wallpaperPrivate.getOfflineWallpaperList(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 function(exists) { 492 if (exists) { 493 self.currentWallpaper_ = wallpaperURL; 494 self.wallpaperGrid_.activeItem = selectedItem; 495 WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout, 496 selectedItem.source); 497 return; 498 } 499 500 // Falls back to request wallpaper from server. 501 if (self.wallpaperRequest_) 502 self.wallpaperRequest_.abort(); 503 504 self.wallpaperRequest_ = new XMLHttpRequest(); 505 self.progressManager_.reset(self.wallpaperRequest_, selectedGridItem); 506 507 var onSuccess = function(xhr) { 508 var image = xhr.response; 509 chrome.wallpaperPrivate.setWallpaper(image, selectedItem.layout, 510 wallpaperURL, 511 self.onFinished_.bind(self, selectedGridItem, selectedItem)); 512 self.currentWallpaper_ = wallpaperURL; 513 WallpaperUtil.saveWallpaperInfo(wallpaperURL, selectedItem.layout, 514 selectedItem.source); 515 self.wallpaperRequest_ = null; 516 }; 517 var onFailure = function() { 518 self.progressManager_.hideProgressBar(selectedGridItem); 519 self.showError_(str('downloadFailed')); 520 self.wallpaperRequest_ = null; 521 }; 522 WallpaperUtil.fetchURL(wallpaperURL, 'arraybuffer', onSuccess, 523 onFailure, self.wallpaperRequest_); 524 }); 525 break; 526 default: 527 console.error('Unsupported wallpaper source.'); 528 } 529 }; 530 531 /* 532 * Removes the oldest custom wallpaper. If the oldest one is set as current 533 * wallpaper, removes the second oldest one to free some space. This should 534 * only be called when exceeding wallpaper quota. 535 */ 536 WallpaperManager.prototype.removeOldestWallpaper_ = function() { 537 // Custom wallpapers should already sorted when put to the data model. The 538 // last element is the add new button, need to exclude it as well. 539 var oldestIndex = this.wallpaperGrid_.dataModel.length - 2; 540 var item = this.wallpaperGrid_.dataModel.item(oldestIndex); 541 if (!item || item.source != Constants.WallpaperSourceEnum.Custom) 542 return; 543 if (item.baseURL == this.currentWallpaper_) 544 item = this.wallpaperGrid_.dataModel.item(--oldestIndex); 545 if (item) { 546 this.removeCustomWallpaper(item.baseURL); 547 this.wallpaperGrid_.dataModel.splice(oldestIndex, 1); 548 } 549 }; 550 551 /* 552 * Shows an error message to user and log the failed reason in console. 553 */ 554 WallpaperManager.prototype.onFileSystemError_ = function(e) { 555 var msg = ''; 556 switch (e.code) { 557 case FileError.QUOTA_EXCEEDED_ERR: 558 msg = 'QUOTA_EXCEEDED_ERR'; 559 // Instead of simply remove oldest wallpaper, we should consider a 560 // better way to handle this situation. See crbug.com/180890. 561 this.removeOldestWallpaper_(); 562 break; 563 case FileError.NOT_FOUND_ERR: 564 msg = 'NOT_FOUND_ERR'; 565 break; 566 case FileError.SECURITY_ERR: 567 msg = 'SECURITY_ERR'; 568 break; 569 case FileError.INVALID_MODIFICATION_ERR: 570 msg = 'INVALID_MODIFICATION_ERR'; 571 break; 572 case FileError.INVALID_STATE_ERR: 573 msg = 'INVALID_STATE_ERR'; 574 break; 575 default: 576 msg = 'Unknown Error'; 577 break; 578 } 579 console.error('Error: ' + msg); 580 this.showError_(str('accessFileFailure')); 581 }; 582 583 /** 584 * Handles changing of selectedItem in wallpaper manager. 585 */ 586 WallpaperManager.prototype.onSelectedItemChanged_ = function() { 587 this.setWallpaperAttribution_(this.selectedItem_); 588 589 if (!this.selectedItem_ || this.selectedItem_.source == 'ADDNEW') 590 return; 591 592 if (this.selectedItem_.baseURL && !this.wallpaperGrid_.inProgramSelection) { 593 if (this.selectedItem_.source == Constants.WallpaperSourceEnum.Custom) { 594 var items = {}; 595 var key = this.selectedItem_.baseURL; 596 var self = this; 597 Constants.WallpaperLocalStorage.get(key, function(items) { 598 self.selectedItem_.layout = 599 items[key] ? items[key] : 'CENTER_CROPPED'; 600 self.setSelectedWallpaper_(self.selectedItem_); 601 }); 602 } else { 603 this.setSelectedWallpaper_(this.selectedItem_); 604 } 605 } 606 }; 607 608 /** 609 * Set attributions of wallpaper with given URL. If URL is not valid, clear 610 * the attributions. 611 * @param {{baseURL: string, dynamicURL: string, layout: string, 612 * author: string, authorWebsite: string, availableOffline: boolean}} 613 * selectedItem selected wallpaper item in grid. 614 * @private 615 */ 616 WallpaperManager.prototype.setWallpaperAttribution_ = function(selectedItem) { 617 if (selectedItem && selectedItem.source != 'ADDNEW') { 618 $('author-name').textContent = selectedItem.author; 619 $('author-website').textContent = $('author-website').href = 620 selectedItem.authorWebsite; 621 chrome.wallpaperPrivate.getThumbnail(selectedItem.baseURL, 622 selectedItem.source, 623 function(data) { 624 var img = $('attribute-image'); 625 if (data) { 626 var blob = new Blob([new Int8Array(data)], {'type' : 'image\/png'}); 627 img.src = window.URL.createObjectURL(blob); 628 img.addEventListener('load', function(e) { 629 window.URL.revokeObjectURL(this.src); 630 }); 631 } else { 632 img.src = ''; 633 } 634 }); 635 $('wallpaper-attribute').hidden = false; 636 $('attribute-image').hidden = false; 637 return; 638 } 639 $('wallpaper-attribute').hidden = true; 640 $('attribute-image').hidden = true; 641 $('author-name').textContent = ''; 642 $('author-website').textContent = $('author-website').href = ''; 643 $('attribute-image').src = ''; 644 }; 645 646 /** 647 * Resize thumbnails grid and categories list to fit the new window size. 648 */ 649 WallpaperManager.prototype.onResize_ = function() { 650 this.wallpaperGrid_.redraw(); 651 this.categoriesList_.redraw(); 652 }; 653 654 /** 655 * Close the last opened overlay on pressing the Escape key. 656 * @param {Event} event A keydown event. 657 */ 658 WallpaperManager.prototype.onKeyDown_ = function(event) { 659 if (event.keyCode == 27) { 660 // The last opened overlay coincides with the first match of querySelector 661 // because the Error Container is declared in the DOM before the Wallpaper 662 // Selection Container. 663 // TODO(bshe): Make the overlay selection not dependent on the DOM. 664 var closeButtonSelector = '.overlay-container:not([hidden]) .close'; 665 var closeButton = this.document_.querySelector(closeButtonSelector); 666 if (closeButton) { 667 closeButton.click(); 668 event.preventDefault(); 669 } 670 } 671 }; 672 673 /** 674 * Constructs the categories list. 675 */ 676 WallpaperManager.prototype.initCategoriesList_ = function() { 677 this.categoriesList_ = $('categories-list'); 678 cr.ui.List.decorate(this.categoriesList_); 679 // cr.ui.list calculates items in view port based on client height and item 680 // height. However, categories list is displayed horizontally. So we should 681 // not calculate visible items here. Sets autoExpands to true to show every 682 // item in the list. 683 // TODO(bshe): Use ul to replace cr.ui.list for category list. 684 this.categoriesList_.autoExpands = true; 685 686 var self = this; 687 this.categoriesList_.itemConstructor = function(entry) { 688 return self.renderCategory_(entry); 689 }; 690 691 this.categoriesList_.selectionModel = new cr.ui.ListSingleSelectionModel(); 692 this.categoriesList_.selectionModel.addEventListener( 693 'change', this.onCategoriesChange_.bind(this)); 694 695 var categoriesDataModel = new cr.ui.ArrayDataModel([]); 696 if (this.enableOnlineWallpaper_) { 697 // Adds all category as first category. 698 categoriesDataModel.push(str('allCategoryLabel')); 699 for (var key in this.manifest_.categories) { 700 categoriesDataModel.push(this.manifest_.categories[key]); 701 } 702 } 703 // Adds custom category as last category. 704 categoriesDataModel.push(str('customCategoryLabel')); 705 this.categoriesList_.dataModel = categoriesDataModel; 706 }; 707 708 /** 709 * Constructs the element in categories list. 710 * @param {string} entry Text content of a category. 711 */ 712 WallpaperManager.prototype.renderCategory_ = function(entry) { 713 var li = this.document_.createElement('li'); 714 cr.defineProperty(li, 'custom', cr.PropertyKind.BOOL_ATTR); 715 li.custom = (entry == str('customCategoryLabel')); 716 cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR); 717 cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR); 718 var div = this.document_.createElement('div'); 719 div.textContent = entry; 720 li.appendChild(div); 721 return li; 722 }; 723 724 /** 725 * Handles the custom wallpaper which user selected from file manager. Called 726 * when users select a file. 727 */ 728 WallpaperManager.prototype.onFileSelectorChanged_ = function() { 729 var files = $('file-selector').files; 730 if (files.length != 1) 731 console.error('More than one files are selected or no file selected'); 732 if (!files[0].type.match('image/jpeg') && 733 !files[0].type.match('image/png')) { 734 this.showError_(str('invalidWallpaper')); 735 return; 736 } 737 var layout = getSelectedLayout(); 738 var self = this; 739 var errorHandler = this.onFileSystemError_.bind(this); 740 var setSelectedFile = function(file, layout, fileName) { 741 var saveThumbnail = function(thumbnail) { 742 var success = function(dirEntry) { 743 dirEntry.getFile(fileName, {create: true}, function(fileEntry) { 744 fileEntry.createWriter(function(fileWriter) { 745 fileWriter.onwriteend = function(e) { 746 $('set-wallpaper-layout').disabled = false; 747 var wallpaperInfo = { 748 baseURL: fileName, 749 layout: layout, 750 source: Constants.WallpaperSourceEnum.Custom, 751 availableOffline: true 752 }; 753 self.wallpaperGrid_.dataModel.splice(0, 0, wallpaperInfo); 754 self.wallpaperGrid_.selectedItem = wallpaperInfo; 755 self.wallpaperGrid_.activeItem = wallpaperInfo; 756 self.currentWallpaper_ = fileName; 757 WallpaperUtil.saveToStorage(self.currentWallpaper_, layout, 758 false); 759 }; 760 761 fileWriter.onerror = errorHandler; 762 763 var blob = new Blob([new Int8Array(thumbnail)], 764 {'type' : 'image\/jpeg'}); 765 fileWriter.write(blob); 766 }, errorHandler); 767 }, errorHandler); 768 }; 769 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, 770 success, errorHandler); 771 }; 772 773 var success = function(dirEntry) { 774 dirEntry.getFile(fileName, {create: true}, function(fileEntry) { 775 fileEntry.createWriter(function(fileWriter) { 776 fileWriter.addEventListener('writeend', function(e) { 777 var reader = new FileReader(); 778 reader.readAsArrayBuffer(file); 779 reader.addEventListener('error', errorHandler); 780 reader.addEventListener('load', function(e) { 781 self.setCustomWallpaper(e.target.result, layout, true, fileName, 782 saveThumbnail, function() { 783 self.removeCustomWallpaper(fileName); 784 errorHandler(); 785 }); 786 }); 787 }); 788 789 fileWriter.addEventListener('error', errorHandler); 790 fileWriter.write(file); 791 }, errorHandler); 792 }, errorHandler); 793 }; 794 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success, 795 errorHandler); 796 }; 797 setSelectedFile(files[0], layout, new Date().getTime().toString()); 798 }; 799 800 /** 801 * Removes wallpaper and thumbnail with fileName from FileSystem. 802 * @param {string} fileName The file name of wallpaper and thumbnail to be 803 * removed. 804 */ 805 WallpaperManager.prototype.removeCustomWallpaper = function(fileName) { 806 var errorHandler = this.onFileSystemError_.bind(this); 807 var self = this; 808 var removeFile = function(fileName) { 809 var success = function(dirEntry) { 810 dirEntry.getFile(fileName, {create: false}, function(fileEntry) { 811 fileEntry.remove(function() { 812 }, errorHandler); 813 }, errorHandler); 814 } 815 816 // Removes copy of original. 817 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, success, 818 errorHandler); 819 820 // Removes generated thumbnail. 821 self.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.THUMBNAIL, success, 822 errorHandler); 823 }; 824 removeFile(fileName); 825 }; 826 827 /** 828 * Sets current wallpaper and generate thumbnail if generateThumbnail is true. 829 * @param {ArrayBuffer} wallpaper The binary representation of wallpaper. 830 * @param {string} layout The user selected wallpaper layout. 831 * @param {boolean} generateThumbnail True if need to generate thumbnail. 832 * @param {string} fileName The unique file name of wallpaper. 833 * @param {function(thumbnail):void} success Success callback. If 834 * generateThumbnail is true, the callback parameter should have the 835 * generated thumbnail. 836 * @param {function(e):void} failure Failure callback. Called when there is an 837 * error from FileSystem. 838 */ 839 WallpaperManager.prototype.setCustomWallpaper = function(wallpaper, 840 layout, 841 generateThumbnail, 842 fileName, 843 success, 844 failure) { 845 var self = this; 846 var onFinished = function(opt_thumbnail) { 847 if (chrome.runtime.lastError != undefined) { 848 self.showError_(chrome.runtime.lastError.message); 849 $('set-wallpaper-layout').disabled = true; 850 failure(); 851 } else { 852 success(opt_thumbnail); 853 // Custom wallpapers are not synced yet. If login on a different 854 // computer after set a custom wallpaper, wallpaper wont change by sync. 855 WallpaperUtil.saveWallpaperInfo(fileName, layout, 856 Constants.WallpaperSourceEnum.Custom); 857 } 858 }; 859 860 chrome.wallpaperPrivate.setCustomWallpaper(wallpaper, layout, 861 generateThumbnail, 862 fileName, onFinished); 863 }; 864 865 /** 866 * Sets wallpaper finished. Displays error message if any. 867 * @param {WallpaperThumbnailsGridItem=} opt_selectedGridItem The wallpaper 868 * thumbnail grid item. It extends from cr.ui.ListItem. 869 * @param {{baseURL: string, layout: string, source: string, 870 * availableOffline: boolean, opt_dynamicURL: string, 871 * opt_author: string, opt_authorWebsite: string}=} 872 * opt_selectedItem the selected item in WallpaperThumbnailsGrid's data 873 * model. 874 */ 875 WallpaperManager.prototype.onFinished_ = function(opt_selectedGridItem, 876 opt_selectedItem) { 877 if (opt_selectedGridItem) 878 this.progressManager_.hideProgressBar(opt_selectedGridItem); 879 880 if (chrome.runtime.lastError != undefined) { 881 this.showError_(chrome.runtime.lastError.message); 882 } else if (opt_selectedItem) { 883 this.wallpaperGrid_.activeItem = opt_selectedItem; 884 } 885 }; 886 887 /** 888 * Handles the layout setting change of custom wallpaper. 889 */ 890 WallpaperManager.prototype.onWallpaperLayoutChanged_ = function() { 891 var layout = getSelectedLayout(); 892 var self = this; 893 chrome.wallpaperPrivate.setCustomWallpaperLayout(layout, function() { 894 if (chrome.runtime.lastError != undefined) { 895 self.showError_(chrome.runtime.lastError.message); 896 self.removeCustomWallpaper(fileName); 897 $('set-wallpaper-layout').disabled = true; 898 } else { 899 WallpaperUtil.saveToStorage(self.currentWallpaper_, layout, false); 900 } 901 }); 902 }; 903 904 /** 905 * Handles user clicking on a different category. 906 */ 907 WallpaperManager.prototype.onCategoriesChange_ = function() { 908 var categoriesList = this.categoriesList_; 909 var selectedIndex = categoriesList.selectionModel.selectedIndex; 910 if (selectedIndex == -1) 911 return; 912 var selectedListItem = categoriesList.getListItemByIndex(selectedIndex); 913 var bar = $('bar'); 914 bar.style.left = selectedListItem.offsetLeft + 'px'; 915 bar.style.width = selectedListItem.offsetWidth + 'px'; 916 917 var wallpapersDataModel = new cr.ui.ArrayDataModel([]); 918 var selectedItem; 919 if (selectedListItem.custom) { 920 this.document_.body.setAttribute('custom', ''); 921 var errorHandler = this.onFileSystemError_.bind(this); 922 var toArray = function(list) { 923 return Array.prototype.slice.call(list || [], 0); 924 } 925 926 var self = this; 927 var processResults = function(entries) { 928 for (var i = 0; i < entries.length; i++) { 929 var entry = entries[i]; 930 var wallpaperInfo = { 931 baseURL: entry.name, 932 // The layout will be replaced by the actual value saved in 933 // local storage when requested later. Layout is not important 934 // for constructing thumbnails grid, we use CENTER_CROPPED here 935 // to speed up the process of constructing. So we do not need to 936 // wait for fetching correct layout. 937 layout: 'CENTER_CROPPED', 938 source: Constants.WallpaperSourceEnum.Custom, 939 availableOffline: true 940 }; 941 if (self.currentWallpaper_ == entry.name) 942 selectedItem = wallpaperInfo; 943 wallpapersDataModel.push(wallpaperInfo); 944 } 945 var lastElement = { 946 baseURL: '', 947 layout: '', 948 source: 'ADDNEW', 949 availableOffline: true 950 }; 951 wallpapersDataModel.push(lastElement); 952 self.wallpaperGrid_.dataModel = wallpapersDataModel; 953 self.wallpaperGrid_.selectedItem = selectedItem; 954 self.wallpaperGrid_.activeItem = selectedItem; 955 } 956 957 var success = function(dirEntry) { 958 var dirReader = dirEntry.createReader(); 959 var entries = []; 960 // All of a directory's entries are not guaranteed to return in a single 961 // call. 962 var readEntries = function() { 963 dirReader.readEntries(function(results) { 964 if (!results.length) { 965 processResults(entries.sort()); 966 } else { 967 entries = entries.concat(toArray(results)); 968 readEntries(); 969 } 970 }, errorHandler); 971 }; 972 readEntries(); // Start reading dirs. 973 } 974 this.wallpaperDirs_.getDirectory(WallpaperDirNameEnum.ORIGINAL, 975 success, errorHandler); 976 } else { 977 this.document_.body.removeAttribute('custom'); 978 for (var key in this.manifest_.wallpaper_list) { 979 if (selectedIndex == AllCategoryIndex || 980 this.manifest_.wallpaper_list[key].categories.indexOf( 981 selectedIndex - OnlineCategoriesOffset) != -1) { 982 var wallpaperInfo = { 983 baseURL: this.manifest_.wallpaper_list[key].base_url, 984 layout: this.manifest_.wallpaper_list[key].default_layout, 985 source: Constants.WallpaperSourceEnum.Online, 986 availableOffline: false, 987 author: this.manifest_.wallpaper_list[key].author, 988 authorWebsite: this.manifest_.wallpaper_list[key].author_website, 989 dynamicURL: this.manifest_.wallpaper_list[key].dynamic_url 990 }; 991 var startIndex = wallpaperInfo.baseURL.lastIndexOf('/') + 1; 992 var fileName = wallpaperInfo.baseURL.substring(startIndex) + 993 Constants.HighResolutionSuffix; 994 if (this.downloadedListMap_ && 995 this.downloadedListMap_.hasOwnProperty(encodeURI(fileName))) { 996 wallpaperInfo.availableOffline = true; 997 } 998 wallpapersDataModel.push(wallpaperInfo); 999 var url = this.manifest_.wallpaper_list[key].base_url + 1000 Constants.HighResolutionSuffix; 1001 if (url == this.currentWallpaper_) { 1002 selectedItem = wallpaperInfo; 1003 } 1004 } 1005 } 1006 this.wallpaperGrid_.dataModel = wallpapersDataModel; 1007 this.wallpaperGrid_.selectedItem = selectedItem; 1008 this.wallpaperGrid_.activeItem = selectedItem; 1009 } 1010 }; 1011 1012})(); 1013