refactor wip
This commit is contained in:
parent
7a418c4e8a
commit
14569a9415
20 changed files with 768 additions and 465 deletions
297
app/app.css
297
app/app.css
|
@ -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) {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
.component {
|
.component {
|
||||||
height: 100%;
|
|
||||||
.flex-column;
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
.flex-row;
|
.flex-row;
|
||||||
padding: var(--spacing);
|
padding: var(--spacing);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
main {
|
main {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#player {
|
cyp-player {
|
||||||
@art-size: 96px;
|
@art-size: 96px;
|
||||||
.flex-row;
|
.flex-row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#queue {
|
cyp-queue {
|
||||||
.component;
|
.component;
|
||||||
|
|
||||||
.current { color: var(--primary); }
|
.current { color: var(--primary); }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#settings {
|
cyp-settings {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../../../node_modules/custom-range/range.js
|
|
170
app/js/lib/range.js
Normal file
170
app/js/lib/range.js
Normal 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
31
app/js/menu.js
Normal 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);
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
148
app/js/player.js
148
app/js/player.js
|
@ -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);
|
113
app/js/queue.js
113
app/js/queue.js
|
@ -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)}"`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
1
index.js
1
index.js
|
@ -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 = [
|
||||||
|
|
Loading…
Reference in a new issue