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

View file

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

View file

@ -1,7 +1,4 @@
.component { .component {
height: 100%;
.flex-column;
header { header {
.flex-row; .flex-row;
padding: var(--spacing); padding: var(--spacing);

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
#player { cyp-player {
@art-size: 96px; @art-size: 96px;
.flex-row; .flex-row;
align-items: stretch; align-items: stretch;

View file

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

View file

@ -1,4 +1,4 @@
#settings { cyp-settings {
font-size: var(--font-size-large); font-size: var(--font-size-large);
dl { dl {

View file

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

View file

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

View file

@ -1,33 +1,18 @@
import * as nav from "./nav.js"; import "./lib/range.js";
import * as mpd from "./lib/mpd.js"; import "./menu.js";
import * as player from "./player.js"; import "./player.js";
import * as html from "./lib/html.js"; import "./queue.js";
import * as range from "./lib/range.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 library from "./library.js";
import * as fs from "./fs.js"; import * as fs from "./fs.js";
import * as playlists from "./playlists.js"; import * as playlists from "./playlists.js";
import * as yt from "./yt.js"; import * as yt from "./yt.js";
import * as settings from "./settings.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() { function initIcons() {
Array.from(document.querySelectorAll("[data-icon]")).forEach(node => { Array.from(document.querySelectorAll("[data-icon]")).forEach(node => {
let icon = html.icon(node.dataset.icon); let icon = html.icon(node.dataset.icon);
@ -35,39 +20,49 @@ function initIcons() {
}); });
} }
function fromHash() { async function mpdExecutor(resolve, reject) {
let hash = location.hash.substring(1);
activate(hash || "queue");
}
function onHashChange(e) {
fromHash();
}
async function init() {
initIcons();
try { try {
await mpd.init(); await mpd.init();
resolve(mpd);
} catch (e) { } catch (e) {
resolve(mpdMock);
console.error(e); 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 { 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); customElements.define("cyp-app", App);

View file

@ -1,3 +1,32 @@
const APP = "cyp-app";
export default class Component extends HTMLElement { 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,20 +1,71 @@
//import * as mpd from "./lib/mpd.js";
import * as mpd from "./lib/mpd-mock.js";
import * as art from "./lib/art.js"; 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 Component from "./component.js"; import Component from "./component.js";
const DELAY = 1000; const DELAY = 1000;
const DOM = {};
let current = {}; class Player extends Component {
let node; constructor() {
let idleTimeout = null; super();
let toggledVolume = 0; this._current = {};
this._toggledVolume = 0;
this._idleTimeout = null;
this._dom = this._initDOM();
this._update();
}
function sync(data) { _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) { if ("volume" in data) {
data["volume"] = Number(data["volume"]); data["volume"] = Number(data["volume"]);
@ -22,14 +73,14 @@ function sync(data) {
DOM.volume.disabled = false; DOM.volume.disabled = false;
DOM.volume.value = data["volume"]; DOM.volume.value = data["volume"];
if (data["volume"] == 0 && current["volume"] > 0) { // muted if (data["volume"] == 0 && this._current["volume"] > 0) { // muted
toggledVolume = current["volume"]; this._toggledVolume = this._current["volume"];
html.clear(DOM.mute); html.clear(DOM.mute);
DOM.mute.appendChild(html.icon("volume-off")); DOM.mute.appendChild(html.icon("volume-off"));
} }
if (data["volume"] > 0 && current["volume"] == 0) { // restored if (data["volume"] > 0 && this._current["volume"] == 0) { // restored
toggledVolume = 0; this._toggledVolume = 0;
html.clear(DOM.mute); html.clear(DOM.mute);
DOM.mute.appendChild(html.icon("volume-high")); DOM.mute.appendChild(html.icon("volume-high"));
} }
@ -45,7 +96,7 @@ function sync(data) {
DOM.progress.value = elapsed; DOM.progress.value = elapsed;
DOM.elapsed.textContent = format.time(elapsed); DOM.elapsed.textContent = format.time(elapsed);
if (data["file"] != current["file"]) { // changed song if (data["file"] != this._current["file"]) { // changed song
if (data["file"]) { // playing at all? if (data["file"]) { // playing at all?
let duration = Number(data["duration"]); let duration = Number(data["duration"]);
DOM.duration.textContent = format.time(duration); DOM.duration.textContent = format.time(duration);
@ -60,12 +111,12 @@ function sync(data) {
DOM.progress.disabled = true; DOM.progress.disabled = true;
} }
pubsub.publish("song-change", null, data); this._dispatchSongChange(data);
} }
let artistNew = data["AlbumArtist"] || data["Artist"]; let artistNew = data["AlbumArtist"] || data["Artist"];
let artistOld = current["AlbumArtist"] || current["Artist"]; let artistOld = this._current["AlbumArtist"] || this._current["Artist"];
if (artistNew != artistOld || data["Album"] != current["Album"]) { // changed album (art) if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art)
html.clear(DOM.art); html.clear(DOM.art);
art.get(artistNew, data["Album"], data["file"]).then(src => { art.get(artistNew, data["Album"], data["file"]).then(src => {
if (src) { if (src) {
@ -79,62 +130,17 @@ function sync(data) {
let flags = []; let flags = [];
if (data["random"] == "1") { flags.push("random"); } if (data["random"] == "1") { flags.push("random"); }
if (data["repeat"] == "1") { flags.push("repeat"); } if (data["repeat"] == "1") { flags.push("repeat"); }
node.dataset.flags = flags.join(" "); this.dataset.flags = flags.join(" ");
this.dataset.state = data["state"];
node.dataset.state = data["state"]; this._current = data;
}
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 {
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 html from "./lib/html.js";
import * as pubsub from "./lib/pubsub.js";
import * as ui from "./lib/ui.js"; import * as ui from "./lib/ui.js";
let node; import Component from "./component.js";
let currentId;
function updateCurrent() { class Queue extends Component {
let all = Array.from(node.querySelectorAll("[data-song-id]")); constructor() {
all.forEach(node => { super();
node.classList.toggle("current", node.dataset.songId == currentId); this._currentId = null;
this.querySelector(".clear").addEventListener("click", async _ => {
const mpd = await this._mpd;
await mpd.command("clear");
this._sync();
}); });
}
function buildSongs(songs) { this.querySelector(".save").addEventListener("click", async _ => {
let ul = node.querySelector("ul"); 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); html.clear(ul);
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul)); songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
updateCurrent(); this._updateCurrent();
}
} }
function onSongChange(message, publisher, data) { customElements.define("cyp-queue", Queue);
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)}"`);
});
}

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"; const prefix = "cyp";
function loadFromStorage(key, def) { function loadFromStorage(key) {
return localStorage.getItem(`${prefix}-${key}`) || def; return localStorage.getItem(`${prefix}-${key}`);
} }
function saveToStorage(key, value) { function saveToStorage(key, value) {
return localStorage.setItem(`${prefix}-${key}`, value); return localStorage.setItem(`${prefix}-${key}`, value);
} }
function load() { class Settings extends Component {
let theme = loadFromStorage("theme", "dark"); constructor() {
inputs.theme.value = theme; super();
setTheme(theme); this._inputs = {
theme: this.querySelector("[name=theme]"),
color: Array.from(this.querySelectorAll("[name=color]"))
};
let color = loadFromStorage("color", "dodgerblue"); this._load();
inputs.color.forEach(input => {
input.checked = (input.value == color); 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; input.parentNode.style.color = input.value;
}); });
setColor(color); }
}
function setTheme(theme) { 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); saveToStorage("theme", theme);
document.documentElement.dataset.theme = theme; app.setAttribute("theme", theme);
} }
function setColor(color) { async _setColor(color) {
const app = await this._app;
saveToStorage("color", color); saveToStorage("color", color);
document.documentElement.dataset.color = color; app.setAttribute("color", color);
}
} }
export async function activate() {} customElements.define("cyp-settings", Settings);
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));
});
}

View file

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