'use strict'; /* global Application, FilterManager, CardManager, Clock, Deck, Edit, Folder, KeyNavigationAdapter, MessageHandler, MozActivity, SearchBar, SharedUtils, SpatialNavigator, URL, XScrollable, Animations, Utils, FTEWizard, AppBookmark */ /* jshint nonew: false */ (function(exports) { const FULLSIZED_ICON = 336 * (window.devicePixelRatio || 1); const DEFAULT_ICON = 'url("/style/images/appic_developer.png")'; const DEFAULT_BGCOLOR = 'rgba(0, 0, 0, 0.5)'; const DEFAULT_BGCOLOR_ARRAY = [0, 0, 0, 0.5]; const CARDLIST_LEFT_MARGIN = 8.4; /** * Main class of smart-home * @class Home */ function Home() {} Home.prototype = { navigableIds: ['search-button', 'search-input', 'edit-button', 'settings-button', 'filter-tab-group'], topElementIds: ['search-button', 'search-input', 'edit-button', 'settings-button'], bottomElementIds: ['filter-tab-group', 'filter-all-button', 'filter-tv-button', 'filter-device-button', 'filter-app-button', 'filter-website-button'], isNavigable: true, navigableClasses: ['filter-tab', 'command-button'], navigableScrollable: [], cardScrollable: undefined, folderScrollable: undefined, _focus: undefined, _focusScrollable: undefined, _folderCard: undefined, filterElementIds: ['filter-all-button', 'filter-tv-button', 'filter-device-button', 'filter-app-button', 'filter-website-button'], filterManager: undefined, cardListElem: document.getElementById('card-list'), folderListElem: document.getElementById('folder-list'), cardManager: undefined, editButton: document.getElementById('edit-button'), settingsButton: document.getElementById('settings-button'), searchButton: document.getElementById('search-button'), timeElem: document.getElementById('time'), fteElem: document.getElementById('fte'), init: function() { var that = this; this.initClock(); this.cardManager = new CardManager(); this.cardManager.init(); this.searchBar = new SearchBar(); this.searchBar.init(document.getElementById('search-bar')); this.searchBar.on('shown', this.onSearchBarShown.bind(this)); this.searchBar.on('hidden', this.onSearchBarHidden.bind(this)); this.cardManager.getCardList().then(function(cardList) { that.messageHandler = new MessageHandler(); that.messageHandler.init(that); that._createCardList(cardList); that.cardScrollable = new XScrollable({ frameElem: 'card-list-frame', listElem: 'card-list', itemClassName: 'app-button', leftMargin: CARDLIST_LEFT_MARGIN}); that.folderScrollable = new XScrollable({ frameElem: 'folder-list-frame', listElem: 'folder-list', itemClassName: 'app-button', leftMargin: CARDLIST_LEFT_MARGIN, scale: 0.68, referenceElement: that.cardScrollable}); that.navigableScrollable = [that.cardScrollable, that.folderScrollable]; var collection = that._getNavigateElements(); that.spatialNavigator = new SpatialNavigator(collection); that.spatialNavigator.straightOnly = true; that.keyNavigatorAdapter = new KeyNavigationAdapter(); that.keyNavigatorAdapter.init(); that.keyNavigatorAdapter.on('move', that.onMove.bind(that)); // All behaviors which no need to have multple events while holding // the key should use keyup. that.keyNavigatorAdapter.on('enter-keyup', that.onEnter.bind(that)); that.cardListElem.addEventListener('transitionend', that.determineFolderExpand.bind(that)); that.cardManager.on('card-inserted', that.onCardInserted.bind(that, that.cardScrollable)); that.cardManager.on('card-removed', that.onCardRemoved.bind(that, that.cardScrollable)); that.cardManager.on('card-updated', that.onCardUpdated.bind(that)); that.spatialNavigator.on('focus', that.handleFocus.bind(that)); that.spatialNavigator.on('unfocus', that.handleUnfocus.bind(that)); var handleCardFocusBound = that.handleCardFocus.bind(that); var handleCardUnfocusBound = that.handleCardUnfocus.bind(that); var handleCardUnhoverBound = that.handleCardUnhover.bind(that); that.navigableScrollable.forEach(function(scrollable) { scrollable.on('focus', handleCardFocusBound); scrollable.on('unfocus', handleCardUnfocusBound); scrollable.on('unhover', handleCardUnhoverBound); if (scrollable.isEmpty()) { that.spatialNavigator.remove(scrollable); } }); that.edit = new Edit(); that.edit.init(that.spatialNavigator, that.cardManager, that.cardScrollable, that.folderScrollable); that.edit.on('arrange', that.onArrangeMode.bind(that)); that.filterManager = new FilterManager(); that.filterManager.init({ cardListElem: that.cardListElem, cardScrollable: that.cardScrollable, home: that, cardManager: that.cardManager, }); // In some case, we can do action at keydown which is translated as // onEnter in home.js. But in button click case, we need to listen // keyup. So, instead keydown/keyup, we just use click event to handle // it. The click event is translated at smart-button when use press // enter on smart-button. that.searchButton.addEventListener('click', function() { that.searchBar.show(); // hide the searchButton because searchBar has an element whose // appearance is the same as it. that.searchButton.classList.add('hidden'); }.bind(that)); // handle animation that.endBubble = null; document.addEventListener( 'visibilitychange', that.onVisibilityChange.bind(that)); // if this init function is executed after the document is set to // visible, the visibilitychange event may not be triggered. if (document.visibilityState === 'visible') { that.onVisibilityChange(); } cardList.forEach(function(card) { if (card instanceof Folder) { card.on('card-inserted', that.onCardInserted.bind(that, that.folderScrollable)); card.on('card-removed', that.onCardRemoved.bind(that, that.folderScrollable)); } }); that._fteWizard = new FTEWizard('homeFTE'); that._fteWizard.init({ container: that.fteElem, onfinish: () => { that.spatialNavigator.focus(that.cardScrollable); that.isNavigable = true; } }); }); }, onVisibilityChange: function() { if (document.visibilityState === 'visible') { Utils.holdFocusForAnimation(); this.cardListElem.classList.remove('hidden'); var that = this; Promise.all([new Promise(function(resolve) { that.skipBubble = Animations.doBubbleAnimation( that.cardListElem, '.app-button', 100, resolve); }), new Promise(function(resolve) { if (that._folderCard) { that.skipFolderBubble = Animations.doBubbleAnimation( that.folderListElem, '.app-button', 100, resolve); } else { resolve(); } })]).then(function() { // Catch focus back unless there is a pin activity since callback of // pinning would catch the focus for us. if (that._fteWizard.running) { that._fteWizard.focus(); } else if (!that.messageHandler.resumeActivity()) { that.spatialNavigator.focus(); } that.isNavigable = true; that.skipBubble = null; that.skipFolderBubble = null; }); } else { this.cardListElem.classList.add('hidden'); this.messageHandler.stopActivity(); this.isNavigable = false; // An user may close home app when bubbling or sliding animations are // still playing, and then open home app again right away. In this case, // the user will see the last unfinished animations. In order to solve // this, we have to force disable all the animations and trigger their // callbacks when home app is in hidden state. if (this.skipBubble) { this.skipBubble(); } if (this.skipFolderBubble) { this.skipFolderBubble(); } if (this.cardScrollable.isSliding) { this.cardScrollable.endSlide(); } this.filterManager.resetFilter(); } }, initClock: function() { var that = this; navigator.mozL10n.ready(function() { that.clock = new Clock(); that.clock.start(that.updateClock.bind(that)); // Listen to 'moztimechange' window.addEventListener('moztimechange', that.restartClock.bind(that)); // Listen to 'timeformatchange' window.addEventListener('timeformatchange', that.restartClock.bind(that)); }); }, _localizeCardName: function(elem, card) { if (!elem || !card) { return; } // We should use user given name first, otherwise we use localized // application/deck name. var lang = document.documentElement.lang; var name = this.cardManager.resolveCardName(card, lang); if (name && name.raw) { elem.removeAttribute('data-l10n-id'); elem.textContent = name.raw; } else if (name && name.id) { SharedUtils.localizeElement(elem, name); } }, onCardInserted: function(scrollable, card, idx, overFolder) { if (card instanceof Folder) { card.on('card-inserted', this.onCardInserted.bind(this, this.folderScrollable)); card.on('card-removed', this.onCardRemoved.bind(this, this.folderScrollable)); } var newCardElem = this.createCardNode(card); var newCardButtonElem = newCardElem.firstElementChild; // Initial transition for new card newCardButtonElem.classList.add('new-card'); newCardButtonElem.classList.add('new-card-transition'); newCardButtonElem.addEventListener('transitionend', function onPinned() { newCardButtonElem.classList.remove('new-card-transition'); newCardButtonElem.classList.remove('last-card'); newCardButtonElem.removeEventListener('transitionend', onPinned); }); // insert new card into cardScrollable this.isNavigable = false; scrollable.on('slideEnd', function() { newCardButtonElem.classList.remove('new-card'); if (scrollable.nodes.indexOf(newCardElem) === scrollable.nodes.length - 1) { newCardButtonElem.classList.add('last-card'); } this.isNavigable = true; }.bind(this)); if (!overFolder) { scrollable.insertNodeBefore(newCardElem, idx); } else { scrollable.insertNodeOver(newCardElem, idx); } }, onCardUpdated: function(card, idx) { var that = this; var cardButton = this.cardScrollable.getItemFromNode( this.cardScrollable.getNode(idx)); var spans = SharedUtils.nodeListToArray(cardButton.getElementsByTagName('span')); spans.forEach(function(span) { if (span.classList.contains('name')) { that._localizeCardName(span, card); } }); }, onCardRemoved: function(scrollable, indices) { indices.forEach(function(idx) { var elm = scrollable.getNode(idx); if (elm && elm.dataset.revokableURL) { URL.revokeObjectURL(elm.dataset.revokableURL); } }, this); scrollable.removeNodes(indices); }, _setCardIcon: function (cardButton, card, blob, bgColor) { try { var bgUrl = URL.createObjectURL(blob); if (bgColor) { cardButton.style.backgroundColor = bgColor; cardButton.classList.add('fitted'); card.backgroundType = 'fitted'; } else { cardButton.classList.add('fullsized'); card.backgroundType = 'fullsized'; } cardButton.dataset.revokableURL = bgUrl; cardButton.style.backgroundImage = 'url("' + bgUrl + '")'; } catch (e) { // If the blob is broken, we may get an exception while creating object // URL. cardButton.style.backgroundImage = DEFAULT_ICON; cardButton.style.backgroundColor = DEFAULT_BGCOLOR; } }, _createWave: function(cardButton, card) { // deck's icon using gaia font var deckIcon = document.createElement('span'); deckIcon.className = 'icon'; deckIcon.dataset.icon = card.deckClass; // front wave of a deck var waveFront = document.createElement('div'); waveFront.className = 'deck-wave'; waveFront.classList.add('wave-front'); waveFront.classList.add(card.deckClass + '-wave-front'); waveFront.classList.add('wave-paused'); // back wave of a deck var waveBack = document.createElement('div'); waveBack.className = 'deck-wave'; waveBack.classList.add('wave-back'); waveBack.classList.add(card.deckClass + '-wave-back'); cardButton.appendChild(waveBack); cardButton.appendChild(deckIcon); cardButton.appendChild(waveFront); cardButton.classList.add('deck-' + card.deckClass); }, _fillCardIcon: function(cardButton, card) { var manifestURL = card.nativeApp && card.nativeApp.manifestURL; var that = this; // We have thumbnail which is created by pin if (card.thumbnail) { this._setCardIcon(cardButton, card, card.thumbnail, card.backgroundColor); // TODO add backgroundColor??? How to do it??? } else if (!card.cachedIconBlob) { // We don't have cachedIconBlob, just get icon from app this.cardManager.getIconBlob({ manifestURL: manifestURL, entryPoint: card.entryPoint, // XXX: preferredSize should be determined by // real offsetWidth of cardThumbnailElem instead of hard-coded value preferredSize: FULLSIZED_ICON }).then(function(iconData) { var blob = iconData[0]; var size = iconData[1]; if (size >= FULLSIZED_ICON) { that._setCardIcon(cardButton, card, blob, null); } else { that._getIconColor(blob, function(color, err) { if (err) { that._setCardIcon(cardButton, card, blob, DEFAULT_BGCOLOR); } else { that._setCardIcon(cardButton, card, blob, 'rgba(' + color[0] + ', ' + color[1] + ', ' + color[2] + ', ' + color[3] + ')'); } }); } card.cachedIconBlob = blob; }); } else if (card.cachedIconBlob) { // We already have cacedIconBlob which is created by previous step. this._setCardIcon(cardButton, card, card.cachedIconBlob, card.backgroundColor); } }, _getIconColor: function(blob, callback) { var dy = 0; function checkColor(color, err) { if (err) { callback(null, err); } else if (color[3] < 255 && dy < 0.5) { dy += 0.25; SharedUtils.readColorCode(blob, 0.5, dy, checkColor); } else { callback(color[3] < 255 ? DEFAULT_BGCOLOR_ARRAY : color, err); } } SharedUtils.readColorCode(blob, 0.5, 0, checkColor); }, onArrangeMode: function() { if (this._focusScrollable !== this.folderScrollable) { this.cleanFolderScrollable(); } }, createCardNode: function(card) { // card element would be created like this: //