VulpesOS/gaia/apps/music/elements/music-seek-bar.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

270 lines
6.3 KiB
JavaScript
Executable file

/* 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);