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