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