player: progress, meter
This commit is contained in:
parent
e75ce14ec0
commit
ed1eeadad5
9 changed files with 120 additions and 44 deletions
47
app/app.css
47
app/app.css
|
@ -159,17 +159,52 @@ nav ul li.active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#player progress {
|
#player .timeline {
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#player .controls {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
#player .controls .icon {
|
#player .timeline .duration,
|
||||||
|
#player .timeline .elapsed {
|
||||||
|
flex-basis: 5ch;
|
||||||
|
}
|
||||||
|
#player .timeline .duration {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#player .timeline progress {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
#player .controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: 160px;
|
||||||
|
}
|
||||||
|
#player .controls .playback {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
#player .controls .playback .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
margin: 8px;
|
}
|
||||||
|
#player .controls .playback .icon-play,
|
||||||
|
#player .controls .playback .icon-pause {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
#player .controls .volume {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#player .controls .volume .icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
#player .controls .volume meter {
|
||||||
|
flex-grow: 1;
|
||||||
|
vertical-align: top;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
#player .misc {
|
#player .misc {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -29,17 +29,53 @@
|
||||||
|
|
||||||
.title, .subtitle { .long-line; }
|
.title, .subtitle { .long-line; }
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
.flex-row;
|
||||||
|
|
||||||
|
.duration, .elapsed {
|
||||||
|
flex-basis: 5ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration { text-align: right; }
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
width: 100%;
|
flex-grow: 1;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
.flex-column;
|
||||||
|
flex-basis: 64px + 2*48px;
|
||||||
|
|
||||||
|
.playback {
|
||||||
.flex-row;
|
.flex-row;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
margin: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-play, .icon-pause {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume {
|
||||||
|
.flex-row;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
meter {
|
||||||
|
flex-grow: 1;
|
||||||
|
vertical-align: top;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.misc {
|
.misc {
|
||||||
|
|
1
app/icons/volume-high.svg
Normal file
1
app/icons/volume-high.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" /></svg>
|
After Width: | Height: | Size: 510 B |
|
@ -13,14 +13,23 @@
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h2 class="title"></h2>
|
<h2 class="title"></h2>
|
||||||
<div class="subtitle"></div>
|
<div class="subtitle"></div>
|
||||||
<progress class="elapsed"></progress>
|
<div class="timeline">
|
||||||
|
<span class="elapsed"></span>
|
||||||
|
<progress></progress>
|
||||||
|
<span class="duration"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<div class="playback">
|
||||||
<button class="prev" data-icon="rewind"></button>
|
<button class="prev" data-icon="rewind"></button>
|
||||||
<button class="play" data-icon="play"></button>
|
<button class="play" data-icon="play"></button>
|
||||||
<button class="pause" data-icon="pause"></button>
|
<button class="pause" data-icon="pause"></button>
|
||||||
<button class="next" data-icon="fast-forward"></button>
|
<button class="next" data-icon="fast-forward"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="volume">
|
||||||
|
<meter max="100"></meter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="misc">
|
<div class="misc">
|
||||||
<button class="repeat" data-icon="repeat"></button>
|
<button class="repeat" data-icon="repeat"></button>
|
||||||
<button class="random" data-icon="shuffle"></button>
|
<button class="random" data-icon="shuffle"></button>
|
||||||
|
@ -52,10 +61,6 @@
|
||||||
</section>
|
</section>
|
||||||
<section id="settings">
|
<section id="settings">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Volume</dt>
|
|
||||||
<dd>
|
|
||||||
<input type="range" name="volume" min="0" max="100" step="1" />
|
|
||||||
</dd>
|
|
||||||
<dt>Theme</dt>
|
<dt>Theme</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<select name="theme">
|
<select name="theme">
|
||||||
|
|
|
@ -7,10 +7,10 @@ export function time(sec) {
|
||||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subtitle(data) {
|
export function subtitle(data, options = {duration:true}) {
|
||||||
let tokens = [];
|
let tokens = [];
|
||||||
data["Artist"] && tokens.push(data["Artist"]);
|
data["Artist"] && tokens.push(data["Artist"]);
|
||||||
data["Album"] && tokens.push(data["Album"]);
|
data["Album"] && tokens.push(data["Album"]);
|
||||||
data["duration"] && tokens.push(time(Number(data["duration"])));
|
options.duration && data["duration"] && tokens.push(time(Number(data["duration"])));
|
||||||
return tokens.join(SEPARATOR);
|
return tokens.join(SEPARATOR);
|
||||||
}
|
}
|
|
@ -29,6 +29,9 @@ ICONS["close-circle"] = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="ht
|
||||||
ICONS["minus"] = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
|
ICONS["minus"] = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
|
||||||
<path d="M19,13H5V11H19V13Z"/>
|
<path d="M19,13H5V11H19V13Z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
ICONS["volume-high"] = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
|
||||||
|
<path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z"/>
|
||||||
|
</svg>`;
|
||||||
ICONS["shuffle"] = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
|
ICONS["shuffle"] = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
|
||||||
<path d="M14.83,13.41L13.42,14.82L16.55,17.95L14.5,20H20V14.5L17.96,16.54L14.83,13.41M14.5,4L16.54,6.04L4,18.59L5.41,20L17.96,7.46L20,9.5V4M10.59,9.17L5.41,4L4,5.41L9.17,10.58L10.59,9.17Z"/>
|
<path d="M14.83,13.41L13.42,14.82L16.55,17.95L14.5,20H20V14.5L17.96,16.54L14.83,13.41M14.5,4L16.54,6.04L4,18.59L5.41,20L17.96,7.46L20,9.5V4M10.59,9.17L5.41,4L4,5.41L9.17,10.58L10.59,9.17Z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
|
|
@ -63,16 +63,14 @@ export async function command(cmd) {
|
||||||
export async function commandAndStatus(cmd) {
|
export async function commandAndStatus(cmd) {
|
||||||
let lines = await command([cmd, "status", "currentsong"]);
|
let lines = await command([cmd, "status", "currentsong"]);
|
||||||
let status = parser.linesToStruct(lines);
|
let status = parser.linesToStruct(lines);
|
||||||
// duration returned 2x => arrayfied
|
if (status["duration"] instanceof Array) { status["duration"] = status["duration"][0]; }
|
||||||
if ("duration" in status) { status["duration"] = status["duration"][0]; }
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function status() {
|
export async function status() {
|
||||||
let lines = await command(["status", "currentsong"]);
|
let lines = await command(["status", "currentsong"]);
|
||||||
let status = parser.linesToStruct(lines);
|
let status = parser.linesToStruct(lines);
|
||||||
// duration returned 2x => arrayfied
|
if (status["duration"] instanceof Array) { status["duration"] = status["duration"][0]; }
|
||||||
if ("duration" in status) { status["duration"] = status["duration"][0]; }
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import * as art from "./lib/art.js";
|
||||||
import * as html from "./lib/html.js";
|
import * as html from "./lib/html.js";
|
||||||
import * as format from "./lib/format.js";
|
import * as format from "./lib/format.js";
|
||||||
import * as pubsub from "./lib/pubsub.js";
|
import * as pubsub from "./lib/pubsub.js";
|
||||||
import * as settings from "./settings.js";
|
|
||||||
|
|
||||||
const DELAY = 2000;
|
const DELAY = 2000;
|
||||||
const DOM = {};
|
const DOM = {};
|
||||||
|
@ -13,15 +12,20 @@ let node;
|
||||||
let idleTimeout = null;
|
let idleTimeout = null;
|
||||||
|
|
||||||
function sync(data) {
|
function sync(data) {
|
||||||
settings.notifyVolume(data["volume"]);
|
if ("volume" in data) { DOM.volume.value = data["volume"]; }
|
||||||
|
|
||||||
DOM.elapsed.value = Number(data["elapsed"] || 0); // changed time
|
// changed time
|
||||||
|
let elapsed = Number(data["elapsed"] || 0);
|
||||||
|
DOM.progress.value = elapsed;
|
||||||
|
DOM.elapsed.textContent = format.time(elapsed);
|
||||||
|
|
||||||
if (data["file"] != current["file"]) { // changed song
|
if (data["file"] != current["file"]) { // changed song
|
||||||
if (data["file"]) { // playing at all?
|
if (data["file"]) { // playing at all?
|
||||||
DOM.elapsed.max = Number(data["duration"]);
|
let duration = Number(data["duration"]);
|
||||||
|
DOM.duration.textContent = format.time(duration);
|
||||||
|
DOM.progress.max = duration;
|
||||||
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
||||||
DOM.subtitle.textContent = format.subtitle(data);
|
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
|
||||||
} else {
|
} else {
|
||||||
DOM.title.textContent = "";
|
DOM.title.textContent = "";
|
||||||
DOM.subtitle.textContent = "";
|
DOM.subtitle.textContent = "";
|
||||||
|
@ -79,6 +83,11 @@ export function init(n) {
|
||||||
let all = node.querySelectorAll("[class]");
|
let all = node.querySelectorAll("[class]");
|
||||||
Array.from(all).forEach(node => DOM[node.className] = node);
|
Array.from(all).forEach(node => DOM[node.className] = node);
|
||||||
|
|
||||||
|
DOM.progress = DOM.timeline.querySelector("progress");
|
||||||
|
|
||||||
|
DOM.volume.insertBefore(html.icon("volume-high"), DOM.volume.firstChild);
|
||||||
|
DOM.volume = DOM.volume.querySelector("meter");
|
||||||
|
|
||||||
DOM.play.addEventListener("click", e => command("play"));
|
DOM.play.addEventListener("click", e => command("play"));
|
||||||
DOM.pause.addEventListener("click", e => command("pause 1"));
|
DOM.pause.addEventListener("click", e => command("pause 1"));
|
||||||
DOM.prev.addEventListener("click", e => command("previous"));
|
DOM.prev.addEventListener("click", e => command("previous"));
|
||||||
|
@ -87,7 +96,7 @@ export function init(n) {
|
||||||
DOM.random.addEventListener("click", e => command(`random ${current["random"] == "1" ? "0" : "1"}`));
|
DOM.random.addEventListener("click", e => command(`random ${current["random"] == "1" ? "0" : "1"}`));
|
||||||
DOM.repeat.addEventListener("click", e => command(`repeat ${current["repeat"] == "1" ? "0" : "1"}`));
|
DOM.repeat.addEventListener("click", e => command(`repeat ${current["repeat"] == "1" ? "0" : "1"}`));
|
||||||
|
|
||||||
DOM.elapsed.addEventListener("click", e => {
|
DOM.progress.addEventListener("click", e => {
|
||||||
let rect = e.target.getBoundingClientRect();
|
let rect = e.target.getBoundingClientRect();
|
||||||
let frac = (e.clientX - rect.left) / rect.width;
|
let frac = (e.clientX - rect.left) / rect.width;
|
||||||
command(`seekcur ${frac * e.target.max}`);
|
command(`seekcur ${frac * e.target.max}`);
|
||||||
|
|
|
@ -35,14 +35,6 @@ function setColor(color) {
|
||||||
document.documentElement.dataset.color = color;
|
document.documentElement.dataset.color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setVolume(volume) {
|
|
||||||
mpd.command(`setvol ${volume}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function notifyVolume(volume) {
|
|
||||||
inputs.volume.value = volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function activate() {}
|
export async function activate() {}
|
||||||
|
|
||||||
export function init(n) {
|
export function init(n) {
|
||||||
|
@ -50,7 +42,6 @@ export function init(n) {
|
||||||
|
|
||||||
inputs.theme = n.querySelector("[name=theme]");
|
inputs.theme = n.querySelector("[name=theme]");
|
||||||
inputs.color = Array.from(n.querySelectorAll("[name=color]"));
|
inputs.color = Array.from(n.querySelectorAll("[name=color]"));
|
||||||
inputs.volume = n.querySelector("[name=volume]");
|
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
||||||
|
@ -58,6 +49,4 @@ export function init(n) {
|
||||||
inputs.color.forEach(input => {
|
inputs.color.forEach(input => {
|
||||||
input.addEventListener("click", e => setColor(e.target.value));
|
input.addEventListener("click", e => setColor(e.target.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
inputs.volume.addEventListener("input", e => setVolume(e.target.valueAsNumber));
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue