refactor wip

This commit is contained in:
Ondrej Zara 2020-03-08 22:11:46 +01:00
parent 7a418c4e8a
commit 14569a9415
No known key found for this signature in database
GPG key ID: B0A5751E616840C5
20 changed files with 768 additions and 465 deletions

View file

@ -7,7 +7,9 @@ html {
background-color: var(--fg);
}
body {
display: flex;
margin: 0;
}
cyp-app {
flex-direction: column;
box-sizing: border-box;
font-family: lato, sans-serif;
@ -20,6 +22,9 @@ body {
overflow: hidden;
height: 100vh;
}
cyp-app:not([hidden]) {
display: flex;
}
header,
footer {
z-index: 1;
@ -35,7 +40,6 @@ button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: flex;
flex-direction: row;
align-items: center;
display: inline-flex;
@ -46,6 +50,9 @@ button {
line-height: 1;
cursor: pointer;
}
button:not([hidden]) {
display: flex;
}
select {
background-color: transparent;
border: 1px solid var(--fg);
@ -74,24 +81,30 @@ select {
fill: currentColor;
}
.flex-row {
display: flex;
flex-direction: row;
align-items: center;
}
.flex-column {
.flex-row:not([hidden]) {
display: flex;
}
.flex-column {
flex-direction: column;
}
.flex-column:not([hidden]) {
display: flex;
}
.long-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.multiline {
display: flex;
flex-direction: row;
align-items: center;
}
.multiline:not([hidden]) {
display: flex;
}
.multiline h2 {
font-weight: normal;
}
@ -159,162 +172,173 @@ x-range:not([disabled]) .-thumb:hover {
}
main {
flex-grow: 1;
overflow: hidden;
overflow: auto;
}
nav ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
cyp-menu {
flex-direction: row;
align-items: center;
height: 56px;
}
nav ul li {
flex: 1 0 0;
cyp-menu:not([hidden]) {
display: flex;
}
cyp-menu button {
flex: 1 0 0;
height: 100%;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 4px 0 8px 0;
padding-top: 4px;
border-top: 4px solid transparent;
}
nav ul li .icon {
cyp-menu button:not([hidden]) {
display: flex;
}
cyp-menu button .icon {
margin-right: var(--icon-spacing);
}
nav ul li.active {
cyp-menu button.active {
border-top-color: var(--primary);
color: var(--primary);
background-color: rgb(var(--primary-raw), 0.1);
}
@media (max-width: 480px) {
nav ul li {
cyp-menu button {
flex-direction: row;
justify-content: center;
}
nav ul li:not([data-for=queue]) .icon {
cyp-menu button:not([data-for=queue]) .icon {
margin-right: 0;
}
nav ul li span:not([id]) {
cyp-menu button span:not([id]) {
display: none;
}
}
#player {
display: flex;
cyp-player {
flex-direction: row;
align-items: center;
align-items: stretch;
}
#player:not([data-state=play]) .pause {
cyp-player:not([hidden]) {
display: flex;
}
cyp-player:not([data-state=play]) .pause {
display: none;
}
#player[data-state=play] .play {
cyp-player[data-state=play] .play {
display: none;
}
#player:not([data-flags~=random]) .random,
#player:not([data-flags~=repeat]) .repeat {
cyp-player:not([data-flags~=random]) .random,
cyp-player:not([data-flags~=repeat]) .repeat {
opacity: 0.5;
}
#player x-range {
cyp-player x-range {
flex-grow: 1;
--elapsed-color: var(--primary);
}
#player .art {
cyp-player .art {
margin-right: 0;
height: 96px;
}
#player .art img,
#player .art .icon {
cyp-player .art img,
cyp-player .art .icon {
width: 96px;
}
#player .info {
cyp-player .info {
flex-grow: 2;
flex-basis: 0;
padding: 0 var(--icon-spacing);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-around;
}
#player .info h2 {
cyp-player .info:not([hidden]) {
display: flex;
}
cyp-player .info h2 {
font-size: 125%;
margin: 0;
}
#player .info .title,
#player .info .subtitle {
cyp-player .info .title,
cyp-player .info .subtitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#player .timeline {
display: flex;
cyp-player .timeline {
flex-direction: row;
align-items: center;
}
#player .timeline .duration,
#player .timeline .elapsed {
cyp-player .timeline:not([hidden]) {
display: flex;
}
cyp-player .timeline .duration,
cyp-player .timeline .elapsed {
flex-basis: 5ch;
text-align: center;
}
#player .controls {
cyp-player .controls {
flex-grow: 1;
flex-basis: 0;
max-width: 220px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
#player .controls .playback {
cyp-player .controls:not([hidden]) {
display: flex;
}
cyp-player .controls .playback {
flex-direction: row;
align-items: center;
justify-content: space-around;
}
#player .controls .playback .icon {
cyp-player .controls .playback:not([hidden]) {
display: flex;
}
cyp-player .controls .playback .icon {
width: 40px;
}
#player .controls .playback .icon-play,
#player .controls .playback .icon-pause {
cyp-player .controls .playback .icon-play,
cyp-player .controls .playback .icon-pause {
width: 64px;
}
#player .controls .volume {
display: flex;
cyp-player .controls .volume {
flex-direction: row;
align-items: center;
}
#player .controls .volume .mute {
cyp-player .controls .volume:not([hidden]) {
display: flex;
}
cyp-player .controls .volume .mute {
margin-right: 4px;
}
#player .misc {
cyp-player .misc {
display: flex;
flex-direction: column;
align-self: stretch;
justify-content: space-around;
}
#player .misc .icon {
cyp-player .misc .icon {
width: 32px;
}
@media (max-width: 519px) {
#player {
cyp-player {
flex-wrap: wrap;
justify-content: space-between;
}
#player .info {
cyp-player .info {
order: 1;
flex-basis: 100%;
height: 96px;
}
}
.component {
height: 100%;
display: flex;
flex-direction: column;
}
.component header {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
.component header:not([hidden]) {
display: flex;
}
.component header button {
font-size: var(--font-size-large);
font-weight: bold;
@ -331,10 +355,12 @@ nav ul li.active {
padding: 0;
}
.component li {
display: flex;
flex-direction: row;
align-items: center;
}
.component li:not([hidden]) {
display: flex;
}
.component li .info {
flex-grow: 1;
overflow: hidden;
@ -363,79 +389,75 @@ nav ul li.active {
.component li:nth-child(odd) {
background-color: var(--bg-alt);
}
#queue {
height: 100%;
display: flex;
flex-direction: column;
}
#queue header {
display: flex;
cyp-queue header {
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
#queue header button {
cyp-queue header:not([hidden]) {
display: flex;
}
cyp-queue header button {
font-size: var(--font-size-large);
font-weight: bold;
overflow: hidden;
}
#queue header button .icon {
cyp-queue header button .icon {
margin-right: var(--icon-spacing);
}
#queue ul {
cyp-queue ul {
flex-grow: 1;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
}
#queue li {
display: flex;
cyp-queue li {
flex-direction: row;
align-items: center;
}
#queue li .info {
cyp-queue li:not([hidden]) {
display: flex;
}
cyp-queue li .info {
flex-grow: 1;
overflow: hidden;
}
#queue li .info .icon {
cyp-queue li .info .icon {
color: var(--primary);
margin-right: var(--icon-spacing);
filter: drop-shadow(var(--text-shadow));
}
#queue li .info h2 {
cyp-queue li .info h2 {
font-size: var(--font-size-large);
margin: 0;
}
#queue li .info h2,
#queue li .info div {
cyp-queue li .info h2,
cyp-queue li .info div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#queue li:not(.has-art) {
cyp-queue li:not(.has-art) {
padding: 8px;
}
#queue li button .icon {
cyp-queue li button .icon {
width: 32px;
}
#queue li:nth-child(odd) {
cyp-queue li:nth-child(odd) {
background-color: var(--bg-alt);
}
#queue .current {
cyp-queue .current {
color: var(--primary);
}
#library {
height: 100%;
display: flex;
flex-direction: column;
}
#library header {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
#library header:not([hidden]) {
display: flex;
}
#library header button {
font-size: var(--font-size-large);
font-weight: bold;
@ -452,10 +474,12 @@ nav ul li.active {
padding: 0;
}
#library li {
display: flex;
flex-direction: row;
align-items: center;
}
#library li:not([hidden]) {
display: flex;
}
#library li .info {
flex-grow: 1;
overflow: hidden;
@ -521,17 +545,14 @@ nav ul li.active {
font-size: 150%;
margin: 4px 0;
}
#fs {
height: 100%;
display: flex;
flex-direction: column;
}
#fs header {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
#fs header:not([hidden]) {
display: flex;
}
#fs header button {
font-size: var(--font-size-large);
font-weight: bold;
@ -548,10 +569,12 @@ nav ul li.active {
padding: 0;
}
#fs li {
display: flex;
flex-direction: row;
align-items: center;
}
#fs li:not([hidden]) {
display: flex;
}
#fs li .info {
flex-grow: 1;
overflow: hidden;
@ -593,24 +616,23 @@ nav ul li.active {
cursor: pointer;
}
#fs .info {
display: flex;
flex-direction: row;
align-items: center;
}
#fs .info:not([hidden]) {
display: flex;
}
#fs .info h2 {
font-weight: normal;
}
#playlists {
height: 100%;
display: flex;
flex-direction: column;
}
#playlists header {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
#playlists header:not([hidden]) {
display: flex;
}
#playlists header button {
font-size: var(--font-size-large);
font-weight: bold;
@ -627,10 +649,12 @@ nav ul li.active {
padding: 0;
}
#playlists li {
display: flex;
flex-direction: row;
align-items: center;
}
#playlists li:not([hidden]) {
display: flex;
}
#playlists li .info {
flex-grow: 1;
overflow: hidden;
@ -660,24 +684,23 @@ nav ul li.active {
background-color: var(--bg-alt);
}
#playlists .info {
display: flex;
flex-direction: row;
align-items: center;
}
#playlists .info:not([hidden]) {
display: flex;
}
#playlists .info h2 {
font-weight: normal;
}
#yt {
height: 100%;
display: flex;
flex-direction: column;
}
#yt header {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing);
}
#yt header:not([hidden]) {
display: flex;
}
#yt header button {
font-size: var(--font-size-large);
font-weight: bold;
@ -694,10 +717,12 @@ nav ul li.active {
padding: 0;
}
#yt li {
display: flex;
flex-direction: row;
align-items: center;
}
#yt li:not([hidden]) {
display: flex;
}
#yt li .info {
flex-grow: 1;
overflow: hidden;
@ -755,36 +780,39 @@ nav ul li.active {
background-position: 100% 100%;
}
}
#settings {
cyp-settings {
font-size: var(--font-size-large);
}
#settings dl {
cyp-settings dl {
margin: var(--spacing);
display: grid;
grid-template-columns: max-content 1fr;
align-items: center;
grid-gap: var(--spacing);
}
#settings dt {
cyp-settings dt {
font-weight: bold;
}
#settings dd {
cyp-settings dd {
margin: 0;
display: flex;
flex-direction: column;
align-items: start;
}
#settings label {
cyp-settings dd:not([hidden]) {
display: flex;
}
cyp-settings label {
flex-direction: row;
align-items: center;
}
#settings label [type=radio],
#settings label [type=checkbox] {
cyp-settings label:not([hidden]) {
display: flex;
}
cyp-settings label [type=radio],
cyp-settings label [type=checkbox] {
margin: 0 4px 0 0;
}
.search {
display: flex;
flex-direction: row;
align-items: center;
margin-left: auto;
@ -792,6 +820,9 @@ nav ul li.active {
width: 32px;
max-width: 20ch;
}
.search:not([hidden]) {
display: flex;
}
.search .icon {
width: 32px;
cursor: pointer;
@ -816,32 +847,48 @@ nav ul li.active {
.art img {
vertical-align: top;
}
:root {
cyp-app {
--font-size-large: 112.5%;
--icon-spacing: 4px;
--primary: rgb(var(--primary-raw));
--spacing: 8px;
--box-shadow: 0 0 3px #000;
}
:root[data-theme=light] {
cyp-app[theme=light] {
--fg: #333;
--bg: #f0f0f0;
--bg-alt: #e0e0e0;
--text-shadow: none;
}
:root[data-theme=dark] {
cyp-app[theme=dark] {
--fg: #f0f0f0;
--bg: #333;
--bg-alt: #555;
--text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
}
:root[data-color=dodgerblue] {
@media (prefers-color-scheme: dark) {
cyp-app[theme=auto] {
--fg: #f0f0f0;
--bg: #333;
--bg-alt: #555;
--text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
}
}
@media (prefers-color-scheme: light) {
cyp-app[theme=auto] {
--fg: #333;
--bg: #f0f0f0;
--bg-alt: #e0e0e0;
--text-shadow: none;
}
}
cyp-app[color=dodgerblue] {
--primary-raw: 30, 144, 255;
}
:root[data-color=darkorange] {
cyp-app[color=darkorange] {
--primary-raw: 255, 140, 0;
}
:root[data-color=limegreen] {
cyp-app[color=limegreen] {
--primary-raw: 50, 205, 50;
}
@media (max-width: 480px) {

View file

@ -5,6 +5,10 @@ html {
}
body {
margin: 0;
}
cyp-app {
.flex-column;
box-sizing: border-box;
@ -57,7 +61,7 @@ select {
@import "mixins.less";
@import "range.less";
@import "main.less";
@import "nav.less";
@import "menu.less";
@import "player.less";
@import "component.less";
@import "queue.less";

View file

@ -1,7 +1,4 @@
.component {
height: 100%;
.flex-column;
header {
.flex-row;
padding: var(--spacing);
@ -26,7 +23,7 @@
li {
.flex-row;
.info {
.info {
flex-grow: 1;
overflow: hidden;

View file

@ -1,4 +1,4 @@
main {
flex-grow: 1;
overflow: hidden;
overflow: auto;
}

View file

@ -1,17 +1,14 @@
nav ul {
margin: 0;
padding: 0;
list-style: none;
cyp-menu {
.flex-row;
height: 56px;
li {
button {
flex: 1 0 0;
height: 100%;
.flex-column;
align-items: center;
cursor: pointer;
padding: 4px 0 8px 0;
padding-top: 4px;
border-top: 4px solid transparent;

View file

@ -1,11 +1,11 @@
.flex-row {
display: flex;
&:not([hidden]) { display: flex; }
flex-direction: row;
align-items: center;
}
.flex-column {
display: flex;
&:not([hidden]) { display: flex; }
flex-direction: column;
}

View file

@ -1,4 +1,4 @@
#player {
cyp-player {
@art-size: 96px;
.flex-row;
align-items: stretch;
@ -36,7 +36,7 @@
.title, .subtitle { .long-line; }
}
.timeline {
.flex-row;

View file

@ -1,4 +1,4 @@
#queue {
cyp-queue {
.component;
.current { color: var(--primary); }

View file

@ -1,4 +1,4 @@
#settings {
cyp-settings {
font-size: var(--font-size-large);
dl {
@ -26,4 +26,4 @@
margin: 0 4px 0 0;
}
}
}
}

View file

@ -1,4 +1,4 @@
:root {
cyp-app {
--font-size-large: 112.5%;
--icon-spacing: 4px;
--primary: rgb(var(--primary-raw));
@ -6,29 +6,40 @@
--box-shadow: 0 0 3px #000;
}
:root[data-theme=light] {
.light() {
--fg: #333;
--bg: #f0f0f0;
--bg-alt: #e0e0e0;
--text-shadow: none;
}
:root[data-theme=dark] {
.dark() {
--fg: #f0f0f0;
--bg: #333;
--bg-alt: #555;
--text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
}
:root[data-color=dodgerblue] {
cyp-app[theme=light] { .light(); }
cyp-app[theme=dark] { .dark(); }
@media (prefers-color-scheme: dark) {
cyp-app[theme=auto] { .dark(); }
}
@media (prefers-color-scheme: light) {
cyp-app[theme=auto] { .light(); }
}
cyp-app[color=dodgerblue] {
--primary-raw: 30, 144, 255;
}
:root[data-color=darkorange] {
cyp-app[color=darkorange] {
--primary-raw: 255, 140, 0;
}
:root[data-color=limegreen] {
cyp-app[color=limegreen] {
--primary-raw: 50, 205, 50;
}

View file

@ -7,8 +7,9 @@
<link rel="stylesheet" href="app.css" />
</head>
<body>
<cyp-app component="queue" theme="dark" color="dodgerblue">
<header>
<section id="player">
<cyp-player>
<span class="art"></span>
<div class="info">
<h2 class="title"></h2>
@ -35,16 +36,16 @@
<button class="repeat" data-icon="repeat"></button>
<button class="random" data-icon="shuffle"></button>
</div>
</section>
</cyp-player>
</header>
<main>
<section id="queue">
<cyp-queue>
<header>
<button class="clear" data-icon="close" title="Clear the queue"></button>
<button class="save" data-icon="content-save" title="Save the queue"></button>
</header>
<ul></ul>
</section>
</cyp-queue>
<section id="playlists">
<ul></ul>
</section>
@ -64,11 +65,12 @@
</header>
<pre></pre>
</section>
<section id="settings">
<cyp-settings>
<dl>
<dt>Theme</dt>
<dd>
<select name="theme">
<option value="auto">Auto</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
@ -89,25 +91,24 @@
</label>
</dd>
</dl>
</section>
</cyp-settings>
</main>
<footer>
<nav>
<ul>
<li data-for="queue" data-icon="music">
<div>
<span>Queue</span>
<span id="queue-length"></span>
</div>
</li>
<li data-for="playlists" data-icon="playlist-music"><span>Playlists</span></li>
<li data-for="library" data-icon="library-music"><span>Library</span></li>
<li data-for="fs" data-icon="folder"><span>Files</span></li>
<li data-for="yt" data-icon="download"><span>YouTube</span></li>
<li data-for="settings" data-icon="settings"><span>Settings</span></li>
</ul>
</nav>
</footer>
<cyp-menu>
<button data-for="queue" data-icon="music">
<div>
<span>Queue</span>
<span id="queue-length"></span>
</div>
</button>
<button data-for="playlists" data-icon="playlist-music"><span>Playlists</span></button>
<button data-for="library" data-icon="library-music"><span>Library</span></button>
<button data-for="fs" data-icon="folder"><span>Files</span></button>
<button data-for="yt" data-icon="download"><span>YouTube</span></button>
<button data-for="settings" data-icon="settings"><span>Settings</span></button>
</cyp-menu>
</footer>
</cyp-app>
<script type="module" src="js/app.js"></script>
</body>
</html>

View file

@ -1,33 +1,18 @@
import * as nav from "./nav.js";
import * as mpd from "./lib/mpd.js";
import * as player from "./player.js";
import * as html from "./lib/html.js";
import * as range from "./lib/range.js";
import "./lib/range.js";
import "./menu.js";
import "./player.js";
import "./queue.js";
import * as mpd from "./lib/mpd.js";
import * as mpdMock from "./lib/mpd-mock.js";
import * as html from "./lib/html.js";
import * as queue from "./queue.js";
import * as library from "./library.js";
import * as fs from "./fs.js";
import * as playlists from "./playlists.js";
import * as yt from "./yt.js";
import * as settings from "./settings.js";
const components = { queue, library, fs, playlists, yt, settings };
export function activate(what) {
location.hash = what;
for (let id in components) {
let node = document.querySelector(`#${id}`);
if (what == id) {
node.style.display = "";
components[id].activate();
} else {
node.style.display = "none";
}
}
nav.active(what);
}
function initIcons() {
Array.from(document.querySelectorAll("[data-icon]")).forEach(node => {
let icon = html.icon(node.dataset.icon);
@ -35,39 +20,49 @@ function initIcons() {
});
}
function fromHash() {
let hash = location.hash.substring(1);
activate(hash || "queue");
}
function onHashChange(e) {
fromHash();
}
async function init() {
initIcons();
async function mpdExecutor(resolve, reject) {
try {
await mpd.init();
resolve(mpd);
} catch (e) {
resolve(mpdMock);
console.error(e);
reject(e);
}
nav.init(document.querySelector("nav"));
for (let id in components) {
let node = document.querySelector(`#${id}`);
components[id].init(node);
}
player.init(document.querySelector("#player"));
window.addEventListener("hashchange", onHashChange);
fromHash();
}
init();
class App extends HTMLElement {
get mpd() { return mpd; }
constructor() {
super();
initIcons();
this._mpd = new Promise(mpdExecutor);
this._load();
}
get mpd() { return this._mpd; }
async _load() {
const promises = ["cyp-player"].map(name => customElements.whenDefined(name));
await Promise.all(promises);
const onHashChange = () => {
const hash = location.hash.substring(1);
this._activate(hash || "queue");
}
window.addEventListener("hashchange", onHashChange);
onHashChange();
}
_activate(what) {
location.hash = what;
this.setAttribute("component", what);
const component = this.querySelector(`cyp-${what}`);
// component.activate();
}
}
customElements.define("cyp-app", App);

View file

@ -1,3 +1,32 @@
const APP = "cyp-app";
export default class Component extends HTMLElement {
get _app() { return this.closest("cyp-app"); }
constructor() {
super();
this._app.then(app => {
let mo = new MutationObserver(mrs => {
mrs.forEach(mr => this._onAppAttributeChange(mr));
});
mo.observe(app, {attributes:true});
});
}
_onAppAttributeChange(mr) {
if (mr.attributeName != "component") { return; }
const component = mr.target.getAttribute(mr.attributeName);
const isThis = (this.nodeName.toLowerCase() == `cyp-${component}`);
this._onComponentChange(component, isThis);
}
get _app() {
return customElements.whenDefined(APP)
.then(() => this.closest(APP));
}
get _mpd() {
return this._app.then(app => app.mpd);
}
_onComponentChange(component) {}
}

View file

@ -1 +0,0 @@
../../../node_modules/custom-range/range.js

170
app/js/lib/range.js Normal file
View file

@ -0,0 +1,170 @@
class Range extends HTMLElement {
static get observedAttributes() { return ["min", "max", "value", "step", "disabled"]; }
constructor() {
super();
this._dom = {};
this.addEventListener("mousedown", this);
this.addEventListener("keydown", this);
}
get _valueAsNumber() {
let raw = (this.hasAttribute("value") ? Number(this.getAttribute("value")) : 50);
return this._constrain(raw);
}
get _minAsNumber() {
return (this.hasAttribute("min") ? Number(this.getAttribute("min")) : 0);
}
get _maxAsNumber() {
return (this.hasAttribute("max") ? Number(this.getAttribute("max")) : 100);
}
get _stepAsNumber() {
return (this.hasAttribute("step") ? Number(this.getAttribute("step")) : 1);
}
get value() { return String(this._valueAsNumber); }
get valueAsNumber() { return this._valueAsNumber; }
get min() { return this.hasAttribute("min") ? this.getAttribute("min") : ""; }
get max() { return this.hasAttribute("max") ? this.getAttribute("max") : ""; }
get step() { return this.hasAttribute("step") ? this.getAttribute("step") : ""; }
get disabled() { return this.hasAttribute("disabled"); }
set _valueAsNumber(value) { this.value = String(value); }
set min(min) { this.setAttribute("min", min); }
set max(max) { this.setAttribute("max", max); }
set value(value) { this.setAttribute("value", value); }
set step(step) { this.setAttribute("step", step); }
set disabled(disabled) {
disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled");
}
connectedCallback() {
if (this.firstChild) { return; }
this.innerHTML = `
<span class="-track"></span>
<span class="-elapsed"></span>
<span class="-remaining"></span>
<div class="-inner">
<button class="-thumb"></button>
</div>
`;
Array.from(this.querySelectorAll("[class^='-']")).forEach(node => {
let name = node.className.substring(1);
this._dom[name] = node;
});
this._update();
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "min":
case "max":
case "value":
case "step":
this._update();
break;
}
}
handleEvent(e) {
switch (e.type) {
case "mousedown":
if (this.disabled) { return; }
document.addEventListener("mousemove", this);
document.addEventListener("mouseup", this);
this._setToMouse(e);
break;
case "mousemove":
this._setToMouse(e);
break;
case "mouseup":
document.removeEventListener("mousemove", this);
document.removeEventListener("mouseup", this);
this.dispatchEvent(new CustomEvent("change"));
break;
case "keydown":
if (this.disabled) { return; }
this._handleKey(e.code);
this.dispatchEvent(new CustomEvent("input"));
this.dispatchEvent(new CustomEvent("change"));
break;
}
}
_handleKey(code) {
let min = this._minAsNumber;
let max = this._maxAsNumber;
let range = max - min;
let step = this._stepAsNumber;
switch (code) {
case "ArrowLeft":
case "ArrowDown":
this._valueAsNumber = this._constrain(this._valueAsNumber - step);
break;
case "ArrowRight":
case "ArrowUp":
this._valueAsNumber = this._constrain(this._valueAsNumber + step);
break;
case "Home": this._valueAsNumber = this._constrain(min); break;
case "End": this._valueAsNumber = this._constrain(max); break;
case "PageUp": this._valueAsNumber = this._constrain(this._valueAsNumber + range/10); break;
case "PageDown": this._valueAsNumber = this._constrain(this._valueAsNumber - range/10); break;
}
}
_constrain(value) {
const min = this._minAsNumber;
const max = this._maxAsNumber;
const step = this._stepAsNumber;
value = Math.max(value, min);
value = Math.min(value, max);
value -= min;
value = Math.round(value / step) * step;
value += min;
if (value > max) { value -= step; }
return value;
}
_update() {
let min = this._minAsNumber;
let max = this._maxAsNumber;
let frac = (this._valueAsNumber-min) / (max-min);
this._dom.thumb.style.left = `${frac * 100}%`;
this._dom.remaining.style.left = `${frac * 100}%`;
this._dom.elapsed.style.width = `${frac * 100}%`;
}
_setToMouse(e) {
let rect = this._dom.inner.getBoundingClientRect();
let x = e.clientX;
x = Math.max(x, rect.left);
x = Math.min(x, rect.right);
let min = this._minAsNumber;
let max = this._maxAsNumber;
let frac = (x-rect.left) / (rect.right-rect.left);
let value = this._constrain(min + frac * (max-min));
if (value == this._valueAsNumber) { return; }
this._valueAsNumber = value;
this.dispatchEvent(new CustomEvent("input"));
}
}
customElements.define('x-range', Range);

31
app/js/menu.js Normal file
View file

@ -0,0 +1,31 @@
import Component from "./component.js";
class Menu extends Component {
constructor() {
super();
this._tabs = Array.from(this.querySelectorAll("[data-for]"));
this._tabs.forEach(tab => {
tab.addEventListener("click", _ => this._activate(tab.dataset.for));
});
}
async _listen() {
const app = await this._app;
let mo = new MutationObserver(_ => this._sync())
mo.observe(app, {attributes:true});
}
async _activate(component) {
const app = await this._app;
app.setAttribute("component", component);
}
_onComponentChange(component) {
this._tabs.forEach(tab => {
tab.classList.toggle("active", tab.dataset.for == component);
});
}
}
customElements.define("cyp-menu", Menu);

View file

@ -1,16 +0,0 @@
import * as app from "./app.js";
let tabs = [];
export function init(node) {
tabs = Array.from(node.querySelectorAll("[data-for]"));
tabs.forEach(tab => {
tab.addEventListener("click", e => app.activate(tab.dataset.for));
});
}
export function active(id) {
tabs.forEach(tab => {
tab.classList.toggle("active", tab.dataset.for == id);
});
}

View file

@ -1,140 +1,146 @@
//import * as mpd from "./lib/mpd.js";
import * as mpd from "./lib/mpd-mock.js";
import * as art from "./lib/art.js";
import * as html from "./lib/html.js";
import * as format from "./lib/format.js";
import * as pubsub from "./lib/pubsub.js";
import Component from "./component.js";
const DELAY = 1000;
const DOM = {};
let current = {};
let node;
let idleTimeout = null;
let toggledVolume = 0;
function sync(data) {
if ("volume" in data) {
data["volume"] = Number(data["volume"]);
DOM.mute.disabled = false;
DOM.volume.disabled = false;
DOM.volume.value = data["volume"];
if (data["volume"] == 0 && current["volume"] > 0) { // muted
toggledVolume = current["volume"];
html.clear(DOM.mute);
DOM.mute.appendChild(html.icon("volume-off"));
}
if (data["volume"] > 0 && current["volume"] == 0) { // restored
toggledVolume = 0;
html.clear(DOM.mute);
DOM.mute.appendChild(html.icon("volume-high"));
}
} else {
DOM.mute.disabled = true;
DOM.volume.disabled = true;
DOM.volume.value = 50;
}
// 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"]) { // playing at all?
let duration = Number(data["duration"]);
DOM.duration.textContent = format.time(duration);
DOM.progress.max = duration;
DOM.progress.disabled = false;
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
} else {
DOM.title.textContent = "";
DOM.subtitle.textContent = "";
DOM.progress.value = 0;
DOM.progress.disabled = true;
}
pubsub.publish("song-change", null, data);
}
let artistNew = data["AlbumArtist"] || data["Artist"];
let artistOld = current["AlbumArtist"] || current["Artist"];
if (artistNew != artistOld || data["Album"] != current["Album"]) { // changed album (art)
html.clear(DOM.art);
art.get(artistNew, data["Album"], data["file"]).then(src => {
if (src) {
html.node("img", {src}, "", DOM.art);
} else {
html.icon("music", DOM.art);
}
});
}
let flags = [];
if (data["random"] == "1") { flags.push("random"); }
if (data["repeat"] == "1") { flags.push("repeat"); }
node.dataset.flags = flags.join(" ");
node.dataset.state = data["state"];
current = data;
}
function idle() {
idleTimeout = setTimeout(update, DELAY);
}
function clearIdle() {
idleTimeout && clearTimeout(idleTimeout);
idleTimeout = null;
}
async function command(cmd) {
clearIdle();
let data = await mpd.commandAndStatus(cmd);
sync(data);
idle();
}
export async function update() {
clearIdle();
let data = await mpd.status();
sync(data);
idle();
}
export function init(n) {
node = n;
let all = node.querySelectorAll("[class]");
Array.from(all).forEach(node => DOM[node.className] = node);
DOM.progress = DOM.timeline.querySelector("x-range");
DOM.volume = DOM.volume.querySelector("x-range");
DOM.play.addEventListener("click", e => command("play"));
DOM.pause.addEventListener("click", e => command("pause 1"));
DOM.prev.addEventListener("click", e => command("previous"));
DOM.next.addEventListener("click", e => command("next"));
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.volume.addEventListener("input", e => command(`setvol ${e.target.valueAsNumber}`));
DOM.progress.addEventListener("input", e => command(`seekcur ${e.target.valueAsNumber}`));
DOM.mute.addEventListener("click", e => command(`setvol ${toggledVolume}`));
update();
}
class Player extends Component {
constructor() {
super();
this._current = {};
this._toggledVolume = 0;
this._idleTimeout = null;
this._dom = this._initDOM();
this._update();
}
_initDOM() {
const DOM = {};
const all = this.querySelectorAll("[class]");
Array.from(all).forEach(node => DOM[node.className] = node);
DOM.progress = DOM.timeline.querySelector("x-range");
DOM.volume = DOM.volume.querySelector("x-range");
DOM.play.addEventListener("click", _ => this._command("play"));
DOM.pause.addEventListener("click", _ => this._command("pause 1"));
DOM.prev.addEventListener("click", _ => this._command("previous"));
DOM.next.addEventListener("click", _ => this._command("next"));
DOM.random.addEventListener("click", _ => this._command(`random ${this._current["random"] == "1" ? "0" : "1"}`));
DOM.repeat.addEventListener("click", _ => this._command(`repeat ${this._current["repeat"] == "1" ? "0" : "1"}`));
DOM.volume.addEventListener("input", e => this._command(`setvol ${e.target.valueAsNumber}`));
DOM.progress.addEventListener("input", e => this._command(`seekcur ${e.target.valueAsNumber}`));
DOM.mute.addEventListener("click", _ => this._command(`setvol ${this._toggledVolume}`));
return DOM;
}
async _command(cmd) {
const mpd = await this._mpd;
this._clearIdle();
const data = await mpd.commandAndStatus(cmd);
this._sync(data);
this._idle();
}
_idle() {
this._idleTimeout = setTimeout(() => this._update(), DELAY);
}
_clearIdle() {
this._idleTimeout && clearTimeout(this._idleTimeout);
this._idleTimeout = null;
}
async _update() {
const mpd = await this._mpd;
this._clearIdle();
const data = await mpd.status();
this._sync(data);
this._idle();
}
_sync(data) {
const DOM = this._dom;
if ("volume" in data) {
data["volume"] = Number(data["volume"]);
DOM.mute.disabled = false;
DOM.volume.disabled = false;
DOM.volume.value = data["volume"];
if (data["volume"] == 0 && this._current["volume"] > 0) { // muted
this._toggledVolume = this._current["volume"];
html.clear(DOM.mute);
DOM.mute.appendChild(html.icon("volume-off"));
}
if (data["volume"] > 0 && this._current["volume"] == 0) { // restored
this._toggledVolume = 0;
html.clear(DOM.mute);
DOM.mute.appendChild(html.icon("volume-high"));
}
} else {
DOM.mute.disabled = true;
DOM.volume.disabled = true;
DOM.volume.value = 50;
}
// changed time
let elapsed = Number(data["elapsed"] || 0);
DOM.progress.value = elapsed;
DOM.elapsed.textContent = format.time(elapsed);
if (data["file"] != this._current["file"]) { // changed song
if (data["file"]) { // playing at all?
let duration = Number(data["duration"]);
DOM.duration.textContent = format.time(duration);
DOM.progress.max = duration;
DOM.progress.disabled = false;
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
} else {
DOM.title.textContent = "";
DOM.subtitle.textContent = "";
DOM.progress.value = 0;
DOM.progress.disabled = true;
}
this._dispatchSongChange(data);
}
let artistNew = data["AlbumArtist"] || data["Artist"];
let artistOld = this._current["AlbumArtist"] || this._current["Artist"];
if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art)
html.clear(DOM.art);
art.get(artistNew, data["Album"], data["file"]).then(src => {
if (src) {
html.node("img", {src}, "", DOM.art);
} else {
html.icon("music", DOM.art);
}
});
}
let flags = [];
if (data["random"] == "1") { flags.push("random"); }
if (data["repeat"] == "1") { flags.push("repeat"); }
this.dataset.flags = flags.join(" ");
this.dataset.state = data["state"];
this._current = data;
}
async _dispatchSongChange(detail) {
const app = await this._app;
const e = new CustomEvent("song-change", {detail});
app.dispatchEvent(e);
}
}
customElements.define("cyp-player", Player);
customElements.define("cyp-player", Player);

View file

@ -1,61 +1,76 @@
// import * as mpd from "./lib/mpd.js";
import * as mpd from "./lib/mpd-mock.js";
import * as html from "./lib/html.js";
import * as pubsub from "./lib/pubsub.js";
import * as ui from "./lib/ui.js";
let node;
let currentId;
import Component from "./component.js";
function updateCurrent() {
let all = Array.from(node.querySelectorAll("[data-song-id]"));
all.forEach(node => {
node.classList.toggle("current", node.dataset.songId == currentId);
});
class Queue extends Component {
constructor() {
super();
this._currentId = null;
this.querySelector(".clear").addEventListener("click", async _ => {
const mpd = await this._mpd;
await mpd.command("clear");
this._sync();
});
this.querySelector(".save").addEventListener("click", async _ => {
let name = prompt("Save current queue as a playlist?", "name");
if (name === null) { return; }
const mpd = await this._mpd;
mpd.command(`save "${mpd.escape(name)}"`);
});
this._app.then(app => {
app.addEventListener("song-change", this);
app.addEventListener("queue-change", this);
})
this._sync();
}
handleEvent(e) {
switch (e.type) {
case "song-change":
this._currentId = e.detail["Id"];
this._updateCurrent();
break;
case "queue-change":
this._sync();
break;
}
}
_onComponentChange(c, isThis) {
this.hidden = !isThis;
isThis && this._sync();
}
async _sync() {
const mpd = await this._mpd;
let songs = await mpd.listQueue();
this._buildSongs(songs);
// FIXME pubsub?
document.querySelector("#queue-length").textContent = `(${songs.length})`;
}
_updateCurrent() {
let all = Array.from(this.querySelectorAll("[data-song-id]"));
all.forEach(node => {
node.classList.toggle("current", node.dataset.songId == this._currentId);
});
}
_buildSongs(songs) {
let ul = this.querySelector("ul");
html.clear(ul);
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
this._updateCurrent();
}
}
function buildSongs(songs) {
let ul = node.querySelector("ul");
html.clear(ul);
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
updateCurrent();
}
function onSongChange(message, publisher, data) {
currentId = data["Id"];
updateCurrent();
}
function onQueueChange(message, publisher, data) {
syncQueue();
}
async function syncQueue() {
let songs = await mpd.listQueue();
buildSongs(songs);
document.querySelector("#queue-length").textContent = `(${songs.length})`;
}
export async function activate() {
syncQueue();
}
export function init(n) {
node = n;
syncQueue();
pubsub.subscribe("song-change", onSongChange);
pubsub.subscribe("queue-change", onQueueChange);
node.querySelector(".clear").addEventListener("click", async e => {
await mpd.command("clear");
syncQueue();
});
node.querySelector(".save").addEventListener("click", e => {
let name = prompt("Save current queue as a playlist?", "name");
if (name === null) { return; }
mpd.command(`save "${mpd.escape(name)}"`);
});
}
customElements.define("cyp-queue", Queue);

View file

@ -1,52 +1,70 @@
import * as mpd from "./lib/mpd.js";
import Component from "./component.js";
let node;
let inputs = {};
const prefix = "cyp";
function loadFromStorage(key, def) {
return localStorage.getItem(`${prefix}-${key}`) || def;
function loadFromStorage(key) {
return localStorage.getItem(`${prefix}-${key}`);
}
function saveToStorage(key, value) {
return localStorage.setItem(`${prefix}-${key}`, value);
}
function load() {
let theme = loadFromStorage("theme", "dark");
inputs.theme.value = theme;
setTheme(theme);
class Settings extends Component {
constructor() {
super();
this._inputs = {
theme: this.querySelector("[name=theme]"),
color: Array.from(this.querySelectorAll("[name=color]"))
};
let color = loadFromStorage("color", "dodgerblue");
inputs.color.forEach(input => {
input.checked = (input.value == color);
input.parentNode.style.color = input.value;
});
setColor(color);
this._load();
this._inputs.theme.addEventListener("change", e => this._setTheme(e.target.value));
this._inputs.color.forEach(input => {
input.addEventListener("click", e => this._setColor(e.target.value));
});
}
_onAppAttributeChange(mr) {
if (mr.attributeName == "theme") { this._syncTheme(); }
if (mr.attributeName == "color") { this._syncColor(); }
}
async _syncTheme() {
const app = await this._app;
this._inputs.theme.value = app.getAttribute("theme");
}
async _syncColor() {
const app = await this._app;
this._inputs.color.forEach(input => {
input.checked = (input.value == app.getAttribute("color"));
input.parentNode.style.color = input.value;
});
}
async _load() {
const app = await this._app;
const theme = loadFromStorage("theme");
(theme ? app.setAttribute("theme", theme) : this._syncTheme());
const color = loadFromStorage("color");
(color ? app.setAttribute("color", color) : this._syncColor());
}
async _setTheme(theme) {
const app = await this._app;
saveToStorage("theme", theme);
app.setAttribute("theme", theme);
}
async _setColor(color) {
const app = await this._app;
saveToStorage("color", color);
app.setAttribute("color", color);
}
}
function setTheme(theme) {
saveToStorage("theme", theme);
document.documentElement.dataset.theme = theme;
}
function setColor(color) {
saveToStorage("color", color);
document.documentElement.dataset.color = color;
}
export async function activate() {}
export function init(n) {
node = n;
inputs.theme = n.querySelector("[name=theme]");
inputs.color = Array.from(n.querySelectorAll("[name=color]"));
load();
inputs.theme.addEventListener("change", e => setTheme(e.target.value));
inputs.color.forEach(input => {
input.addEventListener("click", e => setColor(e.target.value));
});
}
customElements.define("cyp-settings", Settings);

View file

@ -9,7 +9,6 @@ const cmd = "youtube-dl";
function downloadYoutube(q, response) {
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
// response.setHeader("Content-Type", "text/plain; charset=utf-8");
console.log("YouTube downloading", q);
let args = [