/* global View, Sanitizer, LazyLoader */ 'use strict'; var HomeView = View.extend(function HomeView() { View.call(this); // super(); this.thumbnailCache = {}; this.tiles = document.getElementById('tiles'); this.tiles.addEventListener('click', (evt) => { var link = evt.target.closest('a[data-file-path]'); if (link) { this.queueAlbum(link.dataset.filePath); } }); this.onScroll = debounce(this.loadVisibleImages.bind(this), 500); window.addEventListener('scroll', () => this.onScroll()); this.client.on('databaseChange', () => this.update()); // Two phase setup prevents non-critical // work blocking getting content on the screen this.update() .then(() => this.lazyLoadScripts()); }); /** * List of scripts that can be * lazy-loaded after the critical * path has completed. * * @type {Array} */ HomeView.prototype.nonCriticalScripts = [ '/components/dom-scheduler/lib/dom-scheduler.js', '/components/fast-list/fast-list.js', '/components/poplar/poplar.js', '/components/gaia-component/gaia-component.js', '/components/gaia-text-input/gaia-text-input.js', '/components/gaia-fast-list/gaia-fast-list.js', '/components/gaia-sub-header/gaia-sub-header.js', '/elements/music-search-box.js', '/elements/music-search-results.js' ]; /** * Setup everything that is not * critical for first-paint. * * @return {Promise} */ HomeView.prototype.lazyLoadScripts = function() { return new Promise((resolve) => { setTimeout(() => { LazyLoader.load(this.nonCriticalScripts).then(() => { this.setupSearch(); resolve(); }); }, 500); }); }; HomeView.prototype.setupSearch = function() { this.searchBox = document.getElementById('search-box'); this.searchResults = document.getElementById('search-results'); this.searchBoxHeight = this.searchBox.HEIGHT + 11;// 10px padding + 1px border this.searchBox.addEventListener('search', (evt) => this.search(evt.detail)); this.searchResults.addEventListener('open', () => { this.client.method('searchOpen'); document.body.dataset.search = true; document.body.classList.add('search-open'); }); this.searchResults.addEventListener('close', () => { this.client.method('searchClose'); document.body.dataset.search = false; document.body.classList.remove('search-open'); window.scrollTo(0, this.searchBoxHeight); }); this.searchResults.addEventListener('resultclick', (evt) => { var link = evt.detail; if (link) { if (link.dataset.section === 'songs') { this.queueSong(link.dataset.filePath); } this.client.method('navigate', link.getAttribute('href')); } }); this.searchResults.getItemImageSrc = (item) => this.getThumbnail(item.name); this.searchBox.hidden = false; window.scrollTo(0, this.searchBoxHeight); }; HomeView.prototype.update = function() { return this.getAlbums().then((albums) => { this.albums = albums; return this.render(); }); }; HomeView.prototype.loadVisibleImages = function() { var scrollTop = window.scrollY; var scrollBottom = scrollTop + window.innerHeight; var promises = []; var tiles = this.tiles.querySelectorAll('.tile'); var lastTileVisible = false; var tile, tileOffset; for (var i = 0, length = tiles.length; i < length; i++) { tile = tiles[i]; tileOffset = tile.offsetTop; if (scrollTop <= tileOffset && tileOffset <= scrollBottom) { lastTileVisible = true; promises.push(this.loadTile(tile)); } else if (lastTileVisible) { break; } } return Promise.all(promises); }; HomeView.prototype.loadTile = function(tile) { return new Promise((resolve) => { if (tile.dataset.loaded) { return; } this.getThumbnail(tile.dataset.filePath).then((url) => { var img = tile.querySelector('img'); img.src = url; tile.dataset.loaded = true; img.onload = () => { setTimeout(() => { requestAnimationFrame(() => { tile.classList.add('loaded'); resolve(); }); }); }; }); }); }; HomeView.prototype.destroy = function() { this.client.destroy(); View.prototype.destroy.call(this); // super(); // Always call *last* }; HomeView.prototype.render = function() { return document.l10n.formatValues( 'unknownArtist', 'unknownAlbum' ).then(([unknownArtist, unknownAlbum]) => { var html = []; this.albums.forEach((album) => { var template = Sanitizer.createSafeHTML ` `; html.push(template); }); this.tiles.innerHTML = Sanitizer.unwrapSafeHTML(...html); return Promise.all([ this.loadVisibleImages(), document.l10n.ready ]); }).then(this.onRenderDone); }; HomeView.prototype.getAlbums = function() { return this.fetch('/api/albums/list').then(response => response.json()); }; HomeView.prototype.getThumbnail = function(filePath) { if (!filePath) return; if (this.thumbnailCache[filePath]) { return Promise.resolve(this.thumbnailCache[filePath]); } return this.fetch('/api/artwork/url/thumbnail/' + filePath) .then((response) => response.json()) .then((url) => { this.thumbnailCache[filePath] = url; return url; }); }; HomeView.prototype.queueAlbum = function(filePath) { this.fetch('/api/queue/album/' + filePath); }; HomeView.prototype.queueSong = function(filePath) { this.fetch('/api/queue/song/' + filePath); }; HomeView.prototype.search = function(query) { if (!query) { return Promise.resolve(this.searchResults.clearResults()); } var results = []; return document.l10n.formatValues( 'unknownTitle', 'unknownArtist', 'unknownAlbum' ).then(([unknownTitle, unknownArtist, unknownAlbum]) => { var albumResults = this.fetch('/api/search/album/' + query) .then(response => response.json()) .then((albums) => { var albumResults = albums.map((album) => { return { name: album.name, title: album.metadata.album || unknownAlbum, subtitle: album.metadata.artist || unknownArtist, section: 'albums', url: '/album-detail?id=' + encodeURIComponent(album.name) }; }); results = results.concat(albumResults); this.searchResults.setResults(results); return albumResults; }); var artistResults = this.fetch('/api/search/artist/' + query) .then(response => response.json()) .then((artists) => { var artistResults = artists.map((artist) => { return { name: artist.name, title: artist.metadata.artist || unknownArtist, subtitle: '', section: 'artists', url: '/artist-detail?id=' + encodeURIComponent(artist.name) }; }); results = results.concat(artistResults); this.searchResults.setResults(results); return artistResults; }); var songResults = this.fetch('/api/search/title/' + query) .then(response => response.json()) .then((songs) => { var songResults = songs.map((song) => { return { name: song.name, title: song.metadata.title || unknownTitle, subtitle: song.metadata.artist || unknownArtist, section: 'songs', url: '/player' }; }); results = results.concat(songResults); this.searchResults.setResults(results); return songResults; }); return Promise.all([albumResults, artistResults, songResults]).then(() => { return results; }); }); }; function debounce(fn, ms) { var timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn.apply(this, args), ms); }; } window.view = new HomeView();