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
363
gaia/apps/music/elements/music-artwork.js
Executable file
363
gaia/apps/music/elements/music-artwork.js
Executable file
|
|
@ -0,0 +1,363 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
const HIDE_OVERLAY_TIMEOUT = 5000;
|
||||
|
||||
const REPEAT_VALUES = ['off', 'list', 'song'];
|
||||
const SHUFFLE_VALUES = ['off', 'on'];
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML =
|
||||
`<style>
|
||||
[data-icon]:before { /* Copied from /components/gaia-icons/gaia-icons.css */
|
||||
font-family: "gaia-icons";
|
||||
content: attr(data-icon);
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
text-transform: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 30px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#container {
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#container > img {
|
||||
background-color: #000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
#container > img[data-layer="front"] {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 150ms linear, visibility 0s linear 150ms;
|
||||
}
|
||||
#container > img[data-layer="front"].active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition-delay: 0s, 0s;
|
||||
}
|
||||
#container button {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: #fff;
|
||||
flex: 0 0 auto;
|
||||
margin: 0 1rem;
|
||||
width: 6rem;
|
||||
transition: background 200ms ease;
|
||||
}
|
||||
#container button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
#container button:active {
|
||||
background: #00caf2;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
#container button:disabled,
|
||||
#container button[data-value="off"] {
|
||||
opacity: 0.3;
|
||||
}
|
||||
#container button:disabled:active {
|
||||
background: transparent;
|
||||
}
|
||||
#container button[data-icon="repeat"][data-value="song"]:before {
|
||||
content: 'repeat-once'
|
||||
}
|
||||
#caption,
|
||||
#controls {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
visibility: hidden;
|
||||
transition: transform 300ms ease-in-out, visibility 0s linear 300ms;
|
||||
}
|
||||
#caption {
|
||||
top: 0;
|
||||
transform: translateY(-5rem);
|
||||
}
|
||||
#controls {
|
||||
bottom: 0;
|
||||
transform: translateY(5rem);
|
||||
}
|
||||
.show-overlay > #caption,
|
||||
.show-overlay > #controls {
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
transition-delay: 0s, 0s;
|
||||
}
|
||||
#caption-text {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - 8rem);
|
||||
}
|
||||
#artist,
|
||||
#album {
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
text-shadow: 0 0.1rem rgba(0, 0, 0, 0.5);
|
||||
text-indent: 3rem;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#artist {
|
||||
font-size: 1.9rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
#album {
|
||||
font-size: 1.7rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
#rating {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
</style>
|
||||
<div id="container">
|
||||
<img data-layer="back">
|
||||
<img data-layer="front" class="active">
|
||||
<div id="caption">
|
||||
<div id="caption-text">
|
||||
<h1 id="artist"></h1>
|
||||
<h2 id="album"></h2>
|
||||
</div>
|
||||
<button type="button"
|
||||
data-action="share"
|
||||
data-icon="share"
|
||||
data-l10n-id="share-song">
|
||||
</button>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<button type="button"
|
||||
data-action="repeat"
|
||||
data-icon="repeat"
|
||||
data-l10n-id="repeat-off">
|
||||
</button>
|
||||
<music-rating id="rating"></music-rating>
|
||||
<button type="button"
|
||||
data-action="shuffle"
|
||||
data-icon="shuffle"
|
||||
data-l10n-id="shuffle-toggle">
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
var $ = shadowRoot.querySelector.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
container: $('#container'),
|
||||
backImage: $('img[data-layer="back"]'),
|
||||
frontImage: $('img[data-layer="front"]'),
|
||||
caption: $('#caption'),
|
||||
controls: $('#controls'),
|
||||
artist: $('#artist'),
|
||||
album: $('#album'),
|
||||
share: $('[data-action="share"]'),
|
||||
repeat: $('[data-action="repeat"]'),
|
||||
shuffle: $('[data-action="shuffle"]'),
|
||||
rating: $('#rating')
|
||||
};
|
||||
|
||||
var onImageLoad = (evt) => {
|
||||
const image = evt.target;
|
||||
window.requestAnimationFrame(() => {
|
||||
var newActiveImage = image.closest('img:not(.active)');
|
||||
if (!newActiveImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldActiveImage = this.els.container.querySelector('img.active');
|
||||
newActiveImage.classList.add('active');
|
||||
oldActiveImage.classList.remove('active');
|
||||
});
|
||||
};
|
||||
|
||||
[].forEach.call(this.els.container.querySelectorAll('img'), (img) => {
|
||||
img.addEventListener('load', onImageLoad);
|
||||
});
|
||||
|
||||
this.els.container.addEventListener('transitionend', (evt) => {
|
||||
if (!evt.target.matches('img:not(.active)')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldActiveImage = evt.target;
|
||||
oldActiveImage.src = null;
|
||||
});
|
||||
|
||||
this.els.container.addEventListener('click', (evt) => {
|
||||
var button = evt.target.closest('button');
|
||||
if (!button) {
|
||||
if (evt.target.closest('#rating')) {
|
||||
this.overlayVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.overlayVisible = !this.overlayVisible;
|
||||
return;
|
||||
}
|
||||
|
||||
var action = button.dataset.action;
|
||||
switch (action) {
|
||||
case 'repeat':
|
||||
this.nextRepeat();
|
||||
break;
|
||||
case 'shuffle':
|
||||
this.nextShuffle();
|
||||
break;
|
||||
}
|
||||
|
||||
this.overlayVisible = true;
|
||||
this.dispatchEvent(new CustomEvent(action));
|
||||
});
|
||||
|
||||
this.els.rating.addEventListener('change', (evt) => {
|
||||
this.dispatchEvent(new CustomEvent('ratingchange', {
|
||||
detail: evt.detail
|
||||
}));
|
||||
});
|
||||
|
||||
this.onDOMRetranslated = () => {
|
||||
document.l10n.translateFragment(shadowRoot);
|
||||
};
|
||||
|
||||
this.repeat = this.getAttribute('repeat');
|
||||
this.shuffle = this.getAttribute('shuffle');
|
||||
|
||||
this.overlayVisible = true;
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
document.addEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
this.onDOMRetranslated();
|
||||
};
|
||||
|
||||
proto.detachedCallback = function() {
|
||||
document.removeEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
};
|
||||
|
||||
proto.attributeChangedCallback = function(attr, oldVal, newVal) {
|
||||
switch (attr) {
|
||||
case 'artist':
|
||||
this.els.artist.textContent = newVal;
|
||||
break;
|
||||
case 'album':
|
||||
this.els.album.textContent = newVal;
|
||||
break;
|
||||
case 'repeat':
|
||||
this.els.repeat.dataset.value =
|
||||
REPEAT_VALUES.indexOf(newVal) !== -1 ? newVal : REPEAT_VALUES[0];
|
||||
break;
|
||||
case 'shuffle':
|
||||
this.els.shuffle.dataset.value =
|
||||
SHUFFLE_VALUES.indexOf(newVal) !== -1 ? newVal : SHUFFLE_VALUES[0];
|
||||
break;
|
||||
case 'src':
|
||||
this.els.container.querySelector('img:not(.active)').src = newVal;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.nextRepeat = function() {
|
||||
this.repeat = REPEAT_VALUES[REPEAT_VALUES.indexOf(this.repeat) + 1];
|
||||
};
|
||||
|
||||
proto.nextShuffle = function() {
|
||||
this.shuffle = SHUFFLE_VALUES[SHUFFLE_VALUES.indexOf(this.shuffle) + 1];
|
||||
};
|
||||
|
||||
['artist', 'album', 'src'].forEach(function(prop) {
|
||||
Object.defineProperty(proto, prop, {
|
||||
get: function() {
|
||||
return this.getAttribute(prop);
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
this.setAttribute(prop, value || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'repeat', {
|
||||
get: function() {
|
||||
return this.getAttribute('repeat') || REPEAT_VALUES[0];
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
value = REPEAT_VALUES.indexOf(value) !== -1 ? value : REPEAT_VALUES[0];
|
||||
this.setAttribute('repeat', value);
|
||||
|
||||
this.els.repeat.dataset.l10nId = 'repeat-' + value;
|
||||
|
||||
this.onDOMRetranslated();
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'shuffle', {
|
||||
get: function() {
|
||||
return this.getAttribute('shuffle') || SHUFFLE_VALUES[0];
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
value = SHUFFLE_VALUES.indexOf(value) !== -1 ? value : SHUFFLE_VALUES[0];
|
||||
this.setAttribute('shuffle', value);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'overlayVisible', {
|
||||
get: function() {
|
||||
return this.els.container.classList.contains('show-overlay');
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
var overlayVisible = !!value;
|
||||
|
||||
clearTimeout(this._hideOverlayTimeout);
|
||||
|
||||
if (overlayVisible) {
|
||||
this.els.container.classList.add('show-overlay');
|
||||
this._hideOverlayTimeout = setTimeout(() => {
|
||||
this.overlayVisible = false;
|
||||
}, HIDE_OVERLAY_TIMEOUT);
|
||||
}
|
||||
|
||||
else {
|
||||
this.els.container.classList.remove('show-overlay');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
window.MusicArtwork = document.registerElement('music-artwork', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
170
gaia/apps/music/elements/music-controls.js
Executable file
170
gaia/apps/music/elements/music-controls.js
Executable file
|
|
@ -0,0 +1,170 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
var isTouch = 'ontouchstart' in window;
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML =
|
||||
`<style>
|
||||
[data-icon]:before { /* Copied from /components/gaia-icons/gaia-icons.css */
|
||||
font-family: "gaia-icons";
|
||||
content: attr(data-icon);
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
text-transform: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 30px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#container {
|
||||
background-color: #000;
|
||||
border-top: 0.1rem solid rgba(255, 255, 255, 0.1);
|
||||
direction: ltr;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 4.8rem;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#container > button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: #fff;
|
||||
flex: 1 0 auto;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
#container > button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
#container > button:active {
|
||||
background: #00caf2;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
#container > button:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
#container > button:disabled:active {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
<div id="container">
|
||||
<button type="button" id="previous"
|
||||
data-icon="skip-back"
|
||||
data-l10n-id="playbackPrevious">
|
||||
</button>
|
||||
<button type="button" id="toggle"
|
||||
data-icon="play"
|
||||
data-l10n-id="playbackPlay">
|
||||
</button>
|
||||
<button type="button" id="next"
|
||||
data-icon="skip-forward"
|
||||
data-l10n-id="playbackNext">
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
var $id = shadowRoot.getElementById.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
container: $id('container'),
|
||||
previous: $id('previous'),
|
||||
toggle: $id('toggle'),
|
||||
next: $id('next')
|
||||
};
|
||||
|
||||
var seeking = false;
|
||||
|
||||
this.els.container.addEventListener('contextmenu', (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
if (seeking) {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = evt.target.closest('button');
|
||||
if (button.id === 'previous' || button.id === 'next') {
|
||||
seeking = true;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('startseek', {
|
||||
detail: { reverse: button.id === 'previous' }
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
this.els.container.addEventListener(isTouch ? 'touchend' : 'mouseup',
|
||||
(evt) => {
|
||||
if (seeking) {
|
||||
evt.preventDefault();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('stopseek'));
|
||||
seeking = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.els.container.addEventListener('click', (evt) => {
|
||||
var button = evt.target.closest('button');
|
||||
switch (button.id) {
|
||||
case 'previous':
|
||||
case 'next':
|
||||
this.dispatchEvent(new CustomEvent(button.id));
|
||||
break;
|
||||
case 'toggle':
|
||||
this.paused = !this.paused;
|
||||
this.dispatchEvent(new CustomEvent(this.paused ? 'pause' : 'play'));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.onDOMRetranslated = () => {
|
||||
document.l10n.translateFragment(shadowRoot);
|
||||
};
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
document.addEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
this.onDOMRetranslated();
|
||||
};
|
||||
|
||||
proto.detachedCallback = function() {
|
||||
document.removeEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, 'paused', {
|
||||
get: function() {
|
||||
return this.els.toggle.dataset.icon !== 'pause';
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
var paused = !!value;
|
||||
if (paused === this.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.els.toggle.dataset.icon = paused ? 'play' : 'pause';
|
||||
this.els.toggle.dataset.l10nId = paused ? 'playbackPlay' : 'playbackPause';
|
||||
|
||||
this.onDOMRetranslated();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
window.MusicControls = document.registerElement('music-controls', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
236
gaia/apps/music/elements/music-overlay.js
Executable file
236
gaia/apps/music/elements/music-overlay.js
Executable file
|
|
@ -0,0 +1,236 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var buttons = document.createDocumentFragment();
|
||||
[].forEach.call(this.querySelectorAll('button'), (button) => {
|
||||
buttons.appendChild(button);
|
||||
});
|
||||
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML =
|
||||
`<style>
|
||||
#heading:empty,
|
||||
#message:empty,
|
||||
#menu:empty {
|
||||
display: none;
|
||||
}
|
||||
#container {
|
||||
background-color: #2d2d2d;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
flex: 1 0 auto;
|
||||
padding: 1rem 1.5rem;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
vertical-align: middle;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
#heading {
|
||||
color: #fff;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.6rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#message {
|
||||
border-top: 0.1rem solid #686868;
|
||||
color: #fafafa;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 300;
|
||||
line-height: 3rem;
|
||||
margin: 1rem 0 0;
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
#heading:empty + #message {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
#menu {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
#menu > button {
|
||||
background: #d8d8d8;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
color: #333;
|
||||
outline: none;
|
||||
font-family: sans-serif;
|
||||
font-size: 1.6rem;
|
||||
font-style: italic;
|
||||
line-height: 4rem;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 0.2rem;
|
||||
height: 4rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: background 200ms 280ms, color 200ms 280ms;
|
||||
}
|
||||
#menu > button:active {
|
||||
background: #00aacc;
|
||||
color: #fff;
|
||||
transition: none;
|
||||
}
|
||||
#menu > button.primary {
|
||||
background: #00caf2;
|
||||
color: #fff;
|
||||
}
|
||||
#menu > button.danger {
|
||||
background: #e51e1e;
|
||||
color: #fff;
|
||||
}
|
||||
#menu > button.primary:active {
|
||||
background: #006579;
|
||||
color: #c8c8c8;
|
||||
}
|
||||
#menu > button.danger:active {
|
||||
background: #730f0f;
|
||||
color: #c8c8c8;
|
||||
}
|
||||
#menu > button[disabled] {
|
||||
background: #565656;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
#menu > button.primary[disabled] {
|
||||
background: #006579;
|
||||
}
|
||||
#menu > button.danger[disabled] {
|
||||
background: #730f0f;
|
||||
}
|
||||
#menu > button + button {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
<form id="container" role="dialog">
|
||||
<section id="content">
|
||||
<h1 id="heading"></h1>
|
||||
<p id="message"></p>
|
||||
</section>
|
||||
<menu id="menu"></menu>
|
||||
</form>`;
|
||||
|
||||
var $id = shadowRoot.getElementById.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
container: $id('container'),
|
||||
content: $id('content'),
|
||||
heading: $id('heading'),
|
||||
message: $id('message'),
|
||||
menu: $id('menu')
|
||||
};
|
||||
|
||||
this.els.menu.appendChild(buttons);
|
||||
|
||||
this.els.menu.addEventListener('click', (evt) => {
|
||||
var button = evt.target.closest('button[data-action]');
|
||||
if (button) {
|
||||
this.dispatchEvent(new CustomEvent('action', {
|
||||
detail: button.dataset.action
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
this.els.heading.textContent = this.getAttribute('heading');
|
||||
this.els.message.textContent = this.getAttribute('message');
|
||||
|
||||
this.els.heading.dataset.l10nId = this.getAttribute('heading-l10n-id');
|
||||
this.els.message.dataset.l10nId = this.getAttribute('message-l10n-id');
|
||||
|
||||
this.onDOMRetranslated = () => {
|
||||
document.l10n.translateFragment(shadowRoot);
|
||||
};
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
document.addEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
this.onDOMRetranslated();
|
||||
};
|
||||
|
||||
proto.detachedCallback = function() {
|
||||
document.removeEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
};
|
||||
|
||||
proto.attributeChangedCallback = function(attr, oldVal, newVal) {
|
||||
switch (attr) {
|
||||
case 'heading':
|
||||
this.els.heading.textContent = newVal;
|
||||
break;
|
||||
case 'message':
|
||||
this.els.message.textContent = newVal;
|
||||
break;
|
||||
case 'heading-l10n-id':
|
||||
this.els.heading.dataset.l10nId = newVal;
|
||||
this.onDOMRetranslated();
|
||||
break;
|
||||
case 'message-l10n-id':
|
||||
this.els.message.dataset.l10nId = newVal;
|
||||
this.onDOMRetranslated();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.addActionButton = function(title, action, className = '') {
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = title;
|
||||
button.dataset.action = action;
|
||||
button.className = className;
|
||||
|
||||
this.els.menu.appendChild(button);
|
||||
};
|
||||
|
||||
proto.removeActionButton = function(action) {
|
||||
var buttons = this.els.menu.querySelectorAll(`[data-action="${action}"]`);
|
||||
|
||||
[].forEach.call(buttons, button => this.els.menu.removeChild(button));
|
||||
};
|
||||
|
||||
['heading', 'message'].forEach((prop) => {
|
||||
Object.defineProperty(proto, prop, {
|
||||
get: function() {
|
||||
return this.getAttribute(prop);
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
this.setAttribute(prop, value || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
window.MusicOverlay = document.registerElement('music-overlay', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
115
gaia/apps/music/elements/music-rating.js
Executable file
115
gaia/apps/music/elements/music-rating.js
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
(function(window) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML =
|
||||
`<style>
|
||||
#container {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#container > button {
|
||||
background-color: transparent;
|
||||
background-image: url(/img/ui/music-rating-off.png);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 2.2rem auto;
|
||||
border: none;
|
||||
flex: 1 0 auto;
|
||||
width: 2.2rem;
|
||||
height: 100%;
|
||||
}
|
||||
#container[data-value="1"] > :-moz-any([value="1"]),
|
||||
#container[data-value="2"] > :-moz-any([value="1"],[value="2"]),
|
||||
#container[data-value="3"] > :-moz-any([value="1"],[value="2"],[value="3"]),
|
||||
#container[data-value="4"] > :-moz-any([value="1"],[value="2"],[value="3"],[value="4"]),
|
||||
#container[data-value="5"] > * {
|
||||
background-image: url(/img/ui/music-rating-on.png);
|
||||
}
|
||||
</style>
|
||||
<div id="container">
|
||||
<button type="button" value="1" data-l10n-id="rating-star" data-l10n-args='{"n":1}'></button>
|
||||
<button type="button" value="2" data-l10n-id="rating-star" data-l10n-args='{"n":2}'></button>
|
||||
<button type="button" value="3" data-l10n-id="rating-star" data-l10n-args='{"n":3}'></button>
|
||||
<button type="button" value="4" data-l10n-id="rating-star" data-l10n-args='{"n":4}'></button>
|
||||
<button type="button" value="5" data-l10n-id="rating-star" data-l10n-args='{"n":5}'></button>
|
||||
</div>`;
|
||||
|
||||
var $id = shadowRoot.getElementById.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
container: $id('container')
|
||||
};
|
||||
|
||||
this.els.container.addEventListener('click', (evt) => {
|
||||
var button = evt.target.closest('button');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
var value = parseInt(button.value, 10) || 0;
|
||||
if (value === this.value) {
|
||||
value--;
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: value
|
||||
}));
|
||||
});
|
||||
|
||||
this.onDOMRetranslated = () => {
|
||||
document.l10n.translateFragment(shadowRoot);
|
||||
};
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
document.addEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
this.onDOMRetranslated();
|
||||
};
|
||||
|
||||
proto.detachedCallback = function() {
|
||||
document.removeEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
};
|
||||
|
||||
proto.attributeChangedCallback = function(attr, oldVal, newVal) {
|
||||
switch (attr) {
|
||||
case 'value':
|
||||
this.els.container.dataset.value = newVal;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, 'value', {
|
||||
get: function() {
|
||||
return parseInt(this.getAttribute('value'), 10) || 0;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
this.setAttribute('value', clamp(0, 5, parseInt(value, 10) || 0));
|
||||
}
|
||||
});
|
||||
|
||||
function clamp(min, max, value) {
|
||||
return Math.min(Math.max(min, value), max);
|
||||
}
|
||||
|
||||
try {
|
||||
window.MusicRating = document.registerElement('music-rating', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
184
gaia/apps/music/elements/music-scan-progress.js
Executable file
184
gaia/apps/music/elements/music-scan-progress.js
Executable file
|
|
@ -0,0 +1,184 @@
|
|||
(function(window) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
injectGlobalStyles();
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML =
|
||||
`<style scoped>
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
#container {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
overflow: hidden;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#spinner {
|
||||
background: url('/img/ui/music-scan-progress.png') no-repeat center center / 100% auto;
|
||||
border: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
width: 2.9rem;
|
||||
height: 2.9rem;
|
||||
animation: 0.9s music-scan-progress-spinner infinite linear;
|
||||
}
|
||||
#spinner::-moz-progress-bar {
|
||||
background: none;
|
||||
}
|
||||
#value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
line-height: 5rem;
|
||||
text-align: center;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
#heading,
|
||||
#subheading {
|
||||
font-weight: normal;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0 0.5rem;
|
||||
left: 5rem;
|
||||
width: calc(100% - 5rem);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#heading {
|
||||
font-size: 1.9rem;
|
||||
line-height: 3rem;
|
||||
top: 0;
|
||||
}
|
||||
#subheading {
|
||||
font-size: 1.4rem;
|
||||
top: 3rem;
|
||||
}
|
||||
</style>
|
||||
<div id="container">
|
||||
<progress id="spinner"></progress>
|
||||
<span id="value"></span>
|
||||
<h1 id="heading"></h1>
|
||||
<h2 id="subheading"></h2>
|
||||
</div>`;
|
||||
|
||||
var $id = shadowRoot.getElementById.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
container: $id('container'),
|
||||
spinner: $id('spinner'),
|
||||
value: $id('value'),
|
||||
heading: $id('heading'),
|
||||
subheading: $id('subheading')
|
||||
};
|
||||
|
||||
if (!this.hasAttribute('value') && !this.hasAttribute('heading') &&
|
||||
!this.hasAttribute('subheading')) {
|
||||
this.els.container.hidden = true;
|
||||
}
|
||||
|
||||
this.value = this.getAttribute('value');
|
||||
this.heading = this.getAttribute('heading');
|
||||
this.subheading = this.getAttribute('subheading');
|
||||
|
||||
this.onDOMRetranslated = () => {
|
||||
document.l10n.translateFragment(shadowRoot);
|
||||
};
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
document.addEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
this.onDOMRetranslated();
|
||||
};
|
||||
|
||||
proto.detachedCallback = function() {
|
||||
document.removeEventListener('DOMRetranslated', this.onDOMRetranslated);
|
||||
};
|
||||
|
||||
proto.attributeChangedCallback = function(attr, oldVal, newVal) {
|
||||
var promise;
|
||||
var l10nId = this.getAttribute(attr + '-l10n-id');
|
||||
|
||||
if (!newVal && l10nId) {
|
||||
promise = document.l10n.formatValues(l10nId);
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
promise.then(() => {
|
||||
switch (attr) {
|
||||
case 'value':
|
||||
this.els.value.textContent = newVal;
|
||||
break;
|
||||
case 'heading':
|
||||
this.els.heading.textContent = newVal;
|
||||
break;
|
||||
case 'subheading':
|
||||
this.els.subheading.textContent = newVal;
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
proto.update = function(properties = {}) {
|
||||
this.value = properties.value || this.value;
|
||||
this.heading = properties.heading || this.heading;
|
||||
this.subheading = properties.subheading || this.subheading;
|
||||
|
||||
this.els.container.hidden = false;
|
||||
};
|
||||
|
||||
proto.clear = function() {
|
||||
this.els.container.hidden = true;
|
||||
};
|
||||
|
||||
['value', 'heading', 'subheading'].forEach((prop) => {
|
||||
Object.defineProperty(proto, prop, {
|
||||
get: function() {
|
||||
return this.getAttribute(prop);
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
this.setAttribute(prop, value || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function injectGlobalStyles() {
|
||||
var style = document.createElement('style');
|
||||
style.innerHTML =
|
||||
`@keyframes music-scan-progress-spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
try {
|
||||
window.MusicScanProgress = document.registerElement('music-scan-progress', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
190
gaia/apps/music/elements/music-search-box.js
Executable file
190
gaia/apps/music/elements/music-search-box.js
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
(function(window) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.HEIGHT = 52; // 12px margin + 40px height
|
||||
|
||||
// #form height attribute is the value of proto.HEIGHT
|
||||
proto.createdCallback = function() {
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML = `
|
||||
<style>
|
||||
#inner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
gaia-text-input {
|
||||
margin: 6px 0 !important;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#close {
|
||||
display: none;
|
||||
padding: 0 14px;
|
||||
-moz-margin-end: -14px;
|
||||
border: 0;
|
||||
|
||||
background: none;
|
||||
color: var(--highlight-color);
|
||||
font-style: italic;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.open #close {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<div id="inner">
|
||||
<gaia-text-input type="search" clearable id="input"></gaia-text-input>
|
||||
<button id="close" data-l10n-id="search-close">Close</button>
|
||||
</div>`;
|
||||
|
||||
var $id = shadowRoot.getElementById.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
inner: $id('inner'),
|
||||
input: $id('input'),
|
||||
close: $id('close')
|
||||
};
|
||||
|
||||
this.isOpen = false;
|
||||
|
||||
this.els.results = document.querySelector(this.getAttribute('results'));
|
||||
|
||||
var onSearch = debounce(() => {
|
||||
this.emit('search', this.els.input.value);
|
||||
}, 500);
|
||||
|
||||
this.els.input.addEventListener('focus', () => this.open());
|
||||
this.els.close.addEventListener('click', () => this.close());
|
||||
this.els.input.addEventListener('input', onSearch);
|
||||
this.addEventListener('touchmove', e => e.preventDefault());
|
||||
|
||||
/**
|
||||
* Localizes the component's DOM.
|
||||
*
|
||||
* We can't use data-l10n-id attributes
|
||||
* here as l10n.js doesn't support putting
|
||||
* `placeholder` attribute on any element
|
||||
* other than `<input>`.
|
||||
*
|
||||
* Because we have to use `.formatValues()` it
|
||||
* makes sense to localize `search-close` in
|
||||
* the same way.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
this.localize = () => {
|
||||
document.l10n.formatValues('search', 'search-close')
|
||||
.then(([search, close]) => {
|
||||
this.els.input.placeholder = search;
|
||||
this.els.close.textContent = close;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
document.addEventListener('DOMRetranslated', this.localize);
|
||||
this.localize();
|
||||
};
|
||||
|
||||
proto.detachedCallback = function() {
|
||||
document.removeEventListener('DOMRetranslated', this.localize);
|
||||
};
|
||||
|
||||
proto.attributeChangedCallback = function(attr, oldVal, newVal) {
|
||||
switch (attr) {
|
||||
case 'results':
|
||||
this.els.results = document.querySelector(newVal);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.clear = function() {
|
||||
requestAnimationFrame(() => {
|
||||
this.els.input.clear();
|
||||
});
|
||||
};
|
||||
|
||||
proto.focus = function() {
|
||||
requestAnimationFrame(() => {
|
||||
this.els.input.focus();
|
||||
});
|
||||
};
|
||||
|
||||
proto.open = function() {
|
||||
if (this.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpen = true;
|
||||
this.els.inner.classList.add('open');
|
||||
this.focus();
|
||||
this.emit('open');
|
||||
|
||||
if (this.els.results) {
|
||||
this.els.results.open();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
proto.close = function() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpen = false;
|
||||
this.els.inner.classList.remove('open');
|
||||
this.clear();
|
||||
this.emit('close');
|
||||
|
||||
if (this.els.results) {
|
||||
this.els.results.close();
|
||||
}
|
||||
};
|
||||
|
||||
proto.emit = function(name, detail) {
|
||||
var evt = new CustomEvent(name, { detail: detail });
|
||||
this.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, 'value', {
|
||||
get: function() {
|
||||
return this.els.input.value;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
this.els.input.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'results', {
|
||||
get: function() {
|
||||
return this.els.results;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
this.els.results = value;
|
||||
}
|
||||
});
|
||||
|
||||
window.MusicSearchBox = document.registerElement('music-search-box', {
|
||||
prototype: proto
|
||||
});
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
function debounce(fn, ms) {
|
||||
var timeout;
|
||||
return () => {
|
||||
var args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
})(window);
|
||||
124
gaia/apps/music/elements/music-search-results.js
Executable file
124
gaia/apps/music/elements/music-search-results.js
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
(function(window) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.createdCallback = function() {
|
||||
this.innerHTML = `
|
||||
<style scoped>
|
||||
music-search-results {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
music-search-results.transitions-on {
|
||||
transition:
|
||||
opacity 0.2s linear,
|
||||
visibility 0s linear 0.2s;
|
||||
}
|
||||
|
||||
music-search-results[active] {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition-delay: 0s, 0s;
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
<gaia-fast-list id="list">
|
||||
<template>
|
||||
<a href="\${url}" data-file-path="\${name}" data-section="\${section}">
|
||||
<div class="image"><img></div>
|
||||
<h3 dir="auto">\${title}</h3>
|
||||
<p dir="auto">\${subtitle}</p>
|
||||
</a>
|
||||
</template>
|
||||
</gaia-fast-list>`;
|
||||
|
||||
this.els = {
|
||||
list: this.querySelector('#list')
|
||||
};
|
||||
|
||||
this.els.list.configure({
|
||||
getSectionName: (item) => item.section,
|
||||
getItemImageSrc: (item) => this.getItemImageSrc(item)
|
||||
});
|
||||
|
||||
this.els.list.addEventListener('click', (e) => this.onLinkClick(e));
|
||||
};
|
||||
|
||||
proto.attachedCallback = function() {
|
||||
setTimeout(() => this.enableTransitions());
|
||||
};
|
||||
|
||||
proto.enableTransitions = function() {
|
||||
this.classList.add('transitions-on');
|
||||
};
|
||||
|
||||
proto.onLinkClick = function(evt) {
|
||||
var link = evt.target.closest('a');
|
||||
if (!link) return;
|
||||
evt.preventDefault();
|
||||
|
||||
this.close().then(() => {
|
||||
this.emit('resultclick', link);
|
||||
});
|
||||
};
|
||||
|
||||
// To be overriden by view
|
||||
// if images are required
|
||||
proto.getItemImageSrc = function() {};
|
||||
|
||||
proto.open = function() {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
this.setAttribute('active', true);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.emit('open');
|
||||
});
|
||||
};
|
||||
|
||||
proto.close = function() {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
this.removeAttribute('active');
|
||||
this.clearResults();
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.emit('close');
|
||||
});
|
||||
};
|
||||
|
||||
proto.setResults = function(results) {
|
||||
if (results.length === 0) {
|
||||
return document.l10n.formatValue('search-no-result')
|
||||
.then((noResult) => {
|
||||
return this.els.list.setModel([{
|
||||
title: noResult,
|
||||
subtitle: ''
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
return this.els.list.setModel(results);
|
||||
};
|
||||
|
||||
proto.clearResults = function() {
|
||||
return this.els.list.setModel([]);
|
||||
};
|
||||
|
||||
proto.emit = function(name, detail) {
|
||||
var evt = new CustomEvent(name, { detail: detail });
|
||||
this.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
window.MusicSearchResults = document.registerElement('music-search-results', {
|
||||
prototype: proto
|
||||
});
|
||||
|
||||
})(window);
|
||||
270
gaia/apps/music/elements/music-seek-bar.js
Executable file
270
gaia/apps/music/elements/music-seek-bar.js
Executable file
|
|
@ -0,0 +1,270 @@
|
|||
/* global IntlHelper */
|
||||
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
var isTouch = 'ontouchstart' in window;
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadowRoot = this.createShadowRoot();
|
||||
shadowRoot.innerHTML =
|
||||
`<style>
|
||||
#container {
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 4.2rem;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#container > span {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
#elapsed-time,
|
||||
#remaining-time {
|
||||
color: #e7e7e7;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: 4.2rem;
|
||||
direction: ltr;
|
||||
vertical-align: top;
|
||||
flex: 0 0 auto;
|
||||
width: 5.3rem;
|
||||
}
|
||||
#elapsed-time {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
#remaining-time {
|
||||
padding-right: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#seek-bar {
|
||||
flex: 1 0 auto;
|
||||
z-index: 1;
|
||||
}
|
||||
#seek-bar-progress {
|
||||
background-color: #a6b4b7;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
position: absolute;
|
||||
top: calc(50% - 0.1rem);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.1rem;
|
||||
pointer-events: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
#seek-bar-progress::-moz-progress-bar {
|
||||
background-color: #01c5ed;
|
||||
}
|
||||
#seek-bar-indicator {
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: calc(50% - 3rem);
|
||||
left: -3rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
pointer-events: none;
|
||||
transition: transform 20ms linear;
|
||||
will-change: transform;
|
||||
}
|
||||
#seek-bar-indicator:after {
|
||||
content: '';
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 1.85rem;
|
||||
left: 1.85rem;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
}
|
||||
#seek-bar-indicator.highlight {
|
||||
background-color: #00caf2;
|
||||
}
|
||||
</style>
|
||||
<div id="container">
|
||||
<span id="elapsed-time"></span>
|
||||
<span id="seek-bar">
|
||||
<progress id="seek-bar-progress"></progress>
|
||||
<div id="seek-bar-indicator"></div>
|
||||
</span>
|
||||
<span id="remaining-time"></span>
|
||||
</div>`;
|
||||
|
||||
var $id = shadowRoot.getElementById.bind(shadowRoot);
|
||||
|
||||
this.els = {
|
||||
container: $id('container'),
|
||||
elapsedTime: $id('elapsed-time'),
|
||||
seekBar: $id('seek-bar'),
|
||||
seekBarProgress: $id('seek-bar-progress'),
|
||||
seekBarIndicator: $id('seek-bar-indicator'),
|
||||
remainingTime: $id('remaining-time')
|
||||
};
|
||||
|
||||
var container = this.els.container;
|
||||
var seekBar = this.els.seekBar;
|
||||
|
||||
var seekTimeout = null;
|
||||
|
||||
container.addEventListener(isTouch ? 'touchstart' : 'mousedown', (evt) => {
|
||||
clearTimeout(seekTimeout);
|
||||
|
||||
container.addEventListener(isTouch ? 'touchmove' : 'mousemove',
|
||||
pointerMoveHandler);
|
||||
container.addEventListener(isTouch ? 'touchend' : 'mouseup',
|
||||
pointerEndHandler);
|
||||
|
||||
this.els.seekBarIndicator.classList.add('highlight');
|
||||
|
||||
pointerMoveHandler(evt);
|
||||
});
|
||||
|
||||
var pointerMoveHandler = (evt) => {
|
||||
var pointer = isTouch ? evt.targetTouches[0] : evt;
|
||||
var percent = clamp(0, 1,
|
||||
(pointer.clientX - seekBar.offsetLeft) / seekBar.offsetWidth);
|
||||
|
||||
if (document.documentElement.dir === 'rtl') {
|
||||
this.remainingTime = this._overrideRemainingTime =
|
||||
percent * this.duration;
|
||||
}
|
||||
|
||||
else {
|
||||
this.elapsedTime = this._overrideElapsedTime =
|
||||
percent * this.duration;
|
||||
}
|
||||
};
|
||||
|
||||
var pointerEndHandler = (evt) => {
|
||||
this.dispatchEvent(new CustomEvent('seek', {
|
||||
detail: { elapsedTime: this.elapsedTime }
|
||||
}));
|
||||
|
||||
container.removeEventListener(isTouch ? 'touchmove' : 'mousemove',
|
||||
pointerMoveHandler);
|
||||
container.removeEventListener(isTouch ? 'touchend' : 'mouseup',
|
||||
pointerEndHandler);
|
||||
|
||||
seekTimeout = setTimeout(() => {
|
||||
this._overrideRemainingTime = null;
|
||||
this._overrideElapsedTime = null;
|
||||
|
||||
this.els.seekBarIndicator.classList.remove('highlight');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this._overrideRemainingTime = null;
|
||||
this._overrideElapsedTime = null;
|
||||
|
||||
this.duration = null;
|
||||
this.elapsedTime = null;
|
||||
this.remainingTime = null;
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, 'duration', {
|
||||
get: function() {
|
||||
return this._duration;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
if (isNaN(value)) {
|
||||
this._duration = null;
|
||||
this.remainingTime = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this._duration = value;
|
||||
this.remainingTime = this._duration - this._elapsedTime;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'elapsedTime', {
|
||||
get: function() {
|
||||
return this._elapsedTime;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
if (isNaN(value)) {
|
||||
this._elapsedTime = null;
|
||||
this.remainingTime = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this._elapsedTime = this._overrideElapsedTime !== null ?
|
||||
this._overrideElapsedTime : value;
|
||||
this.remainingTime = this._duration - this._elapsedTime;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'remainingTime', {
|
||||
get: function() {
|
||||
return this._remainingTime;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
var indeterminate = isNaN(value) || isNaN(this._duration);
|
||||
if (indeterminate) {
|
||||
this._remainingTime = null;
|
||||
this._elapsedTime = null;
|
||||
}
|
||||
|
||||
else {
|
||||
this._remainingTime = this._overrideRemainingTime !== null ?
|
||||
this._overrideRemainingTime : value;
|
||||
this._elapsedTime = this._duration - this._remainingTime;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
if (indeterminate) {
|
||||
this.els.remainingTime.textContent = '---:--';
|
||||
this.els.elapsedTime.textContent = '--:--';
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = this._duration ? this._elapsedTime / this._duration : 0;
|
||||
var x = this.els.seekBar.offsetWidth * percent;
|
||||
|
||||
if (document.documentElement.dir === 'rtl') {
|
||||
x = this.els.seekBar.offsetWidth - x;
|
||||
}
|
||||
|
||||
this.els.seekBarIndicator.style.transform = 'translateX(' + x + 'px)';
|
||||
|
||||
IntlHelper.get('duration').then((duration) => {
|
||||
this.els.remainingTime.textContent =
|
||||
duration.format(-this._remainingTime * 1000);
|
||||
this.els.elapsedTime.textContent =
|
||||
duration.format(this._elapsedTime * 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function clamp(min, max, value) {
|
||||
return Math.min(Math.max(min, value), max);
|
||||
}
|
||||
|
||||
IntlHelper.define('duration', 'mozduration', {
|
||||
minUnit: 'second',
|
||||
maxUnit: 'minute'
|
||||
});
|
||||
|
||||
try {
|
||||
window.MusicSeekBar = document.registerElement('music-seek-bar', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
104
gaia/apps/music/elements/music-tab-bar.js
Executable file
104
gaia/apps/music/elements/music-tab-bar.js
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var GaiaToolbar = window['gaia-toolbar'];
|
||||
|
||||
var proto = Object.create(GaiaToolbar.prototype);
|
||||
|
||||
proto.createdCallback = function() {
|
||||
GaiaToolbar.prototype.createdCallback.apply(this, arguments);
|
||||
|
||||
this.querySelector('style').innerHTML +=
|
||||
`music-tab-bar {
|
||||
background-color: var(--background-minus);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.-content {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.-content > *,
|
||||
.more-button {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.-content > .selected {
|
||||
color: var(--highlight-color);
|
||||
}`;
|
||||
|
||||
this.addEventListener('click', (evt) => {
|
||||
var tab = evt.target.closest('button');
|
||||
if (!tab || tab === this.selectedElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedElement = tab;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: {
|
||||
selectedElement: this.selectedElement,
|
||||
selectedIndex: this.selectedIndex
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
this.selectedIndex = 0;
|
||||
|
||||
// Fixes issue where rAF-wrapped `reflow()` doesn't get
|
||||
// called in the inherited <gaia-toolbar> `createdCallback()`.
|
||||
this.reflow();
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, 'selectedElement', {
|
||||
get: function() {
|
||||
return this.querySelectorAll('button')[this.selectedIndex];
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
var index = [].indexOf.call(this.querySelectorAll('button'), value);
|
||||
if (index !== -1) {
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'selectedIndex', {
|
||||
get: function() {
|
||||
return this._selectedIndex || 0;
|
||||
},
|
||||
|
||||
set: function(value) {
|
||||
var tabs = this.querySelectorAll('button');
|
||||
if (this._selectedIndex === value || value < 0 || value >= tabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectedIndex = value;
|
||||
|
||||
[].forEach.call(tabs, (tab, index) => {
|
||||
if (index === this._selectedIndex) {
|
||||
tab.classList.add('selected');
|
||||
return;
|
||||
}
|
||||
|
||||
tab.classList.remove('selected');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
window.MusicTabBar = document.registerElement('music-tab-bar', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
})(window);
|
||||
305
gaia/apps/music/elements/music-view-stack.js
Executable file
305
gaia/apps/music/elements/music-view-stack.js
Executable file
|
|
@ -0,0 +1,305 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var style = document.createElement('style');
|
||||
style.innerHTML =
|
||||
`music-view-stack {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
music-view-stack > iframe {
|
||||
border: none;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
music-view-stack > iframe.active,
|
||||
music-view-stack > iframe.fade,
|
||||
music-view-stack > iframe.pop,
|
||||
music-view-stack > iframe.push {
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: linear;
|
||||
visibility: visible;
|
||||
}
|
||||
music-view-stack > iframe.active {
|
||||
pointer-events: auto;
|
||||
}
|
||||
music-view-stack > iframe.fade {
|
||||
animation-duration: 0s;
|
||||
}
|
||||
music-view-stack > iframe.fade.in {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
music-view-stack > iframe.fade.out {
|
||||
animation-name: fade-out;
|
||||
}
|
||||
music-view-stack > iframe.pop.in {
|
||||
animation-name: pop-in;
|
||||
}
|
||||
music-view-stack > iframe.pop.out {
|
||||
animation-name: pop-out;
|
||||
}
|
||||
music-view-stack > iframe.push.in {
|
||||
animation-name: push-in;
|
||||
}
|
||||
music-view-stack > iframe.push.out {
|
||||
animation-name: push-out;
|
||||
}
|
||||
[dir="rtl"] music-view-stack > iframe.pop.in {
|
||||
animation-name: push-in;
|
||||
}
|
||||
[dir="rtl"] music-view-stack > iframe.pop.out {
|
||||
animation-name: push-out;
|
||||
}
|
||||
[dir="rtl"] music-view-stack > iframe.push.in {
|
||||
animation-name: pop-in;
|
||||
}
|
||||
[dir="rtl"] music-view-stack > iframe.push.out {
|
||||
animation-name: pop-out;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@keyframes fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes pop-in {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
@keyframes pop-out {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
@keyframes push-in {
|
||||
0% { transform: translateX(100%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
@keyframes push-out {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}`;
|
||||
|
||||
if (this.firstChild) {
|
||||
this.insertBefore(style, this.firstChild);
|
||||
} else {
|
||||
this.appendChild(style);
|
||||
}
|
||||
|
||||
this.cachedViews = {};
|
||||
|
||||
[].forEach.call(this.querySelectorAll('iframe'), (frame) => {
|
||||
var location = new URL(frame.src);
|
||||
|
||||
var view = {
|
||||
url: location.pathname + location.search,
|
||||
frame: frame,
|
||||
title: frame.contentDocument.title
|
||||
};
|
||||
|
||||
this.initializeView(view).then((view) => {
|
||||
if (this.activeView === view) {
|
||||
this.dispatchEvent(new CustomEvent('titlechange', {
|
||||
detail: view.title
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
this.cachedViews[view.url] = view;
|
||||
});
|
||||
|
||||
this.addEventListener('animationend', (evt) => {
|
||||
var classList = evt.target.classList;
|
||||
if (classList.contains('destroy')) {
|
||||
this.destroyView(evt.target.src);
|
||||
}
|
||||
|
||||
classList.remove('fade', 'pop', 'push', 'in', 'out');
|
||||
});
|
||||
|
||||
this.views = [];
|
||||
this.activeView = null;
|
||||
};
|
||||
|
||||
proto.loadView = function(url) {
|
||||
var view = this.cachedViews[url];
|
||||
if (view) {
|
||||
return Promise.resolve(view);
|
||||
}
|
||||
|
||||
var frame = document.createElement('iframe');
|
||||
this.appendChild(frame);
|
||||
frame.src = url;
|
||||
|
||||
view = {
|
||||
url: url,
|
||||
frame: frame,
|
||||
title: ''
|
||||
};
|
||||
|
||||
return this.initializeView(view);
|
||||
};
|
||||
|
||||
proto.initializeView = function(view) {
|
||||
view.frame.addEventListener('rendered', () => {
|
||||
this.dispatchEvent(new CustomEvent('rendered'));
|
||||
});
|
||||
|
||||
view.frame.addEventListener('titlechange', (evt) => {
|
||||
view.title = evt.detail;
|
||||
|
||||
if (this.activeView === view) {
|
||||
this.dispatchEvent(new CustomEvent('titlechange', {
|
||||
detail: evt.detail
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
const onL10nReady = () => {
|
||||
view.title = view.frame.contentDocument.title;
|
||||
this.cachedViews[view.url] = view;
|
||||
this.dispatchEvent(new CustomEvent('loaded'));
|
||||
return view;
|
||||
};
|
||||
|
||||
return loaded(view.frame)
|
||||
.then(() => view.frame.contentDocument.l10n.ready)
|
||||
.then(onL10nReady);
|
||||
};
|
||||
|
||||
proto.destroyView = function(url) {
|
||||
for (var key in this.cachedViews) {
|
||||
var view = this.cachedViews[key];
|
||||
if (key === url || view.frame.src === url) {
|
||||
view.frame.contentWindow.dispatchEvent(new CustomEvent('viewdestroy'));
|
||||
delete this.cachedViews[key];
|
||||
|
||||
setTimeout(() => this.removeChild(view.frame), 5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
proto.setRootView = function(url) {
|
||||
return this.loadView(url).then((view) => {
|
||||
var oldActiveView = this.activeView;
|
||||
if (oldActiveView) {
|
||||
oldActiveView.frame.classList.add('fade', 'out');
|
||||
oldActiveView.frame.classList.remove('active');
|
||||
oldActiveView.frame.contentWindow.dispatchEvent(
|
||||
new CustomEvent('viewhidden')
|
||||
);
|
||||
}
|
||||
|
||||
this.views = [view];
|
||||
this.activeView = view;
|
||||
|
||||
var newActiveView = this.activeView;
|
||||
newActiveView.frame.classList.add('fade', 'in');
|
||||
newActiveView.frame.classList.add('active');
|
||||
newActiveView.frame.contentWindow.dispatchEvent(
|
||||
new CustomEvent('viewvisible')
|
||||
);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: newActiveView }));
|
||||
});
|
||||
};
|
||||
|
||||
proto.pushView = function(url) {
|
||||
if (!this.activeView) {
|
||||
return this.setRootView(url);
|
||||
}
|
||||
|
||||
return this.loadView(url).then((view) => {
|
||||
requestAnimationFrame(() => {
|
||||
var oldActiveView = this.activeView;
|
||||
oldActiveView.frame.classList.add('push', 'out');
|
||||
oldActiveView.frame.classList.remove('active');
|
||||
oldActiveView.frame.contentWindow.dispatchEvent(
|
||||
new CustomEvent('viewhidden')
|
||||
);
|
||||
|
||||
this.views.push(view);
|
||||
this.activeView = view;
|
||||
|
||||
var newActiveView = this.activeView;
|
||||
newActiveView.frame.classList.add('push', 'in', 'active');
|
||||
newActiveView.frame.contentWindow.dispatchEvent(
|
||||
new CustomEvent('viewvisible')
|
||||
);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: newActiveView }));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
proto.popView = function(destroy) {
|
||||
if (this.views.length < 2) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
var oldActiveView = this.activeView;
|
||||
|
||||
this.views.pop();
|
||||
this.activeView = this.views[this.views.length - 1];
|
||||
|
||||
var newActiveView = this.activeView;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
oldActiveView.frame.classList.add('pop', 'out');
|
||||
oldActiveView.frame.classList.remove('active');
|
||||
oldActiveView.frame.contentWindow.dispatchEvent(
|
||||
new CustomEvent('viewhidden')
|
||||
);
|
||||
|
||||
if (destroy) {
|
||||
oldActiveView.frame.classList.add('destroy');
|
||||
}
|
||||
|
||||
newActiveView.frame.classList.add('pop', 'in', 'active');
|
||||
newActiveView.frame.contentWindow.dispatchEvent(
|
||||
new CustomEvent('viewvisible')
|
||||
);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: newActiveView }));
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
window.MusicViewStack = document.registerElement('music-view-stack', {
|
||||
prototype: proto
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotSupportedError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function loaded(frame) {
|
||||
return new Promise((resolve) => {
|
||||
frame.addEventListener('load', function onLoad() {
|
||||
frame.removeEventListener('load', onLoad);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
})(window);
|
||||
Loading…
Reference in a new issue