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);
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
}
|
||||
cyp-app {
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
font-family: lato, sans-serif;
|
||||
|
@ -20,6 +22,9 @@ body {
|
|||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
cyp-app:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
header,
|
||||
footer {
|
||||
z-index: 1;
|
||||
|
@ -35,7 +40,6 @@ button {
|
|||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
|
@ -46,6 +50,9 @@ button {
|
|||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
select {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--fg);
|
||||
|
@ -74,24 +81,30 @@ select {
|
|||
fill: currentColor;
|
||||
}
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-column {
|
||||
.flex-row:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-column:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
.long-line {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.multiline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.multiline:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
.multiline h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
@ -159,162 +172,173 @@ x-range:not([disabled]) .-thumb:hover {
|
|||
}
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
cyp-menu {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
}
|
||||
nav ul li {
|
||||
flex: 1 0 0;
|
||||
cyp-menu:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-menu button {
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 0 8px 0;
|
||||
padding-top: 4px;
|
||||
border-top: 4px solid transparent;
|
||||
}
|
||||
nav ul li .icon {
|
||||
cyp-menu button:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-menu button .icon {
|
||||
margin-right: var(--icon-spacing);
|
||||
}
|
||||
nav ul li.active {
|
||||
cyp-menu button.active {
|
||||
border-top-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background-color: rgb(var(--primary-raw), 0.1);
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
nav ul li {
|
||||
cyp-menu button {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
nav ul li:not([data-for=queue]) .icon {
|
||||
cyp-menu button:not([data-for=queue]) .icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
nav ul li span:not([id]) {
|
||||
cyp-menu button span:not([id]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#player {
|
||||
display: flex;
|
||||
cyp-player {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
#player:not([data-state=play]) .pause {
|
||||
cyp-player:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-player:not([data-state=play]) .pause {
|
||||
display: none;
|
||||
}
|
||||
#player[data-state=play] .play {
|
||||
cyp-player[data-state=play] .play {
|
||||
display: none;
|
||||
}
|
||||
#player:not([data-flags~=random]) .random,
|
||||
#player:not([data-flags~=repeat]) .repeat {
|
||||
cyp-player:not([data-flags~=random]) .random,
|
||||
cyp-player:not([data-flags~=repeat]) .repeat {
|
||||
opacity: 0.5;
|
||||
}
|
||||
#player x-range {
|
||||
cyp-player x-range {
|
||||
flex-grow: 1;
|
||||
--elapsed-color: var(--primary);
|
||||
}
|
||||
#player .art {
|
||||
cyp-player .art {
|
||||
margin-right: 0;
|
||||
height: 96px;
|
||||
}
|
||||
#player .art img,
|
||||
#player .art .icon {
|
||||
cyp-player .art img,
|
||||
cyp-player .art .icon {
|
||||
width: 96px;
|
||||
}
|
||||
#player .info {
|
||||
cyp-player .info {
|
||||
flex-grow: 2;
|
||||
flex-basis: 0;
|
||||
padding: 0 var(--icon-spacing);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
#player .info h2 {
|
||||
cyp-player .info:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-player .info h2 {
|
||||
font-size: 125%;
|
||||
margin: 0;
|
||||
}
|
||||
#player .info .title,
|
||||
#player .info .subtitle {
|
||||
cyp-player .info .title,
|
||||
cyp-player .info .subtitle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#player .timeline {
|
||||
display: flex;
|
||||
cyp-player .timeline {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#player .timeline .duration,
|
||||
#player .timeline .elapsed {
|
||||
cyp-player .timeline:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-player .timeline .duration,
|
||||
cyp-player .timeline .elapsed {
|
||||
flex-basis: 5ch;
|
||||
text-align: center;
|
||||
}
|
||||
#player .controls {
|
||||
cyp-player .controls {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
max-width: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
#player .controls .playback {
|
||||
cyp-player .controls:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-player .controls .playback {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
#player .controls .playback .icon {
|
||||
cyp-player .controls .playback:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-player .controls .playback .icon {
|
||||
width: 40px;
|
||||
}
|
||||
#player .controls .playback .icon-play,
|
||||
#player .controls .playback .icon-pause {
|
||||
cyp-player .controls .playback .icon-play,
|
||||
cyp-player .controls .playback .icon-pause {
|
||||
width: 64px;
|
||||
}
|
||||
#player .controls .volume {
|
||||
display: flex;
|
||||
cyp-player .controls .volume {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#player .controls .volume .mute {
|
||||
cyp-player .controls .volume:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-player .controls .volume .mute {
|
||||
margin-right: 4px;
|
||||
}
|
||||
#player .misc {
|
||||
cyp-player .misc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: space-around;
|
||||
}
|
||||
#player .misc .icon {
|
||||
cyp-player .misc .icon {
|
||||
width: 32px;
|
||||
}
|
||||
@media (max-width: 519px) {
|
||||
#player {
|
||||
cyp-player {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#player .info {
|
||||
cyp-player .info {
|
||||
order: 1;
|
||||
flex-basis: 100%;
|
||||
height: 96px;
|
||||
}
|
||||
}
|
||||
.component {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.component header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
.component header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
.component header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
|
@ -331,10 +355,12 @@ nav ul li.active {
|
|||
padding: 0;
|
||||
}
|
||||
.component li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.component li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
.component li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
@ -363,79 +389,75 @@ nav ul li.active {
|
|||
.component li:nth-child(odd) {
|
||||
background-color: var(--bg-alt);
|
||||
}
|
||||
#queue {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#queue header {
|
||||
display: flex;
|
||||
cyp-queue header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
#queue header button {
|
||||
cyp-queue header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-queue header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
}
|
||||
#queue header button .icon {
|
||||
cyp-queue header button .icon {
|
||||
margin-right: var(--icon-spacing);
|
||||
}
|
||||
#queue ul {
|
||||
cyp-queue ul {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#queue li {
|
||||
display: flex;
|
||||
cyp-queue li {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#queue li .info {
|
||||
cyp-queue li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-queue li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
#queue li .info .icon {
|
||||
cyp-queue li .info .icon {
|
||||
color: var(--primary);
|
||||
margin-right: var(--icon-spacing);
|
||||
filter: drop-shadow(var(--text-shadow));
|
||||
}
|
||||
#queue li .info h2 {
|
||||
cyp-queue li .info h2 {
|
||||
font-size: var(--font-size-large);
|
||||
margin: 0;
|
||||
}
|
||||
#queue li .info h2,
|
||||
#queue li .info div {
|
||||
cyp-queue li .info h2,
|
||||
cyp-queue li .info div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#queue li:not(.has-art) {
|
||||
cyp-queue li:not(.has-art) {
|
||||
padding: 8px;
|
||||
}
|
||||
#queue li button .icon {
|
||||
cyp-queue li button .icon {
|
||||
width: 32px;
|
||||
}
|
||||
#queue li:nth-child(odd) {
|
||||
cyp-queue li:nth-child(odd) {
|
||||
background-color: var(--bg-alt);
|
||||
}
|
||||
#queue .current {
|
||||
cyp-queue .current {
|
||||
color: var(--primary);
|
||||
}
|
||||
#library {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#library header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
#library header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#library header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
|
@ -452,10 +474,12 @@ nav ul li.active {
|
|||
padding: 0;
|
||||
}
|
||||
#library li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#library li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#library li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
@ -521,17 +545,14 @@ nav ul li.active {
|
|||
font-size: 150%;
|
||||
margin: 4px 0;
|
||||
}
|
||||
#fs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#fs header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
#fs header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#fs header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
|
@ -548,10 +569,12 @@ nav ul li.active {
|
|||
padding: 0;
|
||||
}
|
||||
#fs li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#fs li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#fs li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
@ -593,24 +616,23 @@ nav ul li.active {
|
|||
cursor: pointer;
|
||||
}
|
||||
#fs .info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#fs .info:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#fs .info h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
#playlists {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#playlists header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
#playlists header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#playlists header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
|
@ -627,10 +649,12 @@ nav ul li.active {
|
|||
padding: 0;
|
||||
}
|
||||
#playlists li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#playlists li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#playlists li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
@ -660,24 +684,23 @@ nav ul li.active {
|
|||
background-color: var(--bg-alt);
|
||||
}
|
||||
#playlists .info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#playlists .info:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#playlists .info h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
#yt {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#yt header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: var(--spacing);
|
||||
}
|
||||
#yt header:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#yt header button {
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: bold;
|
||||
|
@ -694,10 +717,12 @@ nav ul li.active {
|
|||
padding: 0;
|
||||
}
|
||||
#yt li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#yt li:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
#yt li .info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
@ -755,36 +780,39 @@ nav ul li.active {
|
|||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
#settings {
|
||||
cyp-settings {
|
||||
font-size: var(--font-size-large);
|
||||
}
|
||||
#settings dl {
|
||||
cyp-settings dl {
|
||||
margin: var(--spacing);
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
align-items: center;
|
||||
grid-gap: var(--spacing);
|
||||
}
|
||||
#settings dt {
|
||||
cyp-settings dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
#settings dd {
|
||||
cyp-settings dd {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
#settings label {
|
||||
cyp-settings dd:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-settings label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
#settings label [type=radio],
|
||||
#settings label [type=checkbox] {
|
||||
cyp-settings label:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
cyp-settings label [type=radio],
|
||||
cyp-settings label [type=checkbox] {
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
@ -792,6 +820,9 @@ nav ul li.active {
|
|||
width: 32px;
|
||||
max-width: 20ch;
|
||||
}
|
||||
.search:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
.search .icon {
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
|
@ -816,32 +847,48 @@ nav ul li.active {
|
|||
.art img {
|
||||
vertical-align: top;
|
||||
}
|
||||
:root {
|
||||
cyp-app {
|
||||
--font-size-large: 112.5%;
|
||||
--icon-spacing: 4px;
|
||||
--primary: rgb(var(--primary-raw));
|
||||
--spacing: 8px;
|
||||
--box-shadow: 0 0 3px #000;
|
||||
}
|
||||
:root[data-theme=light] {
|
||||
cyp-app[theme=light] {
|
||||
--fg: #333;
|
||||
--bg: #f0f0f0;
|
||||
--bg-alt: #e0e0e0;
|
||||
--text-shadow: none;
|
||||
}
|
||||
:root[data-theme=dark] {
|
||||
cyp-app[theme=dark] {
|
||||
--fg: #f0f0f0;
|
||||
--bg: #333;
|
||||
--bg-alt: #555;
|
||||
--text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
:root[data-color=dodgerblue] {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
cyp-app[theme=auto] {
|
||||
--fg: #f0f0f0;
|
||||
--bg: #333;
|
||||
--bg-alt: #555;
|
||||
--text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
cyp-app[theme=auto] {
|
||||
--fg: #333;
|
||||
--bg: #f0f0f0;
|
||||
--bg-alt: #e0e0e0;
|
||||
--text-shadow: none;
|
||||
}
|
||||
}
|
||||
cyp-app[color=dodgerblue] {
|
||||
--primary-raw: 30, 144, 255;
|
||||
}
|
||||
:root[data-color=darkorange] {
|
||||
cyp-app[color=darkorange] {
|
||||
--primary-raw: 255, 140, 0;
|
||||
}
|
||||
:root[data-color=limegreen] {
|
||||
cyp-app[color=limegreen] {
|
||||
--primary-raw: 50, 205, 50;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
|
|
|
@ -5,6 +5,10 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
cyp-app {
|
||||
.flex-column;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
@ -57,7 +61,7 @@ select {
|
|||
@import "mixins.less";
|
||||
@import "range.less";
|
||||
@import "main.less";
|
||||
@import "nav.less";
|
||||
@import "menu.less";
|
||||
@import "player.less";
|
||||
@import "component.less";
|
||||
@import "queue.less";
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
.component {
|
||||
height: 100%;
|
||||
.flex-column;
|
||||
|
||||
header {
|
||||
.flex-row;
|
||||
padding: var(--spacing);
|
||||
|
@ -26,7 +23,7 @@
|
|||
li {
|
||||
.flex-row;
|
||||
|
||||
.info {
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
main {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
cyp-menu {
|
||||
.flex-row;
|
||||
height: 56px;
|
||||
|
||||
li {
|
||||
button {
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
|
||||
.flex-column;
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
padding: 4px 0 8px 0;
|
||||
padding-top: 4px;
|
||||
|
||||
border-top: 4px solid transparent;
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
.flex-row {
|
||||
display: flex;
|
||||
&:not([hidden]) { display: flex; }
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
&:not([hidden]) { display: flex; }
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#player {
|
||||
cyp-player {
|
||||
@art-size: 96px;
|
||||
.flex-row;
|
||||
align-items: stretch;
|
||||
|
@ -36,7 +36,7 @@
|
|||
|
||||
.title, .subtitle { .long-line; }
|
||||
}
|
||||
|
||||
|
||||
.timeline {
|
||||
.flex-row;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#queue {
|
||||
cyp-queue {
|
||||
.component;
|
||||
|
||||
.current { color: var(--primary); }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#settings {
|
||||
cyp-settings {
|
||||
font-size: var(--font-size-large);
|
||||
|
||||
dl {
|
||||
|
@ -26,4 +26,4 @@
|
|||
margin: 0 4px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
:root {
|
||||
cyp-app {
|
||||
--font-size-large: 112.5%;
|
||||
--icon-spacing: 4px;
|
||||
--primary: rgb(var(--primary-raw));
|
||||
|
@ -6,29 +6,40 @@
|
|||
--box-shadow: 0 0 3px #000;
|
||||
}
|
||||
|
||||
:root[data-theme=light] {
|
||||
.light() {
|
||||
--fg: #333;
|
||||
--bg: #f0f0f0;
|
||||
--bg-alt: #e0e0e0;
|
||||
--text-shadow: none;
|
||||
}
|
||||
|
||||
:root[data-theme=dark] {
|
||||
.dark() {
|
||||
--fg: #f0f0f0;
|
||||
--bg: #333;
|
||||
--bg-alt: #555;
|
||||
--text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
:root[data-color=dodgerblue] {
|
||||
cyp-app[theme=light] { .light(); }
|
||||
cyp-app[theme=dark] { .dark(); }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
cyp-app[theme=auto] { .dark(); }
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
cyp-app[theme=auto] { .light(); }
|
||||
}
|
||||
|
||||
cyp-app[color=dodgerblue] {
|
||||
--primary-raw: 30, 144, 255;
|
||||
}
|
||||
|
||||
:root[data-color=darkorange] {
|
||||
cyp-app[color=darkorange] {
|
||||
--primary-raw: 255, 140, 0;
|
||||
}
|
||||
|
||||
:root[data-color=limegreen] {
|
||||
cyp-app[color=limegreen] {
|
||||
--primary-raw: 50, 205, 50;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
<link rel="stylesheet" href="app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<cyp-app component="queue" theme="dark" color="dodgerblue">
|
||||
<header>
|
||||
<section id="player">
|
||||
<cyp-player>
|
||||
<span class="art"></span>
|
||||
<div class="info">
|
||||
<h2 class="title"></h2>
|
||||
|
@ -35,16 +36,16 @@
|
|||
<button class="repeat" data-icon="repeat"></button>
|
||||
<button class="random" data-icon="shuffle"></button>
|
||||
</div>
|
||||
</section>
|
||||
</cyp-player>
|
||||
</header>
|
||||
<main>
|
||||
<section id="queue">
|
||||
<cyp-queue>
|
||||
<header>
|
||||
<button class="clear" data-icon="close" title="Clear the queue"></button>
|
||||
<button class="save" data-icon="content-save" title="Save the queue"></button>
|
||||
</header>
|
||||
<ul></ul>
|
||||
</section>
|
||||
</cyp-queue>
|
||||
<section id="playlists">
|
||||
<ul></ul>
|
||||
</section>
|
||||
|
@ -64,11 +65,12 @@
|
|||
</header>
|
||||
<pre></pre>
|
||||
</section>
|
||||
<section id="settings">
|
||||
<cyp-settings>
|
||||
<dl>
|
||||
<dt>Theme</dt>
|
||||
<dd>
|
||||
<select name="theme">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</select>
|
||||
|
@ -89,25 +91,24 @@
|
|||
</label>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
</cyp-settings>
|
||||
</main>
|
||||
<footer>
|
||||
<nav>
|
||||
<ul>
|
||||
<li data-for="queue" data-icon="music">
|
||||
<div>
|
||||
<span>Queue</span>
|
||||
<span id="queue-length"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li data-for="playlists" data-icon="playlist-music"><span>Playlists</span></li>
|
||||
<li data-for="library" data-icon="library-music"><span>Library</span></li>
|
||||
<li data-for="fs" data-icon="folder"><span>Files</span></li>
|
||||
<li data-for="yt" data-icon="download"><span>YouTube</span></li>
|
||||
<li data-for="settings" data-icon="settings"><span>Settings</span></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
<cyp-menu>
|
||||
<button data-for="queue" data-icon="music">
|
||||
<div>
|
||||
<span>Queue</span>
|
||||
<span id="queue-length"></span>
|
||||
</div>
|
||||
</button>
|
||||
<button data-for="playlists" data-icon="playlist-music"><span>Playlists</span></button>
|
||||
<button data-for="library" data-icon="library-music"><span>Library</span></button>
|
||||
<button data-for="fs" data-icon="folder"><span>Files</span></button>
|
||||
<button data-for="yt" data-icon="download"><span>YouTube</span></button>
|
||||
<button data-for="settings" data-icon="settings"><span>Settings</span></button>
|
||||
</cyp-menu>
|
||||
</footer>
|
||||
</cyp-app>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
import * as nav from "./nav.js";
|
||||
import * as mpd from "./lib/mpd.js";
|
||||
import * as player from "./player.js";
|
||||
import * as html from "./lib/html.js";
|
||||
import * as range from "./lib/range.js";
|
||||
import "./lib/range.js";
|
||||
import "./menu.js";
|
||||
import "./player.js";
|
||||
import "./queue.js";
|
||||
|
||||
import * as mpd from "./lib/mpd.js";
|
||||
import * as mpdMock from "./lib/mpd-mock.js";
|
||||
import * as html from "./lib/html.js";
|
||||
|
||||
import * as queue from "./queue.js";
|
||||
import * as library from "./library.js";
|
||||
import * as fs from "./fs.js";
|
||||
import * as playlists from "./playlists.js";
|
||||
import * as yt from "./yt.js";
|
||||
import * as settings from "./settings.js";
|
||||
|
||||
const components = { queue, library, fs, playlists, yt, settings };
|
||||
|
||||
export function activate(what) {
|
||||
location.hash = what;
|
||||
|
||||
for (let id in components) {
|
||||
let node = document.querySelector(`#${id}`);
|
||||
if (what == id) {
|
||||
node.style.display = "";
|
||||
components[id].activate();
|
||||
} else {
|
||||
node.style.display = "none";
|
||||
}
|
||||
}
|
||||
nav.active(what);
|
||||
}
|
||||
|
||||
function initIcons() {
|
||||
Array.from(document.querySelectorAll("[data-icon]")).forEach(node => {
|
||||
let icon = html.icon(node.dataset.icon);
|
||||
|
@ -35,39 +20,49 @@ function initIcons() {
|
|||
});
|
||||
}
|
||||
|
||||
function fromHash() {
|
||||
let hash = location.hash.substring(1);
|
||||
activate(hash || "queue");
|
||||
}
|
||||
|
||||
function onHashChange(e) {
|
||||
fromHash();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
initIcons();
|
||||
async function mpdExecutor(resolve, reject) {
|
||||
try {
|
||||
await mpd.init();
|
||||
resolve(mpd);
|
||||
} catch (e) {
|
||||
resolve(mpdMock);
|
||||
console.error(e);
|
||||
reject(e);
|
||||
}
|
||||
|
||||
nav.init(document.querySelector("nav"));
|
||||
for (let id in components) {
|
||||
let node = document.querySelector(`#${id}`);
|
||||
components[id].init(node);
|
||||
}
|
||||
|
||||
player.init(document.querySelector("#player"));
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
fromHash();
|
||||
}
|
||||
|
||||
|
||||
init();
|
||||
|
||||
class App extends HTMLElement {
|
||||
get mpd() { return mpd; }
|
||||
constructor() {
|
||||
super();
|
||||
initIcons();
|
||||
|
||||
this._mpd = new Promise(mpdExecutor);
|
||||
|
||||
this._load();
|
||||
}
|
||||
|
||||
get mpd() { return this._mpd; }
|
||||
|
||||
async _load() {
|
||||
const promises = ["cyp-player"].map(name => customElements.whenDefined(name));
|
||||
await Promise.all(promises);
|
||||
|
||||
const onHashChange = () => {
|
||||
const hash = location.hash.substring(1);
|
||||
this._activate(hash || "queue");
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
onHashChange();
|
||||
}
|
||||
|
||||
_activate(what) {
|
||||
location.hash = what;
|
||||
this.setAttribute("component", what);
|
||||
|
||||
const component = this.querySelector(`cyp-${what}`);
|
||||
// component.activate();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cyp-app", App);
|
||||
|
|
|
@ -1,3 +1,32 @@
|
|||
const APP = "cyp-app";
|
||||
|
||||
export default class Component extends HTMLElement {
|
||||
get _app() { return this.closest("cyp-app"); }
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._app.then(app => {
|
||||
let mo = new MutationObserver(mrs => {
|
||||
mrs.forEach(mr => this._onAppAttributeChange(mr));
|
||||
});
|
||||
mo.observe(app, {attributes:true});
|
||||
});
|
||||
}
|
||||
|
||||
_onAppAttributeChange(mr) {
|
||||
if (mr.attributeName != "component") { return; }
|
||||
const component = mr.target.getAttribute(mr.attributeName);
|
||||
const isThis = (this.nodeName.toLowerCase() == `cyp-${component}`);
|
||||
this._onComponentChange(component, isThis);
|
||||
}
|
||||
|
||||
get _app() {
|
||||
return customElements.whenDefined(APP)
|
||||
.then(() => this.closest(APP));
|
||||
}
|
||||
|
||||
get _mpd() {
|
||||
return this._app.then(app => app.mpd);
|
||||
}
|
||||
|
||||
_onComponentChange(component) {}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
264
app/js/player.js
264
app/js/player.js
|
@ -1,140 +1,146 @@
|
|||
//import * as mpd from "./lib/mpd.js";
|
||||
import * as mpd from "./lib/mpd-mock.js";
|
||||
import * as art from "./lib/art.js";
|
||||
import * as html from "./lib/html.js";
|
||||
import * as format from "./lib/format.js";
|
||||
import * as pubsub from "./lib/pubsub.js";
|
||||
import Component from "./component.js";
|
||||
|
||||
const DELAY = 1000;
|
||||
const DOM = {};
|
||||
|
||||
let current = {};
|
||||
let node;
|
||||
let idleTimeout = null;
|
||||
let toggledVolume = 0;
|
||||
|
||||
function sync(data) {
|
||||
if ("volume" in data) {
|
||||
data["volume"] = Number(data["volume"]);
|
||||
|
||||
DOM.mute.disabled = false;
|
||||
DOM.volume.disabled = false;
|
||||
DOM.volume.value = data["volume"];
|
||||
|
||||
if (data["volume"] == 0 && current["volume"] > 0) { // muted
|
||||
toggledVolume = current["volume"];
|
||||
html.clear(DOM.mute);
|
||||
DOM.mute.appendChild(html.icon("volume-off"));
|
||||
}
|
||||
|
||||
if (data["volume"] > 0 && current["volume"] == 0) { // restored
|
||||
toggledVolume = 0;
|
||||
html.clear(DOM.mute);
|
||||
DOM.mute.appendChild(html.icon("volume-high"));
|
||||
}
|
||||
|
||||
} else {
|
||||
DOM.mute.disabled = true;
|
||||
DOM.volume.disabled = true;
|
||||
DOM.volume.value = 50;
|
||||
}
|
||||
|
||||
// changed time
|
||||
let elapsed = Number(data["elapsed"] || 0);
|
||||
DOM.progress.value = elapsed;
|
||||
DOM.elapsed.textContent = format.time(elapsed);
|
||||
|
||||
if (data["file"] != current["file"]) { // changed song
|
||||
if (data["file"]) { // playing at all?
|
||||
let duration = Number(data["duration"]);
|
||||
DOM.duration.textContent = format.time(duration);
|
||||
DOM.progress.max = duration;
|
||||
DOM.progress.disabled = false;
|
||||
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
||||
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
|
||||
} else {
|
||||
DOM.title.textContent = "";
|
||||
DOM.subtitle.textContent = "";
|
||||
DOM.progress.value = 0;
|
||||
DOM.progress.disabled = true;
|
||||
}
|
||||
|
||||
pubsub.publish("song-change", null, data);
|
||||
}
|
||||
|
||||
let artistNew = data["AlbumArtist"] || data["Artist"];
|
||||
let artistOld = current["AlbumArtist"] || current["Artist"];
|
||||
if (artistNew != artistOld || data["Album"] != current["Album"]) { // changed album (art)
|
||||
html.clear(DOM.art);
|
||||
art.get(artistNew, data["Album"], data["file"]).then(src => {
|
||||
if (src) {
|
||||
html.node("img", {src}, "", DOM.art);
|
||||
} else {
|
||||
html.icon("music", DOM.art);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let flags = [];
|
||||
if (data["random"] == "1") { flags.push("random"); }
|
||||
if (data["repeat"] == "1") { flags.push("repeat"); }
|
||||
node.dataset.flags = flags.join(" ");
|
||||
|
||||
node.dataset.state = data["state"];
|
||||
|
||||
current = data;
|
||||
}
|
||||
|
||||
function idle() {
|
||||
idleTimeout = setTimeout(update, DELAY);
|
||||
}
|
||||
|
||||
function clearIdle() {
|
||||
idleTimeout && clearTimeout(idleTimeout);
|
||||
idleTimeout = null;
|
||||
}
|
||||
|
||||
async function command(cmd) {
|
||||
clearIdle();
|
||||
let data = await mpd.commandAndStatus(cmd);
|
||||
sync(data);
|
||||
idle();
|
||||
}
|
||||
|
||||
export async function update() {
|
||||
clearIdle();
|
||||
let data = await mpd.status();
|
||||
sync(data);
|
||||
idle();
|
||||
}
|
||||
|
||||
export function init(n) {
|
||||
node = n;
|
||||
let all = node.querySelectorAll("[class]");
|
||||
Array.from(all).forEach(node => DOM[node.className] = node);
|
||||
|
||||
DOM.progress = DOM.timeline.querySelector("x-range");
|
||||
DOM.volume = DOM.volume.querySelector("x-range");
|
||||
|
||||
DOM.play.addEventListener("click", e => command("play"));
|
||||
DOM.pause.addEventListener("click", e => command("pause 1"));
|
||||
DOM.prev.addEventListener("click", e => command("previous"));
|
||||
DOM.next.addEventListener("click", e => command("next"));
|
||||
|
||||
DOM.random.addEventListener("click", e => command(`random ${current["random"] == "1" ? "0" : "1"}`));
|
||||
DOM.repeat.addEventListener("click", e => command(`repeat ${current["repeat"] == "1" ? "0" : "1"}`));
|
||||
|
||||
DOM.volume.addEventListener("input", e => command(`setvol ${e.target.valueAsNumber}`));
|
||||
DOM.progress.addEventListener("input", e => command(`seekcur ${e.target.valueAsNumber}`));
|
||||
|
||||
DOM.mute.addEventListener("click", e => command(`setvol ${toggledVolume}`));
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
class Player extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this._current = {};
|
||||
this._toggledVolume = 0;
|
||||
this._idleTimeout = null;
|
||||
this._dom = this._initDOM();
|
||||
this._update();
|
||||
}
|
||||
|
||||
_initDOM() {
|
||||
const DOM = {};
|
||||
const all = this.querySelectorAll("[class]");
|
||||
Array.from(all).forEach(node => DOM[node.className] = node);
|
||||
|
||||
DOM.progress = DOM.timeline.querySelector("x-range");
|
||||
DOM.volume = DOM.volume.querySelector("x-range");
|
||||
|
||||
DOM.play.addEventListener("click", _ => this._command("play"));
|
||||
DOM.pause.addEventListener("click", _ => this._command("pause 1"));
|
||||
DOM.prev.addEventListener("click", _ => this._command("previous"));
|
||||
DOM.next.addEventListener("click", _ => this._command("next"));
|
||||
|
||||
DOM.random.addEventListener("click", _ => this._command(`random ${this._current["random"] == "1" ? "0" : "1"}`));
|
||||
DOM.repeat.addEventListener("click", _ => this._command(`repeat ${this._current["repeat"] == "1" ? "0" : "1"}`));
|
||||
|
||||
DOM.volume.addEventListener("input", e => this._command(`setvol ${e.target.valueAsNumber}`));
|
||||
DOM.progress.addEventListener("input", e => this._command(`seekcur ${e.target.valueAsNumber}`));
|
||||
|
||||
DOM.mute.addEventListener("click", _ => this._command(`setvol ${this._toggledVolume}`));
|
||||
|
||||
return DOM;
|
||||
}
|
||||
|
||||
async _command(cmd) {
|
||||
const mpd = await this._mpd;
|
||||
this._clearIdle();
|
||||
const data = await mpd.commandAndStatus(cmd);
|
||||
this._sync(data);
|
||||
this._idle();
|
||||
}
|
||||
|
||||
_idle() {
|
||||
this._idleTimeout = setTimeout(() => this._update(), DELAY);
|
||||
}
|
||||
|
||||
_clearIdle() {
|
||||
this._idleTimeout && clearTimeout(this._idleTimeout);
|
||||
this._idleTimeout = null;
|
||||
}
|
||||
|
||||
async _update() {
|
||||
const mpd = await this._mpd;
|
||||
this._clearIdle();
|
||||
const data = await mpd.status();
|
||||
this._sync(data);
|
||||
this._idle();
|
||||
}
|
||||
|
||||
_sync(data) {
|
||||
const DOM = this._dom;
|
||||
if ("volume" in data) {
|
||||
data["volume"] = Number(data["volume"]);
|
||||
|
||||
DOM.mute.disabled = false;
|
||||
DOM.volume.disabled = false;
|
||||
DOM.volume.value = data["volume"];
|
||||
|
||||
if (data["volume"] == 0 && this._current["volume"] > 0) { // muted
|
||||
this._toggledVolume = this._current["volume"];
|
||||
html.clear(DOM.mute);
|
||||
DOM.mute.appendChild(html.icon("volume-off"));
|
||||
}
|
||||
|
||||
if (data["volume"] > 0 && this._current["volume"] == 0) { // restored
|
||||
this._toggledVolume = 0;
|
||||
html.clear(DOM.mute);
|
||||
DOM.mute.appendChild(html.icon("volume-high"));
|
||||
}
|
||||
|
||||
} else {
|
||||
DOM.mute.disabled = true;
|
||||
DOM.volume.disabled = true;
|
||||
DOM.volume.value = 50;
|
||||
}
|
||||
|
||||
// changed time
|
||||
let elapsed = Number(data["elapsed"] || 0);
|
||||
DOM.progress.value = elapsed;
|
||||
DOM.elapsed.textContent = format.time(elapsed);
|
||||
|
||||
if (data["file"] != this._current["file"]) { // changed song
|
||||
if (data["file"]) { // playing at all?
|
||||
let duration = Number(data["duration"]);
|
||||
DOM.duration.textContent = format.time(duration);
|
||||
DOM.progress.max = duration;
|
||||
DOM.progress.disabled = false;
|
||||
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
||||
DOM.subtitle.textContent = format.subtitle(data, {duration:false});
|
||||
} else {
|
||||
DOM.title.textContent = "";
|
||||
DOM.subtitle.textContent = "";
|
||||
DOM.progress.value = 0;
|
||||
DOM.progress.disabled = true;
|
||||
}
|
||||
|
||||
this._dispatchSongChange(data);
|
||||
}
|
||||
|
||||
let artistNew = data["AlbumArtist"] || data["Artist"];
|
||||
let artistOld = this._current["AlbumArtist"] || this._current["Artist"];
|
||||
if (artistNew != artistOld || data["Album"] != this._current["Album"]) { // changed album (art)
|
||||
html.clear(DOM.art);
|
||||
art.get(artistNew, data["Album"], data["file"]).then(src => {
|
||||
if (src) {
|
||||
html.node("img", {src}, "", DOM.art);
|
||||
} else {
|
||||
html.icon("music", DOM.art);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let flags = [];
|
||||
if (data["random"] == "1") { flags.push("random"); }
|
||||
if (data["repeat"] == "1") { flags.push("repeat"); }
|
||||
this.dataset.flags = flags.join(" ");
|
||||
this.dataset.state = data["state"];
|
||||
|
||||
this._current = data;
|
||||
}
|
||||
|
||||
async _dispatchSongChange(detail) {
|
||||
const app = await this._app;
|
||||
const e = new CustomEvent("song-change", {detail});
|
||||
app.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cyp-player", Player);
|
||||
customElements.define("cyp-player", Player);
|
||||
|
|
125
app/js/queue.js
125
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 pubsub from "./lib/pubsub.js";
|
||||
import * as ui from "./lib/ui.js";
|
||||
|
||||
let node;
|
||||
let currentId;
|
||||
import Component from "./component.js";
|
||||
|
||||
function updateCurrent() {
|
||||
let all = Array.from(node.querySelectorAll("[data-song-id]"));
|
||||
all.forEach(node => {
|
||||
node.classList.toggle("current", node.dataset.songId == currentId);
|
||||
});
|
||||
class Queue extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this._currentId = null;
|
||||
|
||||
this.querySelector(".clear").addEventListener("click", async _ => {
|
||||
const mpd = await this._mpd;
|
||||
await mpd.command("clear");
|
||||
this._sync();
|
||||
});
|
||||
|
||||
this.querySelector(".save").addEventListener("click", async _ => {
|
||||
let name = prompt("Save current queue as a playlist?", "name");
|
||||
if (name === null) { return; }
|
||||
const mpd = await this._mpd;
|
||||
mpd.command(`save "${mpd.escape(name)}"`);
|
||||
});
|
||||
|
||||
this._app.then(app => {
|
||||
app.addEventListener("song-change", this);
|
||||
app.addEventListener("queue-change", this);
|
||||
})
|
||||
this._sync();
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
switch (e.type) {
|
||||
case "song-change":
|
||||
this._currentId = e.detail["Id"];
|
||||
this._updateCurrent();
|
||||
break;
|
||||
|
||||
case "queue-change":
|
||||
this._sync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onComponentChange(c, isThis) {
|
||||
this.hidden = !isThis;
|
||||
|
||||
isThis && this._sync();
|
||||
}
|
||||
|
||||
async _sync() {
|
||||
const mpd = await this._mpd;
|
||||
let songs = await mpd.listQueue();
|
||||
this._buildSongs(songs);
|
||||
|
||||
// FIXME pubsub?
|
||||
document.querySelector("#queue-length").textContent = `(${songs.length})`;
|
||||
}
|
||||
|
||||
_updateCurrent() {
|
||||
let all = Array.from(this.querySelectorAll("[data-song-id]"));
|
||||
all.forEach(node => {
|
||||
node.classList.toggle("current", node.dataset.songId == this._currentId);
|
||||
});
|
||||
}
|
||||
|
||||
_buildSongs(songs) {
|
||||
let ul = this.querySelector("ul");
|
||||
html.clear(ul);
|
||||
|
||||
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
|
||||
|
||||
this._updateCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
function buildSongs(songs) {
|
||||
let ul = node.querySelector("ul");
|
||||
html.clear(ul);
|
||||
|
||||
songs.map(song => ui.song(ui.CTX_QUEUE, song, ul));
|
||||
|
||||
updateCurrent();
|
||||
}
|
||||
|
||||
function onSongChange(message, publisher, data) {
|
||||
currentId = data["Id"];
|
||||
updateCurrent();
|
||||
}
|
||||
|
||||
function onQueueChange(message, publisher, data) {
|
||||
syncQueue();
|
||||
}
|
||||
|
||||
async function syncQueue() {
|
||||
let songs = await mpd.listQueue();
|
||||
buildSongs(songs);
|
||||
document.querySelector("#queue-length").textContent = `(${songs.length})`;
|
||||
}
|
||||
|
||||
export async function activate() {
|
||||
syncQueue();
|
||||
}
|
||||
|
||||
export function init(n) {
|
||||
node = n;
|
||||
syncQueue();
|
||||
pubsub.subscribe("song-change", onSongChange);
|
||||
pubsub.subscribe("queue-change", onQueueChange);
|
||||
|
||||
node.querySelector(".clear").addEventListener("click", async e => {
|
||||
await mpd.command("clear");
|
||||
syncQueue();
|
||||
});
|
||||
|
||||
node.querySelector(".save").addEventListener("click", e => {
|
||||
let name = prompt("Save current queue as a playlist?", "name");
|
||||
if (name === null) { return; }
|
||||
mpd.command(`save "${mpd.escape(name)}"`);
|
||||
});
|
||||
}
|
||||
customElements.define("cyp-queue", Queue);
|
||||
|
|
|
@ -1,52 +1,70 @@
|
|||
import * as mpd from "./lib/mpd.js";
|
||||
import Component from "./component.js";
|
||||
|
||||
let node;
|
||||
let inputs = {};
|
||||
const prefix = "cyp";
|
||||
|
||||
function loadFromStorage(key, def) {
|
||||
return localStorage.getItem(`${prefix}-${key}`) || def;
|
||||
function loadFromStorage(key) {
|
||||
return localStorage.getItem(`${prefix}-${key}`);
|
||||
}
|
||||
|
||||
function saveToStorage(key, value) {
|
||||
return localStorage.setItem(`${prefix}-${key}`, value);
|
||||
}
|
||||
|
||||
function load() {
|
||||
let theme = loadFromStorage("theme", "dark");
|
||||
inputs.theme.value = theme;
|
||||
setTheme(theme);
|
||||
class Settings extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this._inputs = {
|
||||
theme: this.querySelector("[name=theme]"),
|
||||
color: Array.from(this.querySelectorAll("[name=color]"))
|
||||
};
|
||||
|
||||
let color = loadFromStorage("color", "dodgerblue");
|
||||
inputs.color.forEach(input => {
|
||||
input.checked = (input.value == color);
|
||||
input.parentNode.style.color = input.value;
|
||||
});
|
||||
setColor(color);
|
||||
this._load();
|
||||
|
||||
this._inputs.theme.addEventListener("change", e => this._setTheme(e.target.value));
|
||||
this._inputs.color.forEach(input => {
|
||||
input.addEventListener("click", e => this._setColor(e.target.value));
|
||||
});
|
||||
}
|
||||
|
||||
_onAppAttributeChange(mr) {
|
||||
if (mr.attributeName == "theme") { this._syncTheme(); }
|
||||
if (mr.attributeName == "color") { this._syncColor(); }
|
||||
}
|
||||
|
||||
async _syncTheme() {
|
||||
const app = await this._app;
|
||||
this._inputs.theme.value = app.getAttribute("theme");
|
||||
}
|
||||
|
||||
async _syncColor() {
|
||||
const app = await this._app;
|
||||
this._inputs.color.forEach(input => {
|
||||
input.checked = (input.value == app.getAttribute("color"));
|
||||
input.parentNode.style.color = input.value;
|
||||
});
|
||||
}
|
||||
|
||||
async _load() {
|
||||
const app = await this._app;
|
||||
|
||||
const theme = loadFromStorage("theme");
|
||||
(theme ? app.setAttribute("theme", theme) : this._syncTheme());
|
||||
|
||||
const color = loadFromStorage("color");
|
||||
(color ? app.setAttribute("color", color) : this._syncColor());
|
||||
}
|
||||
|
||||
async _setTheme(theme) {
|
||||
const app = await this._app;
|
||||
saveToStorage("theme", theme);
|
||||
app.setAttribute("theme", theme);
|
||||
}
|
||||
|
||||
async _setColor(color) {
|
||||
const app = await this._app;
|
||||
saveToStorage("color", color);
|
||||
app.setAttribute("color", color);
|
||||
}
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
saveToStorage("theme", theme);
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}
|
||||
|
||||
function setColor(color) {
|
||||
saveToStorage("color", color);
|
||||
document.documentElement.dataset.color = color;
|
||||
}
|
||||
|
||||
export async function activate() {}
|
||||
|
||||
export function init(n) {
|
||||
node = n;
|
||||
|
||||
inputs.theme = n.querySelector("[name=theme]");
|
||||
inputs.color = Array.from(n.querySelectorAll("[name=color]"));
|
||||
|
||||
load();
|
||||
|
||||
inputs.theme.addEventListener("change", e => setTheme(e.target.value));
|
||||
inputs.color.forEach(input => {
|
||||
input.addEventListener("click", e => setColor(e.target.value));
|
||||
});
|
||||
}
|
||||
customElements.define("cyp-settings", Settings);
|
||||
|
|
1
index.js
1
index.js
|
@ -9,7 +9,6 @@ const cmd = "youtube-dl";
|
|||
|
||||
function downloadYoutube(q, response) {
|
||||
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
|
||||
// response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
|
||||
console.log("YouTube downloading", q);
|
||||
let args = [
|
||||
|
|
Loading…
Reference in a new issue