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