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
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();
|
||||
Loading…
Reference in a new issue