diff --git a/apps/music/css/tv.css b/apps/music/css/tv.css
new file mode 100644
index 000000000000..1d80a7ba83fd
--- /dev/null
+++ b/apps/music/css/tv.css
@@ -0,0 +1,7 @@
+#view-stack {
+ bottom: 0;
+}
+
+#tab-bar {
+ display: none !important;
+}
diff --git a/apps/music/elements/music-view-stack.js b/apps/music/elements/music-view-stack.js
index 197c4c49a1a4..5d6439d10166 100644
--- a/apps/music/elements/music-view-stack.js
+++ b/apps/music/elements/music-view-stack.js
@@ -217,6 +217,7 @@ proto.setRootView = function(url) {
newActiveView.frame.contentWindow.dispatchEvent(
new CustomEvent('viewvisible')
);
+ setTimeout(() => newActiveView.frame.contentWindow.focus(), 100);
this.dispatchEvent(new CustomEvent('change', { detail: newActiveView }));
});
@@ -245,6 +246,7 @@ proto.pushView = function(url) {
newActiveView.frame.contentWindow.dispatchEvent(
new CustomEvent('viewvisible')
);
+ setTimeout(() => newActiveView.frame.contentWindow.focus(), 100);
this.dispatchEvent(new CustomEvent('change', { detail: newActiveView }));
});
@@ -279,6 +281,7 @@ proto.popView = function(destroy) {
newActiveView.frame.contentWindow.dispatchEvent(
new CustomEvent('viewvisible')
);
+ setTimeout(() => newActiveView.frame.contentWindow.focus(), 100);
this.dispatchEvent(new CustomEvent('change', { detail: newActiveView }));
diff --git a/apps/music/index-tv.html b/apps/music/index-tv.html
new file mode 100644
index 000000000000..232fbec98238
--- /dev/null
+++ b/apps/music/index-tv.html
@@ -0,0 +1,96 @@
+
+
+
+
+ Music
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/music/manifest.webapp b/apps/music/manifest.webapp
index 1b3b901ec081..9ecf696f56e4 100644
--- a/apps/music/manifest.webapp
+++ b/apps/music/manifest.webapp
@@ -2,7 +2,7 @@
"version": "0.0.1",
"name": "Music",
"description": "Gaia Music",
- "launch_path": "/index.html",
+ "launch_path": "/index-tv.html",
"icons": {
"84": "/img/icons/music_84.png",
"126": "/img/icons/music_126.png",
@@ -74,7 +74,7 @@
}
},
"default_locale": "en",
- "orientation": "default",
+ "orientation": "landscape",
"messages": [
{ "media-button": "/index.html" }
]
diff --git a/apps/music/views/home-tv/index.html b/apps/music/views/home-tv/index.html
new file mode 100644
index 000000000000..45693bb84355
--- /dev/null
+++ b/apps/music/views/home-tv/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Music
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/music/views/home-tv/view.css b/apps/music/views/home-tv/view.css
new file mode 100644
index 000000000000..ae811433e877
--- /dev/null
+++ b/apps/music/views/home-tv/view.css
@@ -0,0 +1,91 @@
+body {
+ overflow-x: hidden;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+}
+
+body[data-search="true"] {
+ overflow: hidden;
+}
+
+music-search-box {
+ padding: 0 16px 10px;
+ border-bottom: solid 1px var(--border-color);
+ box-sizing: border-box;
+}
+
+music-search-box[hidden] {
+ display: none;
+}
+
+#tiles {
+ min-height: 100%;
+}
+
+.search-open #tiles {
+ display: none;
+}
+
+.tile {
+ border: solid 1px var(--background);
+ box-sizing: border-box;
+ display: block;
+ float: inline-start;
+ text-align: match-parent;
+ position: relative;
+ width: 33.3vw;
+ height: 33.3vw;
+ overflow: hidden;
+ text-decoration: none;
+ transition: border 0.2s ease;
+}
+
+.tile.selected {
+ border: solid 10px var(--highlight-color);
+}
+
+.tile:before,
+.tile:after {
+ background-color: rgba(0, 0, 0, 0.5);
+ box-sizing: border-box;
+ color: #fff;
+ font-size: 1.9rem;
+ line-height: 2.8rem;
+ text-shadow: 0 0.1rem rgba(0, 0, 0, 0.5);
+ display: block;
+ position: absolute;
+ padding: 0.15rem 1rem;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 2.8rem;
+ z-index: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tile:before {
+ content: attr(data-artist);
+}
+
+.tile:after {
+ content: attr(data-album);
+ color: rgba(255, 255, 255, 0.65);
+ font-size: 1.4rem;
+ line-height: 2.2rem;
+ top: 2.8rem;
+}
+
+.tile > img {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0;
+ transition: opacity 250ms;
+}
+
+.tile > img.loaded {
+ opacity: 1;
+}
diff --git a/apps/music/views/home-tv/view.js b/apps/music/views/home-tv/view.js
new file mode 100644
index 000000000000..49f733c37790
--- /dev/null
+++ b/apps/music/views/home-tv/view.js
@@ -0,0 +1,181 @@
+/* global View, Sanitizer */
+'use strict';
+
+var HomeView = View.extend(function HomeView() {
+ View.call(this); // super();
+
+ this.thumbnailCache = {};
+
+ this.tiles = document.getElementById('tiles');
+
+ this.onScroll = debounce(this.loadVisibleImages.bind(this), 500);
+ window.addEventListener('scroll', () => this.onScroll());
+ this.client.on('databaseChange', () => this.update());
+
+ window.addEventListener('keydown', (evt) => {
+ evt.preventDefault();
+
+ var selectedElement = this.tiles.querySelector('.selected');
+ if (!selectedElement) {
+ return;
+ }
+
+ var elements = this.tiles.querySelectorAll('.tile');
+ var selectedIndex = [].indexOf.call(elements, selectedElement);
+
+ switch (evt.key) {
+ case 'ArrowUp':
+ selectedIndex = clamp(0, elements.length - 1, selectedIndex - 3);
+ break;
+ case 'ArrowDown':
+ selectedIndex = clamp(0, elements.length - 1, selectedIndex + 3);
+ break;
+ case 'ArrowLeft':
+ selectedIndex = clamp(0, elements.length - 1, selectedIndex - 1);
+ break;
+ case 'ArrowRight':
+ selectedIndex = clamp(0, elements.length - 1, selectedIndex + 1);
+ break;
+ case 'Enter':
+ this.queueAlbum(selectedElement.dataset.filePath);
+ this.client.method('navigate', selectedElement.getAttribute('href'));
+ break;
+ }
+
+ console.log('keydown == ', evt);
+
+ selectedElement.classList.remove('selected');
+
+ selectedElement = elements[selectedIndex];
+ selectedElement.classList.add('selected');
+
+ window.scrollTo(0, selectedElement.offsetTop);
+ });
+
+ this.update();
+});
+
+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(() => {
+ img.classList.add('loaded');
+ resolve();
+ });
+ });
+ };
+ });
+ });
+};
+
+HomeView.prototype.destroy = function() {
+ this.client.destroy();
+
+ View.prototype.destroy.call(this); // super(); // Always call *last*
+};
+
+HomeView.prototype.render = function() {
+ View.prototype.render.call(this); // super();
+
+ 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);
+ this.tiles.firstElementChild.classList.add('selected');
+ return this.loadVisibleImages();
+ });
+};
+
+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);
+};
+
+function clamp(min, max, value) {
+ return Math.min(Math.max(min, value), max);
+}
+
+function debounce(fn, ms) {
+ var timeout;
+ return (...args) => {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => fn.apply(this, args), ms);
+ };
+}
+
+window.view = new HomeView();
diff --git a/apps/music/views/player-tv/index.html b/apps/music/views/player-tv/index.html
new file mode 100644
index 000000000000..8cb6d20bb336
--- /dev/null
+++ b/apps/music/views/player-tv/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+ Music
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/music/views/player-tv/view.css b/apps/music/views/player-tv/view.css
new file mode 100644
index 000000000000..eeeaa5cab142
--- /dev/null
+++ b/apps/music/views/player-tv/view.css
@@ -0,0 +1,5 @@
+music-artwork {
+ display: block;
+ width: 100%;
+ height: calc(100% - 9.1rem);
+}
diff --git a/apps/music/views/player-tv/view.js b/apps/music/views/player-tv/view.js
new file mode 100644
index 000000000000..1bfe198b79ec
--- /dev/null
+++ b/apps/music/views/player-tv/view.js
@@ -0,0 +1,156 @@
+/* global View */
+'use strict';
+
+const REPEAT_VALUES = ['off', 'list', 'song'];
+const SHUFFLE_VALUES = ['off', 'on'];
+
+var PlayerView = View.extend(function PlayerView() {
+ View.call(this); // super();
+
+ this.artwork = document.getElementById('artwork');
+ this.controls = document.getElementById('controls');
+ this.seekBar = document.getElementById('seek-bar');
+
+ this.artwork.addEventListener('share', () => this.share());
+ this.artwork.addEventListener('repeat', () => {
+ this.setRepeatSetting(this.artwork.repeat);
+ });
+ this.artwork.addEventListener('shuffle', () => {
+ this.setShuffleSetting(this.artwork.shuffle);
+ });
+ this.artwork.addEventListener('ratingchange', (evt) => {
+ this.setSongRating(evt.detail);
+ });
+
+ this.controls.addEventListener('play', () => this.play());
+ this.controls.addEventListener('pause', () => this.pause());
+ this.controls.addEventListener('previous', () => this.previous());
+ this.controls.addEventListener('next', () => this.next());
+ this.controls.addEventListener('startseek', (evt) => {
+ this.startFastSeek(evt.detail.reverse);
+ });
+ this.controls.addEventListener('stopseek', () => this.stopFastSeek());
+
+ this.seekBar.addEventListener('seek', (evt) => {
+ this.seek(evt.detail.elapsedTime);
+ });
+
+ this.client.on('play', () => this.controls.paused = false);
+ this.client.on('pause', () => this.controls.paused = true);
+ this.client.on('songChange', () => this.update());
+ this.client.on('durationChange', (duration) => {
+ this.seekBar.duration = duration;
+ });
+ this.client.on('elapsedTimeChange', (elapsedTime) => {
+ this.seekBar.elapsedTime = elapsedTime;
+ });
+
+ this.update();
+});
+
+PlayerView.prototype.update = function() {
+ this.getPlaybackStatus().then((status) => {
+ this.getSong(status.filePath).then((song) => {
+ if (!song) {
+ return;
+ }
+
+ document.l10n.formatValues(
+ 'unknownTitle', 'unknownArtist', 'unknownAlbum'
+ ).then(([unknownTitle, unknownArtist, unknownAlbum]) => {
+ this.title = song.metadata.title || unknownTitle;
+ this.artwork.artist = song.metadata.artist || unknownArtist;
+ this.artwork.album = song.metadata.album || unknownAlbum;
+ });
+
+ this.artwork.els.rating.value = song.metadata.rated;
+ });
+
+ this.getSongArtwork(status.filePath)
+ .then((url) => this.artwork.src = url);
+
+ this.artwork.repeat = REPEAT_VALUES[status.repeat];
+ this.artwork.shuffle = SHUFFLE_VALUES[status.shuffle];
+ this.controls.paused = status.paused;
+ this.seekBar.duration = status.duration;
+ this.seekBar.elapsedTime = status.elapsedTime;
+ this.render();
+ });
+};
+
+PlayerView.prototype.destroy = function() {
+ this.client.destroy();
+
+ View.prototype.destroy.call(this); // super(); // Always call *last*
+};
+
+PlayerView.prototype.render = function() {
+ View.prototype.render.call(this); // super();
+};
+
+PlayerView.prototype.startFastSeek = function(reverse) {
+ this.fetch('/api/audio/fastseek/start/' + (reverse ? 'reverse' : 'forward'));
+};
+
+PlayerView.prototype.stopFastSeek = function() {
+ this.fetch('/api/audio/fastseek/stop');
+};
+
+PlayerView.prototype.seek = function(time) {
+ this.fetch('/api/audio/seek/' + time);
+};
+
+PlayerView.prototype.play = function() {
+ this.fetch('/api/audio/play');
+};
+
+PlayerView.prototype.pause = function() {
+ this.fetch('/api/audio/pause');
+};
+
+PlayerView.prototype.previous = function() {
+ this.fetch('/api/queue/previous');
+};
+
+PlayerView.prototype.next = function() {
+ this.fetch('/api/queue/next');
+};
+
+PlayerView.prototype.share = function() {
+ this.getPlaybackStatus().then((status) => {
+ this.fetch('/api/activities/share/' + status.filePath);
+ });
+};
+
+PlayerView.prototype.getPlaybackStatus = function() {
+ return this.fetch('/api/audio/status').then(response => response.json());
+};
+
+PlayerView.prototype.setRepeatSetting = function(repeat) {
+ this.fetch('/api/queue/repeat/' + REPEAT_VALUES.indexOf(repeat));
+};
+
+PlayerView.prototype.setShuffleSetting = function(shuffle) {
+ this.fetch('/api/queue/shuffle/' + SHUFFLE_VALUES.indexOf(shuffle));
+};
+
+PlayerView.prototype.setSongRating = function(rating) {
+ this.getPlaybackStatus().then((status) => {
+ this.fetch('/api/songs/rating/' + rating + '/' + status.filePath);
+ });
+};
+
+PlayerView.prototype.getSong = function(filePath) {
+ return this.fetch('/api/songs/info/' + filePath).then((response) => {
+ return response.json();
+ });
+};
+
+PlayerView.prototype.getSongArtwork = function(filePath) {
+ return this.fetch('/api/artwork/url/original/' + filePath)
+ .then((response) => {
+ return response.json();
+ });
+};
+
+window.view = new PlayerView();
diff --git a/build/csslint/xfail.list b/build/csslint/xfail.list
index 081a0ef4ae3a..6e4f548cabf9 100644
--- a/build/csslint/xfail.list
+++ b/build/csslint/xfail.list
@@ -70,8 +70,10 @@ apps/music/components/dom-scheduler/demo-app/css/ul.css 1 1
apps/music/components/gaia-theme/gaia-theme.css 5 0
apps/music/css/view.css 1 1
apps/music/css/app.css 0 2
+apps/music/css/tv.css 0 1
apps/music/node_modules/nws/node_modules/connect/lib/public/style.css 4 10
apps/music/views/home/view.css 3 0
+apps/music/views/home-tv/view.css 2 0
apps/pdfjs/content/web/viewer.css 0 1
apps/ringtones/style/pick.css 0 1
apps/search/style/newtab.css 3 0