Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
38
gaia/apps/music/views/album-detail/index.html
Executable file
38
gaia/apps/music/views/album-detail/index.html
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="albums">Albums</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/shared/js/intl_helper.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<gaia-fast-list id="list" top="50" bottom="0">
|
||||
<template>
|
||||
<a href="/player" data-file-path="${name}" data-number="${index}">
|
||||
<h3 dir="auto">${title}</h3>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
</body>
|
||||
</html>
|
||||
85
gaia/apps/music/views/album-detail/view.js
Executable file
85
gaia/apps/music/views/album-detail/view.js
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
/* global View, IntlHelper */
|
||||
'use strict';
|
||||
|
||||
var AlbumDetailView = View.extend(function AlbumDetailView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
// Triggers player service to begin playing the track.
|
||||
// This works for now, but we might have the PlayerView
|
||||
// take care of this task as it's a big more webby :)
|
||||
this.list.addEventListener('click', (evt) => {
|
||||
var link = evt.target.closest('a[data-file-path]');
|
||||
if (link) {
|
||||
this.queueAlbum(link.dataset.filePath);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
|
||||
this.update();
|
||||
});
|
||||
|
||||
AlbumDetailView.prototype.update = function() {
|
||||
return this.getAlbum().then((songs) => {
|
||||
this.songs = songs;
|
||||
return this.render();
|
||||
});
|
||||
};
|
||||
|
||||
AlbumDetailView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
AlbumDetailView.prototype.render = function() {
|
||||
return this.list.setModel(this.songs)
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
AlbumDetailView.prototype.getAlbum = function() {
|
||||
var unpaddedIndex = IntlHelper.get('unpaddedIndex');
|
||||
var paddedIndex = IntlHelper.get('paddedIndex');
|
||||
|
||||
return this.fetch('/api/albums/info/' + decodeURIComponent(this.params.id))
|
||||
.then(response => response.json())
|
||||
.then(songs => {
|
||||
var maxDiscNumber = Math.max(
|
||||
songs[songs.length - 1].metadata.disccount,
|
||||
songs[songs.length - 1].metadata.discnum
|
||||
);
|
||||
|
||||
return songs.map((song) => {
|
||||
return {
|
||||
index: maxDiscNumber > 1 &&
|
||||
song.metadata.discnum && song.metadata.tracknum ?
|
||||
unpaddedIndex.format(song.metadata.discnum) + '.' +
|
||||
paddedIndex.format(song.metadata.tracknum) :
|
||||
(song.metadata.tracknum ?
|
||||
unpaddedIndex.format(song.metadata.tracknum) : ''),
|
||||
name: song.name,
|
||||
title: song.metadata.title
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
AlbumDetailView.prototype.queueAlbum = function(filePath) {
|
||||
this.fetch('/api/queue/album/' + filePath);
|
||||
};
|
||||
|
||||
IntlHelper.define('unpaddedIndex', 'number', {
|
||||
style: 'decimal',
|
||||
useGrouping: false,
|
||||
minimumIntegerDigits: 1
|
||||
});
|
||||
|
||||
IntlHelper.define('paddedIndex', 'number', {
|
||||
style: 'decimal',
|
||||
useGrouping: false,
|
||||
minimumIntegerDigits: 2
|
||||
});
|
||||
|
||||
window.view = new AlbumDetailView();
|
||||
45
gaia/apps/music/views/albums/index.html
Executable file
45
gaia/apps/music/views/albums/index.html
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="albums">Albums</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/components/gaia-sub-header/gaia-sub-header.js"></script>
|
||||
<script defer src="/components/gaia-text-input/gaia-text-input.js"></script>
|
||||
<script defer src="/elements/music-search-box.js"></script>
|
||||
<script defer src="/elements/music-search-results.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application" hidden>
|
||||
<gaia-fast-list id="list" top="50" bottom="0" caching>
|
||||
<music-search-box id="search-box" results="#search-results"></music-search-box>
|
||||
<template>
|
||||
<a href="${url}">
|
||||
<div class="image"><img/></div>
|
||||
<h3 dir="auto">${album}</h3>
|
||||
<p dir="auto">${artist}</p>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
<music-search-results id="search-results"></music-search-results>
|
||||
</body>
|
||||
</html>
|
||||
86
gaia/apps/music/views/albums/view.js
Executable file
86
gaia/apps/music/views/albums/view.js
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
/* global View */
|
||||
'use strict';
|
||||
|
||||
var AlbumsView = View.extend(function AlbumsView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.searchBox = document.getElementById('search-box');
|
||||
this.searchResults = document.getElementById('search-results');
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
|
||||
this.setupSearch();
|
||||
this.setupList();
|
||||
|
||||
this.update();
|
||||
});
|
||||
|
||||
AlbumsView.prototype.update = function() {
|
||||
return this.getAlbums().then((albums) => {
|
||||
this.albums = albums;
|
||||
return this.render();
|
||||
});
|
||||
};
|
||||
|
||||
AlbumsView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
AlbumsView.prototype.render = function() {
|
||||
return this.list.setModel(this.albums)
|
||||
.then(() => this.list.cache())
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
AlbumsView.prototype.getAlbums = function() {
|
||||
return document.l10n.formatValues('unknownAlbum', 'unknownArtist')
|
||||
.then(([unknownAlbum, unknownArtist]) => {
|
||||
return this.fetch('/api/albums/list')
|
||||
.then(response => response.json())
|
||||
.then((albums) => {
|
||||
return albums.map((album) => {
|
||||
return {
|
||||
name: album.name,
|
||||
url: '/album-detail?id=' + encodeURIComponent(album.name),
|
||||
album: album.metadata.album || unknownAlbum,
|
||||
artist: album.metadata.artist || unknownArtist
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AlbumsView.prototype.getThumbnail = function(filePath) {
|
||||
return this.fetch('/api/artwork/url/thumbnail/' + filePath)
|
||||
.then(response => response.json());
|
||||
};
|
||||
|
||||
AlbumsView.prototype.search = function(query) {
|
||||
if (!query) {
|
||||
return Promise.resolve(this.searchResults.clearResults());
|
||||
}
|
||||
|
||||
return document.l10n.formatValues(
|
||||
'unknownAlbum', 'unknownArtist'
|
||||
).then(([unknownAlbum, unknownArtist]) => {
|
||||
return this.fetch('/api/search/album/' + query)
|
||||
.then(response => response.json())
|
||||
.then((albums) => {
|
||||
var results = 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)
|
||||
};
|
||||
});
|
||||
|
||||
return this.searchResults.setResults(results);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.view = new AlbumsView();
|
||||
39
gaia/apps/music/views/artist-detail/index.html
Executable file
39
gaia/apps/music/views/artist-detail/index.html
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="artists">Artists</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/shared/js/intl_helper.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/components/gaia-sub-header/gaia-sub-header.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<gaia-fast-list id="list" top="50" bottom="0">
|
||||
<template>
|
||||
<a href="/player" data-file-path="${name}" data-number="${index}">
|
||||
<h3 dir="auto">${title}</h3>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
</body>
|
||||
</html>
|
||||
72
gaia/apps/music/views/artist-detail/view.js
Executable file
72
gaia/apps/music/views/artist-detail/view.js
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
/* global View, IntlHelper */
|
||||
'use strict';
|
||||
|
||||
var ArtistDetailView = View.extend(function ArtistDetailView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
// Triggers player service to begin playing the track.
|
||||
// This works for now, but we might have the PlayerView
|
||||
// take care of this task as it's a big more webby :)
|
||||
this.list.addEventListener('click', evt => {
|
||||
var link = evt.target.closest('a[data-file-path]');
|
||||
if (link) {
|
||||
this.queueArtist(link.dataset.filePath);
|
||||
}
|
||||
});
|
||||
|
||||
this.list.configure({
|
||||
getSectionName: item => item.section
|
||||
});
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
this.update();
|
||||
});
|
||||
|
||||
ArtistDetailView.prototype.update = function() {
|
||||
return this.getArtist().then(songs => {
|
||||
this.songs = songs;
|
||||
return this.render();
|
||||
});
|
||||
};
|
||||
|
||||
ArtistDetailView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
ArtistDetailView.prototype.render = function() {
|
||||
return this.list.setModel(this.songs)
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
ArtistDetailView.prototype.getArtist = function() {
|
||||
var unpaddedIndex = IntlHelper.get('unpaddedIndex');
|
||||
|
||||
return this.fetch('/api/artists/info/' + decodeURIComponent(this.params.id))
|
||||
.then(response => response.json())
|
||||
.then(songs => {
|
||||
return songs.map(song => {
|
||||
return {
|
||||
index: song.metadata.tracknum ?
|
||||
unpaddedIndex.format(song.metadata.tracknum) : '',
|
||||
name: song.name,
|
||||
title: song.metadata.title,
|
||||
section: song.metadata.album
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ArtistDetailView.prototype.queueArtist = function(filePath) {
|
||||
this.fetch('/api/queue/artist/' + filePath);
|
||||
};
|
||||
|
||||
IntlHelper.define('unpaddedIndex', 'number', {
|
||||
style: 'decimal',
|
||||
useGrouping: false,
|
||||
minimumIntegerDigits: 1
|
||||
});
|
||||
|
||||
window.view = new ArtistDetailView();
|
||||
44
gaia/apps/music/views/artists/index.html
Executable file
44
gaia/apps/music/views/artists/index.html
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="artists">Artists</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/components/gaia-sub-header/gaia-sub-header.js"></script>
|
||||
<script defer src="/components/gaia-text-input/gaia-text-input.js"></script>
|
||||
<script defer src="/elements/music-search-box.js"></script>
|
||||
<script defer src="/elements/music-search-results.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application" hidden>
|
||||
<gaia-fast-list id="list" top="50" bottom="0" caching>
|
||||
<music-search-box id="search-box" results="#search-results"></music-search-box>
|
||||
<template>
|
||||
<a href="${url}">
|
||||
<div class="image"><img/></div>
|
||||
<h3 dir="auto">${artist}</h3>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
<music-search-results id="search-results"></music-search-results>
|
||||
</body>
|
||||
</html>
|
||||
82
gaia/apps/music/views/artists/view.js
Executable file
82
gaia/apps/music/views/artists/view.js
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
/* global View */
|
||||
'use strict';
|
||||
|
||||
var ArtistsView = View.extend(function ArtistsView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.searchBox = document.getElementById('search-box');
|
||||
this.searchResults = document.getElementById('search-results');
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
|
||||
this.setupSearch();
|
||||
this.setupList();
|
||||
this.update();
|
||||
});
|
||||
|
||||
ArtistsView.prototype.update = function() {
|
||||
return this.getArtists().then((artists) => {
|
||||
this.artists = artists;
|
||||
return this.render();
|
||||
});
|
||||
};
|
||||
|
||||
ArtistsView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
ArtistsView.prototype.render = function() {
|
||||
return this.list.setModel(this.artists)
|
||||
.then(() => this.list.cache())
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
ArtistsView.prototype.getArtists = function() {
|
||||
return document.l10n.formatValue('unknownArtist')
|
||||
.then((unknownArtist) => {
|
||||
return this.fetch('/api/artists/list')
|
||||
.then(response => response.json())
|
||||
.then((artists) => {
|
||||
return artists.map((artist) => {
|
||||
return {
|
||||
name: artist.name,
|
||||
url: '/artist-detail?id=' + encodeURIComponent(artist.name),
|
||||
artist: artist.metadata.artist || unknownArtist
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ArtistsView.prototype.getThumbnail = function(filePath) {
|
||||
return this.fetch('/api/artwork/url/thumbnail/' + filePath)
|
||||
.then(response => response.json());
|
||||
};
|
||||
|
||||
ArtistsView.prototype.search = function(query) {
|
||||
if (!query) {
|
||||
return Promise.resolve(this.searchResults.clearResults());
|
||||
}
|
||||
|
||||
return document.l10n.formatValue('unknownArtist').then((unknownArtist) => {
|
||||
return this.fetch('/api/search/artist/' + query)
|
||||
.then(response => response.json())
|
||||
.then((artists) => {
|
||||
var results = artists.map((artist) => {
|
||||
return {
|
||||
name: artist.name,
|
||||
title: artist.metadata.artist || unknownArtist,
|
||||
subtitle: '',
|
||||
section: 'artists',
|
||||
url: '/artist-detail?id=' + encodeURIComponent(artist.name)
|
||||
};
|
||||
});
|
||||
|
||||
return this.searchResults.setResults(results);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.view = new ArtistsView();
|
||||
31
gaia/apps/music/views/home/index.html
Executable file
31
gaia/apps/music/views/home/index.html
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="music">Music</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<link rel="stylesheet" href="view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/shared/js/sanitizer.js"></script>
|
||||
<script defer src="/shared/js/lazy_loader.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<music-search-box id="search-box" results="#search-results" hidden></music-search-box>
|
||||
<section id="tiles"></section>
|
||||
<music-search-results id="search-results"></music-search-results>
|
||||
</body>
|
||||
</html>
|
||||
116
gaia/apps/music/views/home/view.css
Executable file
116
gaia/apps/music/views/home/view.css
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
body {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
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-bottom: solid 0.1rem var(--background);
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
float: inline-start;
|
||||
text-align: match-parent;
|
||||
position: relative;
|
||||
width: 33.333vw;
|
||||
height: 33.333vw;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tile:nth-child(3n+1) {
|
||||
width: 66.666vw;
|
||||
height: 66.666vw;
|
||||
}
|
||||
|
||||
.tile:nth-child(6n+4),
|
||||
.tile:nth-child(6n+5),
|
||||
.tile:nth-child(6n+6) {
|
||||
float: inline-end;
|
||||
}
|
||||
|
||||
.tile:nth-child(1),
|
||||
.tile:nth-child(2) {
|
||||
border-top: solid 0.1rem var(--background);
|
||||
}
|
||||
|
||||
.tile:nth-child(6n+1) {
|
||||
border-right: solid 0.1rem var(--background);
|
||||
}
|
||||
|
||||
.tile:nth-child(6n+4) {
|
||||
border-left: solid 0.1rem var(--background);
|
||||
}
|
||||
|
||||
.tile:before,
|
||||
.tile:after {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
text-shadow: 0 0.1rem rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 0.15rem 1rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2.8rem;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
.tile.loaded:not(.show-title):before,
|
||||
.tile.loaded:not(.show-title):after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tile:before {
|
||||
content: attr(data-artist);
|
||||
color: #fff;
|
||||
font-size: 1.9rem;
|
||||
line-height: 2.8rem;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.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.loaded > img {
|
||||
opacity: 1;
|
||||
}
|
||||
297
gaia/apps/music/views/home/view.js
Executable file
297
gaia/apps/music/views/home/view.js
Executable file
|
|
@ -0,0 +1,297 @@
|
|||
/* 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 `
|
||||
<a class="${album.metadata.picture ? 'tile' : 'tile show-title'}" dir="auto"
|
||||
href="/player"
|
||||
data-artist="${album.metadata.artist || unknownArtist}"
|
||||
data-album="${album.metadata.album || unknownAlbum}"
|
||||
data-file-path="${album.name}">
|
||||
<img>
|
||||
</a>`;
|
||||
|
||||
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();
|
||||
36
gaia/apps/music/views/player/index.html
Executable file
36
gaia/apps/music/views/player/index.html
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="music">Music</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<link rel="stylesheet" href="view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/shared/js/moz_intl.js"></script>
|
||||
<script defer src="/shared/js/intl_helper.js"></script>
|
||||
<script defer src="/elements/music-artwork.js"></script>
|
||||
<script defer src="/elements/music-controls.js"></script>
|
||||
<script defer src="/elements/music-rating.js"></script>
|
||||
<script defer src="/elements/music-seek-bar.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<music-artwork id="artwork"></music-artwork>
|
||||
<music-seek-bar id="seek-bar"></music-seek-bar>
|
||||
<music-controls id="controls"></music-controls>
|
||||
</body>
|
||||
</html>
|
||||
5
gaia/apps/music/views/player/view.css
Executable file
5
gaia/apps/music/views/player/view.css
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
music-artwork {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: calc(100% - 9.1rem);
|
||||
}
|
||||
156
gaia/apps/music/views/player/view.js
Executable file
156
gaia/apps/music/views/player/view.js
Executable file
|
|
@ -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() {
|
||||
this.onRenderDone();
|
||||
};
|
||||
|
||||
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();
|
||||
40
gaia/apps/music/views/playlist-detail/index.html
Executable file
40
gaia/apps/music/views/playlist-detail/index.html
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="playlists2">Playlists</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<link rel="stylesheet" href="view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/shared/js/intl_helper.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<gaia-fast-list id="list" top="50" bottom="0">
|
||||
<template>
|
||||
<a href="/player" data-file-path="${name}" data-number="${index}">
|
||||
<h3 dir="auto">${title}</h3>
|
||||
<p dir="auto">${artist}</p>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
</body>
|
||||
</html>
|
||||
3
gaia/apps/music/views/playlist-detail/view.css
Executable file
3
gaia/apps/music/views/playlist-detail/view.css
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
gaia-fast-list em {
|
||||
line-height: 0;
|
||||
}
|
||||
65
gaia/apps/music/views/playlist-detail/view.js
Executable file
65
gaia/apps/music/views/playlist-detail/view.js
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
/* global View, IntlHelper */
|
||||
'use strict';
|
||||
|
||||
var PlaylistDetailView = View.extend(function PlaylistDetailView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
this.list.addEventListener('click', (evt) => {
|
||||
var link = evt.target.closest('a[data-file-path]');
|
||||
if (link) {
|
||||
this.queuePlaylist(link.dataset.filePath);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
|
||||
this.update();
|
||||
});
|
||||
|
||||
PlaylistDetailView.prototype.update = function() {
|
||||
return this.getPlaylist().then((songs) => {
|
||||
this.songs = songs;
|
||||
return this.render();
|
||||
});
|
||||
};
|
||||
|
||||
PlaylistDetailView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
PlaylistDetailView.prototype.render = function() {
|
||||
return this.list.setModel(this.songs)
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
PlaylistDetailView.prototype.getPlaylist = function() {
|
||||
var unpaddedIndex = IntlHelper.get('unpaddedIndex');
|
||||
|
||||
return this.fetch('/api/playlists/info/' + this.params.id)
|
||||
.then(response => response.json())
|
||||
.then(songs => {
|
||||
return songs.map((song, index) => {
|
||||
return {
|
||||
index: unpaddedIndex.format(index + 1),
|
||||
name: song.name,
|
||||
title: song.metadata.title,
|
||||
artist: song.metadata.artist
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
PlaylistDetailView.prototype.queuePlaylist = function(filePath) {
|
||||
this.fetch('/api/queue/playlist/' + this.params.id + '/song/' + filePath);
|
||||
};
|
||||
|
||||
IntlHelper.define('unpaddedIndex', 'number', {
|
||||
style: 'decimal',
|
||||
useGrouping: false,
|
||||
minimumIntegerDigits: 1
|
||||
});
|
||||
|
||||
window.view = new PlaylistDetailView();
|
||||
43
gaia/apps/music/views/playlists/index.html
Executable file
43
gaia/apps/music/views/playlists/index.html
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="playlists2">Playlists</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/components/gaia-sub-header/gaia-sub-header.js"></script>
|
||||
<script defer src="/components/gaia-text-input/gaia-text-input.js"></script>
|
||||
<script defer src="/elements/music-search-box.js"></script>
|
||||
<script defer src="/elements/music-search-results.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application">
|
||||
<gaia-fast-list id="list" top="50" bottom="0">
|
||||
<music-search-box id="search-box" results="#search-results"></music-search-box>
|
||||
<template>
|
||||
<a href="/playlist-detail?id=${id}" data-id="${id}" data-shuffle="${shuffle}">
|
||||
<h3 dir="auto">${title}</h3>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
<music-search-results id="search-results"></music-search-results>
|
||||
</body>
|
||||
</html>
|
||||
170
gaia/apps/music/views/playlists/view.js
Executable file
170
gaia/apps/music/views/playlists/view.js
Executable file
|
|
@ -0,0 +1,170 @@
|
|||
/* global View */
|
||||
'use strict';
|
||||
|
||||
var PlaylistsView = View.extend(function PlaylistsView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.searchBox = document.getElementById('search-box');
|
||||
this.searchResults = document.getElementById('search-results');
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
this.searchBox.addEventListener('search', (evt) => this.search(evt.detail));
|
||||
|
||||
this.searchResults.addEventListener('open', () => {
|
||||
this.client.method('searchOpen');
|
||||
});
|
||||
|
||||
this.searchResults.addEventListener('close', () => {
|
||||
this.client.method('searchClose');
|
||||
this.list.scrollTop = this.searchBox.HEIGHT;
|
||||
});
|
||||
|
||||
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.list.minScrollHeight = `calc(100% + ${this.searchBox.HEIGHT}px)`;
|
||||
this.list.offset = this.searchBox.HEIGHT;
|
||||
|
||||
this.list.addEventListener('click', (evt) => {
|
||||
var link = evt.target.closest('a[data-shuffle="true"]');
|
||||
if (link) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
this.queuePlaylist(link.dataset.id);
|
||||
|
||||
this.client.method('navigate', '/player');
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
|
||||
this.update()
|
||||
.then(() => this.list.scrollTo(this.searchBox.HEIGHT));
|
||||
});
|
||||
|
||||
PlaylistsView.prototype.update = function() {
|
||||
return this.getPlaylists().then((playlists) => {
|
||||
return Promise.all(playlists.map((playlist) => {
|
||||
return document.l10n.formatValue('playlists-' + playlist.id);
|
||||
}))
|
||||
|
||||
.then((titles) => {
|
||||
titles.forEach((title, index) => playlists[index].title = title);
|
||||
this.playlists = playlists;
|
||||
return this.render();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
PlaylistsView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
PlaylistsView.prototype.render = function() {
|
||||
return this.list.setModel(this.playlists)
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
PlaylistsView.prototype.getPlaylists = function() {
|
||||
return this.fetch('/api/playlists/list').then(response => response.json());
|
||||
};
|
||||
|
||||
PlaylistsView.prototype.queuePlaylist = function(id) {
|
||||
this.fetch('/api/queue/playlist/' + id + '/shuffle');
|
||||
};
|
||||
|
||||
PlaylistsView.prototype.queueSong = function(filePath) {
|
||||
this.fetch('/api/queue/song/' + filePath);
|
||||
};
|
||||
|
||||
PlaylistsView.prototype.getThumbnail = function(filePath) {
|
||||
return this.fetch('/api/artwork/url/thumbnail/' + filePath)
|
||||
.then(response => response.json());
|
||||
};
|
||||
|
||||
PlaylistsView.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;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.view = new PlaylistsView();
|
||||
45
gaia/apps/music/views/songs/index.html
Executable file
45
gaia/apps/music/views/songs/index.html
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="theme-media">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-l10n-id="songs">Songs</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="/locales/music.{locale}.properties">
|
||||
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/components/gaia-icons/gaia-icons.css">
|
||||
<link rel="stylesheet" href="/components/gaia-theme/gaia-theme.css">
|
||||
<link rel="stylesheet" href="/css/view.css">
|
||||
<!-- Deferred Scripts -->
|
||||
<script defer src="/components/bridge/client.js"></script>
|
||||
<script defer src="/shared/js/intl/l20n-client.js"></script>
|
||||
<script defer src="/components/dom-scheduler/lib/dom-scheduler.js"></script>
|
||||
<script defer src="/components/fast-list/fast-list.js"></script>
|
||||
<script defer src="/components/poplar/poplar.js"></script>
|
||||
<script defer src="/components/gaia-component/gaia-component.js"></script>
|
||||
<script defer src="/components/gaia-fast-list/gaia-fast-list.js"></script>
|
||||
<script defer src="/components/gaia-sub-header/gaia-sub-header.js"></script>
|
||||
<script defer src="/components/gaia-text-input/gaia-text-input.js"></script>
|
||||
<script defer src="/elements/music-search-box.js"></script>
|
||||
<script defer src="/elements/music-search-results.js"></script>
|
||||
<script defer src="/js/config.js"></script>
|
||||
<script defer src="/js/view.js"></script>
|
||||
<script defer src="view.js"></script>
|
||||
</head>
|
||||
<body role="application" hidden>
|
||||
<gaia-fast-list id="list" top="50" bottom="0" caching>
|
||||
<music-search-box id="search-box" results="#search-results"></music-search-box>
|
||||
<template>
|
||||
<a href="/player" data-file-path="${name}">
|
||||
<div class="image"><img/></div>
|
||||
<h3 dir="auto">${title}</h3>
|
||||
<p dir="auto">${artist}</p>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>
|
||||
<music-search-results id="search-results"></music-search-results>
|
||||
</body>
|
||||
</html>
|
||||
125
gaia/apps/music/views/songs/view.js
Executable file
125
gaia/apps/music/views/songs/view.js
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
/* global View */
|
||||
'use strict';
|
||||
|
||||
var SongsView = View.extend(function SongsView() {
|
||||
View.call(this); // super();
|
||||
|
||||
this.searchBox = document.getElementById('search-box');
|
||||
this.searchResults = document.getElementById('search-results');
|
||||
this.list = document.getElementById('list');
|
||||
|
||||
this.client.on('databaseChange', () => this.update());
|
||||
|
||||
this.setupSearch();
|
||||
this.setupList();
|
||||
this.update();
|
||||
});
|
||||
|
||||
SongsView.prototype.setupSearch = function() {
|
||||
this.searchBox.addEventListener('search', (evt) => this.search(evt.detail));
|
||||
|
||||
this.searchResults.addEventListener('open', () => {
|
||||
this.client.method('searchOpen');
|
||||
});
|
||||
|
||||
this.searchResults.addEventListener('close', () => {
|
||||
this.client.method('searchClose');
|
||||
this.list.scrollTop = this.searchBox.HEIGHT;
|
||||
});
|
||||
|
||||
this.searchResults.addEventListener('resultclick', (evt) => {
|
||||
var link = evt.detail;
|
||||
if (link) {
|
||||
this.queueSong(link.dataset.filePath);
|
||||
this.client.method('navigate', link.getAttribute('href'));
|
||||
}
|
||||
});
|
||||
|
||||
this.searchResults.getItemImageSrc = (item) => this.getThumbnail(item.name);
|
||||
};
|
||||
|
||||
SongsView.prototype.setupList = function() {
|
||||
View.prototype.setupList.call(this);
|
||||
|
||||
// Triggers player service to begin playing the track.
|
||||
// This works for now, but we might have the PlayerView
|
||||
// take care of this task as it's a big more webby :)
|
||||
this.list.addEventListener('click', (evt) => {
|
||||
var link = evt.target.closest('a[data-file-path]');
|
||||
if (link) {
|
||||
this.queueSong(link.dataset.filePath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SongsView.prototype.update = function() {
|
||||
return this.getSongs().then((songs) => {
|
||||
this.songs = songs;
|
||||
return this.render();
|
||||
});
|
||||
};
|
||||
|
||||
SongsView.prototype.destroy = function() {
|
||||
this.client.destroy();
|
||||
View.prototype.destroy.call(this); // super(); // Always call *last*
|
||||
};
|
||||
|
||||
SongsView.prototype.render = function() {
|
||||
return this.list.setModel(this.songs)
|
||||
.then(() => this.list.cache())
|
||||
.then(this.onRenderDone);
|
||||
};
|
||||
|
||||
SongsView.prototype.getSongs = function() {
|
||||
return document.l10n.formatValues('unknownTitle', 'unknownArtist')
|
||||
.then(([unknownTitle, unknownArtist]) => {
|
||||
return this.fetch('/api/songs/list')
|
||||
.then(response => response.json())
|
||||
.then((songs) => {
|
||||
return songs.map((song) => {
|
||||
return {
|
||||
name: song.name,
|
||||
title: song.metadata.title || unknownTitle,
|
||||
artist: song.metadata.artist || unknownArtist,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
SongsView.prototype.queueSong = function(filePath) {
|
||||
this.fetch('/api/queue/song/' + filePath);
|
||||
};
|
||||
|
||||
SongsView.prototype.getThumbnail = function(filePath) {
|
||||
return this.fetch('/api/artwork/url/thumbnail/' + filePath)
|
||||
.then(response => response.json());
|
||||
};
|
||||
|
||||
SongsView.prototype.search = function(query) {
|
||||
if (!query) {
|
||||
return Promise.resolve(this.searchResults.clearResults());
|
||||
}
|
||||
|
||||
return document.l10n.formatValues(
|
||||
'unknownTitle', 'unknownArtist'
|
||||
).then(([unknownTitle, unknownArtist]) => {
|
||||
return this.fetch('/api/search/title/' + query)
|
||||
.then(response => response.json())
|
||||
.then((songs) => {
|
||||
var results = songs.map((song) => {
|
||||
return {
|
||||
name: song.name,
|
||||
title: song.metadata.title || unknownTitle,
|
||||
subtitle: song.metadata.artist || unknownArtist,
|
||||
section: 'songs',
|
||||
url: '/player'
|
||||
};
|
||||
});
|
||||
|
||||
return this.searchResults.setResults(results);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.view = new SongsView();
|
||||
Loading…
Reference in a new issue