diff --git a/Makefile b/Makefile index cb801f2..1109956 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ LESS := $(shell npm bin)/lessc APP := app -CSS := $(APP)/app.css +CSS := $(APP)/cyp.css ICONS := $(APP)/js/lib/icons.js SYSD_USER := ~/.config/systemd/user SERVICE := cyp.service @@ -12,8 +12,8 @@ icons: $(ICONS) $(ICONS): $(APP)/icons/* $(APP)/svg2js.sh $(APP)/icons > $@ -$(CSS): $(APP)/css/* - $(LESS) $(APP)/css/app.less > $@ +$(CSS): $(APP)/css/* $(APP)/css/elements/* + $(LESS) $(APP)/css/cyp.less > $@ service: $(SERVICE) systemctl --user enable $(PWD)/$(SERVICE) diff --git a/app/css/commands.less b/app/css/commands.less deleted file mode 100644 index e69de29..0000000 diff --git a/app/css/app.less b/app/css/cyp.less similarity index 68% rename from app/css/app.less rename to app/css/cyp.less index 47b99db..2c2f3d4 100644 --- a/app/css/app.less +++ b/app/css/cyp.less @@ -13,21 +13,6 @@ main { overflow: auto; } -cyp-app { - .flex-column; - - box-sizing: border-box; - font-family: lato, sans-serif; - line-height: 1.25; - background-color: var(--bg); - color: var(--fg); - text-shadow: var(--text-shadow); - max-width: 800px; - margin: 0 auto; - overflow: hidden; - height: 100vh; -} - header, footer { flex-shrink: 0; z-index: 1; @@ -73,18 +58,20 @@ select { @import "font.less"; @import "icons.less"; @import "mixins.less"; -@import "range.less"; -@import "player.less"; @import "component.less"; -@import "queue.less"; @import "library.less"; @import "fs.less"; -@import "playlists.less"; -@import "yt.less"; -@import "settings.less"; @import "search.less"; @import "art.less"; @import "variables.less"; -@import "song.less"; -@import "menu.less"; +@import "elements/app.less"; +@import "elements/menu.less"; +@import "elements/song.less"; +@import "elements/player.less"; +@import "elements/playlists.less"; +@import "elements/queue.less"; +@import "elements/settings.less"; +@import "elements/yt.less"; +@import "elements/range.less"; +@import "elements/playlist.less"; diff --git a/app/css/elements/app.less b/app/css/elements/app.less new file mode 100644 index 0000000..321e864 --- /dev/null +++ b/app/css/elements/app.less @@ -0,0 +1,14 @@ +cyp-app { + .flex-column; + + box-sizing: border-box; + font-family: lato, sans-serif; + line-height: 1.25; + background-color: var(--bg); + color: var(--fg); + text-shadow: var(--text-shadow); + max-width: 800px; + margin: 0 auto; + overflow: hidden; + height: 100vh; +} diff --git a/app/css/menu.less b/app/css/elements/menu.less similarity index 100% rename from app/css/menu.less rename to app/css/elements/menu.less diff --git a/app/css/player.less b/app/css/elements/player.less similarity index 100% rename from app/css/player.less rename to app/css/elements/player.less diff --git a/app/css/elements/playlist.less b/app/css/elements/playlist.less new file mode 100644 index 0000000..6cc20df --- /dev/null +++ b/app/css/elements/playlist.less @@ -0,0 +1,9 @@ +cyp-playlist { + .flex-row; + + padding: 8px; + + &:nth-child(odd) { + background-color: var(--bg-alt); + } +} diff --git a/app/css/playlists.less b/app/css/elements/playlists.less similarity index 100% rename from app/css/playlists.less rename to app/css/elements/playlists.less diff --git a/app/css/queue.less b/app/css/elements/queue.less similarity index 78% rename from app/css/queue.less rename to app/css/elements/queue.less index b2299f8..50f01d2 100644 --- a/app/css/queue.less +++ b/app/css/elements/queue.less @@ -1,5 +1,3 @@ cyp-queue { - .component; - .current { color: var(--primary); } } diff --git a/app/css/elements/range.less b/app/css/elements/range.less new file mode 120000 index 0000000..6d34d2c --- /dev/null +++ b/app/css/elements/range.less @@ -0,0 +1 @@ +../../../node_modules/custom-range/range.less \ No newline at end of file diff --git a/app/css/settings.less b/app/css/elements/settings.less similarity index 100% rename from app/css/settings.less rename to app/css/elements/settings.less diff --git a/app/css/song.less b/app/css/elements/song.less similarity index 93% rename from app/css/song.less rename to app/css/elements/song.less index c653445..a187253 100644 --- a/app/css/song.less +++ b/app/css/elements/song.less @@ -2,7 +2,7 @@ cyp-song { .selectable; .flex-row; - .info { + .info { // FIXME zrevidovat flex-grow: 1; overflow: hidden; diff --git a/app/css/yt.less b/app/css/elements/yt.less similarity index 100% rename from app/css/yt.less rename to app/css/elements/yt.less diff --git a/app/css/main.less b/app/css/main.less deleted file mode 100644 index e69de29..0000000 diff --git a/app/css/range.less b/app/css/range.less deleted file mode 120000 index cf33e2d..0000000 --- a/app/css/range.less +++ /dev/null @@ -1 +0,0 @@ -../../node_modules/custom-range/range.less \ No newline at end of file diff --git a/app/app.css b/app/cyp.css similarity index 94% rename from app/app.css rename to app/cyp.css index 0c44f8b..7eea054 100644 --- a/app/app.css +++ b/app/cyp.css @@ -13,22 +13,6 @@ main { flex-grow: 1; overflow: auto; } -cyp-app { - flex-direction: column; - box-sizing: border-box; - font-family: lato, sans-serif; - line-height: 1.25; - background-color: var(--bg); - color: var(--fg); - text-shadow: var(--text-shadow); - max-width: 800px; - margin: 0 auto; - overflow: hidden; - height: 100vh; -} -cyp-app:not([hidden]) { - display: flex; -} header, footer { flex-shrink: 0; @@ -128,186 +112,6 @@ select { .selectable.selected { border-left-color: var(--primary); } -x-range { - --thumb-size: 8px; - --thumb-color: #fff; - --thumb-shadow: #000; - --thumb-hover-color: #ddd; - --track-size: 4px; - --track-color: #888; - --track-shadow: #000; - --elapsed-color: #ddd; - --remaining-color: transparent; - --radius: calc(var(--track-size)/2); - display: inline-block; - position: relative; - width: 192px; - height: 16px; -} -x-range .-track, -x-range .-elapsed, -x-range .-remaining { - position: absolute; - top: calc(50% - var(--track-size)/2); - height: var(--track-size); - border-radius: var(--radius); -} -x-range .-track { - width: 100%; - left: 0; - background-color: var(--track-color); - box-shadow: 0 0 1px var(--thumb-shadow); -} -x-range .-elapsed { - left: 0; - background-color: var(--elapsed-color); -} -x-range .-remaining { - right: 0; - background-color: var(--remaining-color); -} -x-range .-inner { - position: absolute; - left: var(--thumb-size); - right: var(--thumb-size); - top: 0; - bottom: 0; -} -x-range .-thumb { - all: unset; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - border-radius: 50%; - width: calc(2*var(--thumb-size)); - height: calc(2*var(--thumb-size)); - background-color: var(--thumb-color); - box-shadow: 0 0 2px var(--thumb-shadow); -} -x-range[disabled] { - opacity: 0.5; -} -x-range:not([disabled]) .-thumb:hover { - background-color: var(--thumb-hover-color); -} -cyp-player { - flex-direction: row; - align-items: center; - align-items: stretch; -} -cyp-player:not([hidden]) { - display: flex; -} -cyp-player:not([data-state=play]) .pause { - display: none; -} -cyp-player[data-state=play] .play { - display: none; -} -cyp-player:not([data-flags~=random]) .random, -cyp-player:not([data-flags~=repeat]) .repeat { - opacity: 0.5; -} -cyp-player x-range { - flex-grow: 1; - --elapsed-color: var(--primary); -} -cyp-player .art { - margin-right: 0; - height: 96px; -} -cyp-player .art img, -cyp-player .art .icon { - width: 96px; -} -cyp-player .info { - flex-grow: 2; - flex-basis: 0; - padding: 0 var(--icon-spacing); - overflow: hidden; - flex-direction: column; - justify-content: space-around; -} -cyp-player .info:not([hidden]) { - display: flex; -} -cyp-player .info h2 { - font-size: 125%; - margin: 0; -} -cyp-player .info .title, -cyp-player .info .subtitle { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -cyp-player .timeline { - flex-direction: row; - align-items: center; -} -cyp-player .timeline:not([hidden]) { - display: flex; -} -cyp-player .timeline .duration, -cyp-player .timeline .elapsed { - flex-basis: 5ch; - text-align: center; -} -cyp-player .controls { - flex-grow: 1; - flex-basis: 0; - max-width: 220px; - flex-direction: column; - justify-content: space-around; -} -cyp-player .controls:not([hidden]) { - display: flex; -} -cyp-player .controls .playback { - flex-direction: row; - align-items: center; - justify-content: space-around; -} -cyp-player .controls .playback:not([hidden]) { - display: flex; -} -cyp-player .controls .playback .icon { - width: 40px; -} -cyp-player .controls .playback .icon-play, -cyp-player .controls .playback .icon-pause { - width: 64px; -} -cyp-player .controls .volume { - flex-direction: row; - align-items: center; -} -cyp-player .controls .volume:not([hidden]) { - display: flex; -} -cyp-player .controls .volume .mute { - margin-right: 4px; -} -cyp-player .misc { - display: flex; - flex-direction: column; - align-self: stretch; - justify-content: space-around; -} -cyp-player .misc .icon { - width: 32px; -} -@media (max-width: 519px) { - cyp-player { - flex-wrap: wrap; - justify-content: space-between; - } - cyp-player .info { - order: 1; - flex-basis: 100%; - height: 96px; - } -} .component header { flex-direction: row; align-items: center; @@ -366,67 +170,6 @@ cyp-player .misc .icon { .component li:nth-child(odd) { background-color: var(--bg-alt); } -cyp-queue header { - flex-direction: row; - align-items: center; - padding: var(--spacing); -} -cyp-queue header:not([hidden]) { - display: flex; -} -cyp-queue header button { - font-size: var(--font-size-large); - font-weight: bold; - overflow: hidden; -} -cyp-queue header button .icon { - margin-right: var(--icon-spacing); -} -cyp-queue ul { - flex-grow: 1; - overflow: auto; - list-style: none; - margin: 0; - padding: 0; -} -cyp-queue li { - flex-direction: row; - align-items: center; -} -cyp-queue li:not([hidden]) { - display: flex; -} -cyp-queue li .info { - flex-grow: 1; - overflow: hidden; -} -cyp-queue li .info .icon { - color: var(--primary); - margin-right: var(--icon-spacing); - filter: drop-shadow(var(--text-shadow)); -} -cyp-queue li .info h2 { - font-size: var(--font-size-large); - margin: 0; -} -cyp-queue li .info h2, -cyp-queue li .info div { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -cyp-queue li:not(.has-art) { - padding: 8px; -} -cyp-queue li button .icon { - width: 32px; -} -cyp-queue li:nth-child(odd) { - background-color: var(--bg-alt); -} -cyp-queue .current { - color: var(--primary); -} #library header { flex-direction: row; align-items: center; @@ -602,6 +345,323 @@ cyp-queue .current { #fs .info h2 { font-weight: normal; } +.search { + flex-direction: row; + align-items: center; + margin-left: auto; + transition: all 300ms; + width: 32px; + max-width: 20ch; +} +.search:not([hidden]) { + display: flex; +} +.search .icon { + width: 32px; + cursor: pointer; +} +.search input { + border: none; + outline: none; + color: inherit; + background-color: inherit; + border-bottom: 1px solid var(--fg); + width: 0; + padding: 0; + flex-grow: 1; +} +.search.open { + flex: 1; +} +.art { + margin-right: var(--icon-spacing); +} +.art .icon, +.art img { + vertical-align: top; +} +cyp-app { + --font-size-large: 112.5%; + --icon-spacing: 4px; + --primary: rgb(var(--primary-raw)); + --spacing: 8px; + --box-shadow: 0 0 3px #000; +} +cyp-app[theme=light] { + --fg: #333; + --bg: #f0f0f0; + --bg-alt: #e0e0e0; + --text-shadow: none; +} +cyp-app[theme=dark] { + --fg: #f0f0f0; + --bg: #333; + --bg-alt: #555; + --text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8); +} +@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; +} +cyp-app[color=darkorange] { + --primary-raw: 255, 140, 0; +} +cyp-app[color=limegreen] { + --primary-raw: 50, 205, 50; +} +@media (max-width: 480px) { + :root { + --spacing: var(--icon-spacing); + } +} +cyp-app { + flex-direction: column; + box-sizing: border-box; + font-family: lato, sans-serif; + line-height: 1.25; + background-color: var(--bg); + color: var(--fg); + text-shadow: var(--text-shadow); + max-width: 800px; + margin: 0 auto; + overflow: hidden; + height: 100vh; +} +cyp-app:not([hidden]) { + display: flex; +} +cyp-menu, +cyp-commands { + flex-direction: row; + align-items: center; + height: 100%; +} +cyp-menu:not([hidden]), +cyp-commands:not([hidden]) { + display: flex; +} +cyp-menu button, +cyp-commands button { + height: 100%; + flex-direction: column; + align-items: center; + justify-content: center; +} +cyp-menu button:not([hidden]), +cyp-commands button:not([hidden]) { + display: flex; +} +@media (max-width: 480px) { + cyp-menu button, + cyp-commands button { + flex-direction: row; + } + cyp-menu button span:not([id]), + cyp-commands button span:not([id]) { + display: none; + } +} +cyp-menu button { + flex: 1 0 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; +} +cyp-menu button .icon { + margin-right: var(--icon-spacing); +} +cyp-menu button.active { + border-top-color: var(--primary); + color: var(--primary); + background-color: rgb(var(--primary-raw), 0.1); +} +cyp-commands { + position: absolute; + left: 0; + top: 0; + width: 100%; + transition: top 300ms; + background-color: var(--bg); +} +cyp-commands[hidden] { + display: flex; + top: 100%; +} +cyp-commands button { + flex: 0 0 80px; +} +cyp-commands button.last { + order: 1; + margin-left: auto; +} +cyp-song { + border-left: 4px solid transparent; + flex-direction: row; + align-items: center; +} +cyp-song.selected { + border-left-color: var(--primary); +} +cyp-song:not([hidden]) { + display: flex; +} +cyp-song .info { + flex-grow: 1; + overflow: hidden; +} +cyp-song .info .icon { + color: var(--primary); + margin-right: var(--icon-spacing); + filter: drop-shadow(var(--text-shadow)); +} +cyp-song .info h2 { + font-size: var(--font-size-large); + margin: 0; +} +cyp-song .info h2, +cyp-song .info div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +cyp-song:nth-child(odd) { + background-color: var(--bg-alt); +} +cyp-song:not(.has-art) { + padding: 8px; +} +cyp-player { + flex-direction: row; + align-items: center; + align-items: stretch; +} +cyp-player:not([hidden]) { + display: flex; +} +cyp-player:not([data-state=play]) .pause { + display: none; +} +cyp-player[data-state=play] .play { + display: none; +} +cyp-player:not([data-flags~=random]) .random, +cyp-player:not([data-flags~=repeat]) .repeat { + opacity: 0.5; +} +cyp-player x-range { + flex-grow: 1; + --elapsed-color: var(--primary); +} +cyp-player .art { + margin-right: 0; + height: 96px; +} +cyp-player .art img, +cyp-player .art .icon { + width: 96px; +} +cyp-player .info { + flex-grow: 2; + flex-basis: 0; + padding: 0 var(--icon-spacing); + overflow: hidden; + flex-direction: column; + justify-content: space-around; +} +cyp-player .info:not([hidden]) { + display: flex; +} +cyp-player .info h2 { + font-size: 125%; + margin: 0; +} +cyp-player .info .title, +cyp-player .info .subtitle { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +cyp-player .timeline { + flex-direction: row; + align-items: center; +} +cyp-player .timeline:not([hidden]) { + display: flex; +} +cyp-player .timeline .duration, +cyp-player .timeline .elapsed { + flex-basis: 5ch; + text-align: center; +} +cyp-player .controls { + flex-grow: 1; + flex-basis: 0; + max-width: 220px; + flex-direction: column; + justify-content: space-around; +} +cyp-player .controls:not([hidden]) { + display: flex; +} +cyp-player .controls .playback { + flex-direction: row; + align-items: center; + justify-content: space-around; +} +cyp-player .controls .playback:not([hidden]) { + display: flex; +} +cyp-player .controls .playback .icon { + width: 40px; +} +cyp-player .controls .playback .icon-play, +cyp-player .controls .playback .icon-pause { + width: 64px; +} +cyp-player .controls .volume { + flex-direction: row; + align-items: center; +} +cyp-player .controls .volume:not([hidden]) { + display: flex; +} +cyp-player .controls .volume .mute { + margin-right: 4px; +} +cyp-player .misc { + display: flex; + flex-direction: column; + align-self: stretch; + justify-content: space-around; +} +cyp-player .misc .icon { + width: 32px; +} +@media (max-width: 519px) { + cyp-player { + flex-wrap: wrap; + justify-content: space-between; + } + cyp-player .info { + order: 1; + flex-basis: 100%; + height: 96px; + } +} cyp-playlists header { flex-direction: row; align-items: center; @@ -670,6 +730,41 @@ cyp-playlists .info:not([hidden]) { cyp-playlists .info h2 { font-weight: normal; } +cyp-queue .current { + color: var(--primary); +} +cyp-settings { + font-size: var(--font-size-large); +} +cyp-settings dl { + margin: var(--spacing); + display: grid; + grid-template-columns: max-content 1fr; + align-items: center; + grid-gap: var(--spacing); +} +cyp-settings dt { + font-weight: bold; +} +cyp-settings dd { + margin: 0; + flex-direction: column; + align-items: start; +} +cyp-settings dd:not([hidden]) { + display: flex; +} +cyp-settings label { + flex-direction: row; + align-items: center; +} +cyp-settings label:not([hidden]) { + display: flex; +} +cyp-settings label [type=radio], +cyp-settings label [type=checkbox] { + margin: 0 4px 0 0; +} cyp-yt header { flex-direction: row; align-items: center; @@ -757,218 +852,76 @@ cyp-yt.pending header { background-position: 100% 100%; } } -cyp-settings { - font-size: var(--font-size-large); +x-range { + --thumb-size: 8px; + --thumb-color: #fff; + --thumb-shadow: #000; + --thumb-hover-color: #ddd; + --track-size: 4px; + --track-color: #888; + --track-shadow: #000; + --elapsed-color: #ddd; + --remaining-color: transparent; + --radius: calc(var(--track-size)/2); + display: inline-block; + position: relative; + width: 192px; + height: 16px; } -cyp-settings dl { - margin: var(--spacing); - display: grid; - grid-template-columns: max-content 1fr; - align-items: center; - grid-gap: var(--spacing); +x-range .-track, +x-range .-elapsed, +x-range .-remaining { + position: absolute; + top: calc(50% - var(--track-size)/2); + height: var(--track-size); + border-radius: var(--radius); } -cyp-settings dt { - font-weight: bold; +x-range .-track { + width: 100%; + left: 0; + background-color: var(--track-color); + box-shadow: 0 0 1px var(--thumb-shadow); } -cyp-settings dd { - margin: 0; - flex-direction: column; - align-items: start; +x-range .-elapsed { + left: 0; + background-color: var(--elapsed-color); } -cyp-settings dd:not([hidden]) { - display: flex; +x-range .-remaining { + right: 0; + background-color: var(--remaining-color); } -cyp-settings label { +x-range .-inner { + position: absolute; + left: var(--thumb-size); + right: var(--thumb-size); + top: 0; + bottom: 0; +} +x-range .-thumb { + all: unset; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + width: calc(2*var(--thumb-size)); + height: calc(2*var(--thumb-size)); + background-color: var(--thumb-color); + box-shadow: 0 0 2px var(--thumb-shadow); +} +x-range[disabled] { + opacity: 0.5; +} +x-range:not([disabled]) .-thumb:hover { + background-color: var(--thumb-hover-color); +} +cyp-playlist { flex-direction: row; align-items: center; -} -cyp-settings label:not([hidden]) { - display: flex; -} -cyp-settings label [type=radio], -cyp-settings label [type=checkbox] { - margin: 0 4px 0 0; -} -.search { - flex-direction: row; - align-items: center; - margin-left: auto; - transition: all 300ms; - width: 32px; - max-width: 20ch; -} -.search:not([hidden]) { - display: flex; -} -.search .icon { - width: 32px; - cursor: pointer; -} -.search input { - border: none; - outline: none; - color: inherit; - background-color: inherit; - border-bottom: 1px solid var(--fg); - width: 0; - padding: 0; - flex-grow: 1; -} -.search.open { - flex: 1; -} -.art { - margin-right: var(--icon-spacing); -} -.art .icon, -.art img { - vertical-align: top; -} -cyp-app { - --font-size-large: 112.5%; - --icon-spacing: 4px; - --primary: rgb(var(--primary-raw)); - --spacing: 8px; - --box-shadow: 0 0 3px #000; -} -cyp-app[theme=light] { - --fg: #333; - --bg: #f0f0f0; - --bg-alt: #e0e0e0; - --text-shadow: none; -} -cyp-app[theme=dark] { - --fg: #f0f0f0; - --bg: #333; - --bg-alt: #555; - --text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8); -} -@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; -} -cyp-app[color=darkorange] { - --primary-raw: 255, 140, 0; -} -cyp-app[color=limegreen] { - --primary-raw: 50, 205, 50; -} -@media (max-width: 480px) { - :root { - --spacing: var(--icon-spacing); - } -} -cyp-song { - border-left: 4px solid transparent; - flex-direction: row; - align-items: center; -} -cyp-song.selected { - border-left-color: var(--primary); -} -cyp-song:not([hidden]) { - display: flex; -} -cyp-song .info { - flex-grow: 1; - overflow: hidden; -} -cyp-song .info .icon { - color: var(--primary); - margin-right: var(--icon-spacing); - filter: drop-shadow(var(--text-shadow)); -} -cyp-song .info h2 { - font-size: var(--font-size-large); - margin: 0; -} -cyp-song .info h2, -cyp-song .info div { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -cyp-song:nth-child(odd) { - background-color: var(--bg-alt); -} -cyp-song:not(.has-art) { padding: 8px; } -cyp-menu, -cyp-commands { - flex-direction: row; - align-items: center; - height: 100%; -} -cyp-menu:not([hidden]), -cyp-commands:not([hidden]) { +cyp-playlist:not([hidden]) { display: flex; } -cyp-menu button, -cyp-commands button { - height: 100%; - flex-direction: column; - align-items: center; - justify-content: center; -} -cyp-menu button:not([hidden]), -cyp-commands button:not([hidden]) { - display: flex; -} -@media (max-width: 480px) { - cyp-menu button, - cyp-commands button { - flex-direction: row; - } - cyp-menu button span:not([id]), - cyp-commands button span:not([id]) { - display: none; - } -} -cyp-menu button { - flex: 1 0 0; - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; -} -cyp-menu button .icon { - margin-right: var(--icon-spacing); -} -cyp-menu button.active { - border-top-color: var(--primary); - color: var(--primary); - background-color: rgb(var(--primary-raw), 0.1); -} -cyp-commands { - position: absolute; - left: 0; - top: 0; - width: 100%; - transition: top 300ms; - background-color: var(--bg); -} -cyp-commands[hidden] { - display: flex; - top: 100%; -} -cyp-commands button { - flex: 0 0 80px; -} -cyp-commands button.last { - order: 1; - margin-left: auto; +cyp-playlist:nth-child(odd) { + background-color: var(--bg-alt); } diff --git a/app/icons/delete.svg b/app/icons/delete.svg new file mode 100644 index 0000000..f9daefe --- /dev/null +++ b/app/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/index.html b/app/index.html index 8e67d55..cb3dd65 100644 --- a/app/index.html +++ b/app/index.html @@ -4,7 +4,7 @@ Control Your Player - + @@ -103,6 +103,15 @@ - + + + + + + + + + + diff --git a/app/js/lib/art.js b/app/js/art.js similarity index 95% rename from app/js/lib/art.js rename to app/js/art.js index 00b4207..18c9129 100644 --- a/app/js/lib/art.js +++ b/app/js/art.js @@ -1,7 +1,6 @@ import * as mpd from "./mpd.js"; -import * as parser from "./parser.js"; import * as html from "./html.js"; -import * as conf from "../conf.js"; +import * as conf from "./conf.js"; let cache = {}; const MIME = "image/jpeg"; diff --git a/app/js/component.js b/app/js/component.js index 2c305c5..27c2471 100644 --- a/app/js/component.js +++ b/app/js/component.js @@ -1,10 +1,17 @@ -import Selection from "./lib/selection.js"; +import Selection from "./selection.js"; export class HasApp extends HTMLElement { get _app() { return this.closest("cyp-app"); } get _mpd() { return this._app.mpd; } } +export class Item extends HasApp { + constructor() { + super(); + this.addEventListener("click", _ => this.parentNode.selection.toggle(this)); + } +} + export default class Component extends HasApp { constructor() { super(); @@ -20,6 +27,6 @@ export default class Component extends HasApp { }); } - _onComponentChange(_component, _isThis) {} _onAppLoad() {} + _onComponentChange(_component, _isThis) {} } diff --git a/app/js/app.js b/app/js/elements/app.js similarity index 69% rename from app/js/app.js rename to app/js/elements/app.js index 3c75758..f355c8a 100644 --- a/app/js/app.js +++ b/app/js/elements/app.js @@ -1,17 +1,9 @@ -import "./lib/range.js"; -import "./menu.js"; -import "./player.js"; -import "./queue.js"; +import * as mpd from "../mpd.js"; +import * as mpdMock from "../mpd-mock.js"; +import * as html from "../html.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 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"; +// import * as library from "./library.js"; +// import * as fs from "./fs.js"; function initIcons() { Array.from(document.querySelectorAll("[data-icon]")).forEach(/** @param {HTMLElement} node */ node => { @@ -37,13 +29,12 @@ class App extends HTMLElement { super(); initIcons(); - - this._mpdPromise = initMpd().then(mpd => this.mpd = mpd); } async connectedCallback() { + this.mpd = await initMpd(); + const promises = ["cyp-player"].map(name => customElements.whenDefined(name)); - promises.push(this._mpdPromise); await Promise.all(promises); diff --git a/app/js/menu.js b/app/js/elements/menu.js similarity index 94% rename from app/js/menu.js rename to app/js/elements/menu.js index 8c90278..42340b5 100644 --- a/app/js/menu.js +++ b/app/js/elements/menu.js @@ -1,4 +1,4 @@ -import Component from "./component.js"; +import Component from "../component.js"; class Menu extends Component { constructor() { diff --git a/app/js/player.js b/app/js/elements/player.js similarity index 96% rename from app/js/player.js rename to app/js/elements/player.js index 04b571c..f0a3fe2 100644 --- a/app/js/player.js +++ b/app/js/elements/player.js @@ -1,7 +1,8 @@ -import * as art from "./lib/art.js"; -import * as html from "./lib/html.js"; -import * as format from "./lib/format.js"; -import Component from "./component.js"; +import * as art from "../art.js"; +import * as html from "../html.js"; +import * as format from "../format.js"; + +import Component from "../component.js"; const DELAY = 1000; diff --git a/app/js/elements/playlist.js b/app/js/elements/playlist.js new file mode 100644 index 0000000..c5ec335 --- /dev/null +++ b/app/js/elements/playlist.js @@ -0,0 +1,22 @@ +import * as html from "../html.js"; +import { Item } from "../component.js"; + +export default class Playlist extends Item { + constructor(name) { + super(); + this.name = name; + } + + connectedCallback() { + html.icon("playlist-music", this) + html.node("h2", {}, this.name, this); + +/* + playButton(TYPE_PLAYLIST, name, node); + addButton(TYPE_PLAYLIST, name, node); + deleteButton(TYPE_PLAYLIST, name, node); +*/ + } +} + +customElements.define("cyp-playlist", Playlist); diff --git a/app/js/playlists.js b/app/js/elements/playlists.js similarity index 67% rename from app/js/playlists.js rename to app/js/elements/playlists.js index 36227fb..8477db9 100644 --- a/app/js/playlists.js +++ b/app/js/elements/playlists.js @@ -1,7 +1,8 @@ -import * as html from "./lib/html.js"; -import * as ui from "./lib/ui.js"; +import * as html from "../html.js"; +import * as ui from "../ui.js"; -import Component from "./component.js"; +import Component from "../component.js"; +import Playlist from "./playlist.js"; class Playlists extends Component { @@ -28,10 +29,9 @@ class Playlists extends Component { } _buildLists(lists) { - let ul = this.querySelector("ul"); - html.clear(ul); + html.clear(this); - lists.map(list => ui.playlist(list, ul)); + lists.forEach(name => this.appendChild(new Playlist(name))); } } diff --git a/app/js/queue.js b/app/js/elements/queue.js similarity index 50% rename from app/js/queue.js rename to app/js/elements/queue.js index 4615131..0822236 100644 --- a/app/js/queue.js +++ b/app/js/elements/queue.js @@ -1,33 +1,12 @@ -import * as html from "./lib/html.js"; -import * as format from "./lib/format.js"; - -import Component, { HasApp } from "./component.js"; +import * as html from "../html.js"; +import Component from "../component.js"; +import Song from "./song.js"; class Queue extends Component { constructor() { super(); this._currentId = null; -/* - this.querySelector(".clear").addEventListener("click", async _ => { - await this._mpd.command("clear"); - this._sync(); FIXME! - }); -*/ - this.selection.addCommand(_ => { - }, {label:"Select all", icon:"plus"}); - - this.selection.addCommand(async items => { - let name = prompt("Save selected songs as a playlist?", "name"); - if (name === null) { return; } - - name = this._mpd.escape(name); - const commands = items.map(item => { - return `playlistadd "${name}" "${this._mpd.escape(item.data["file"])}"`; - }); - - await this._mpd.command(commands); - // FIXME notify? - }, {label:"Save", icon:"content-save"}); + this._initCommands(); } handleEvent(e) { @@ -56,6 +35,7 @@ class Queue extends Component { } async _sync() { + this.selection.clear(); let songs = await this._mpd.listQueue(); this._buildSongs(songs); @@ -76,60 +56,36 @@ class Queue extends Component { this._updateCurrent(); } + + _initCommands() { + const sel = this.selection; + + sel.addCommandAll(); + + sel.addCommand(async items => { + let name = prompt("Save selected songs as a playlist?", "name"); + if (name === null) { return; } + + name = this._mpd.escape(name); + const commands = items.map(item => { + return `playlistadd "${name}" "${this._mpd.escape(item.data["file"])}"`; + }); + + await this._mpd.command(commands); + // FIXME notify? + }, {label:"Save", icon:"content-save"}); + + sel.addCommand(async items => { + if (!confirm(`Remove these ${items.length} songs from the queue?`)) { return; } + + const commands = items.map(item => `deleteid ${item.data["Id"]}`); + await this._mpd.command(commands); + + this._sync(); + }, {label:"Remove", icon:"delete"}); + + sel.addCommandClear(); + } } customElements.define("cyp-queue", Queue); - -class Item extends HasApp { - constructor() { - super(); - this.addEventListener("click", e => this.parentNode.selection.toggle(this)); - } -} - -class Song extends Item { - constructor(data) { - super(); - this.data = data; - this.dataset.songId = data["Id"]; - } - - connectedCallback() { - let info = html.node("div", {className:"info"}, "", this); - - let lines = formatSongInfo(this.data); - html.node("h2", {}, lines.shift(), info); - lines.length && html.node("div", {}, lines.shift(), info); - -/* - playButton(TYPE_ID, id, node); - deleteButton(TYPE_ID, id, node); -*/ - } -} - -customElements.define("cyp-song", Song); - - -// FIXME vyfaktorovat nekam do haje -function formatSongInfo(data) { - let lines = []; - let tokens = []; - - if (data["Title"]) { - tokens.push(data["Title"]); - lines.push(tokens.join(" ")); - lines.push(format.subtitle(data)); - } else { - lines.push(fileName(data)); - lines.push("\u00A0"); - } - - return lines; -} - -// FIXME vyfaktorovat nekam do haje -function fileName(data) { - return data["file"].split("/").pop(); -} - diff --git a/app/js/lib/range.js b/app/js/elements/range.js similarity index 100% rename from app/js/lib/range.js rename to app/js/elements/range.js diff --git a/app/js/settings.js b/app/js/elements/settings.js similarity index 97% rename from app/js/settings.js rename to app/js/elements/settings.js index 5442dd9..79d533d 100644 --- a/app/js/settings.js +++ b/app/js/elements/settings.js @@ -1,4 +1,4 @@ -import Component from "./component.js"; +import Component from "../component.js"; const prefix = "cyp"; diff --git a/app/js/elements/song.js b/app/js/elements/song.js new file mode 100644 index 0000000..7793241 --- /dev/null +++ b/app/js/elements/song.js @@ -0,0 +1,48 @@ +import * as format from "../format.js"; +import * as html from "../html.js"; +import { Item } from "../component.js"; + +export default class Song extends Item { + constructor(data) { + super(); + this.data = data; + this.dataset.songId = data["Id"]; + } + + connectedCallback() { + let info = html.node("div", {className:"info"}, "", this); + + let lines = formatSongInfo(this.data); + html.node("h2", {}, lines.shift(), info); + lines.length && html.node("div", {}, lines.shift(), info); + +/* + playButton(TYPE_ID, id, node); +*/ + } +} + +customElements.define("cyp-song", Song); + + +// FIXME vyfaktorovat nekam do haje +function formatSongInfo(data) { + let lines = []; + let tokens = []; + + if (data["Title"]) { + tokens.push(data["Title"]); + lines.push(tokens.join(" ")); + lines.push(format.subtitle(data)); + } else { + lines.push(fileName(data)); + lines.push("\u00A0"); + } + + return lines; +} + +// FIXME vyfaktorovat nekam do haje +function fileName(data) { + return data["file"].split("/").pop(); +} diff --git a/app/js/yt.js b/app/js/elements/yt.js similarity index 90% rename from app/js/yt.js rename to app/js/elements/yt.js index 5db1a3c..0d2a493 100644 --- a/app/js/yt.js +++ b/app/js/elements/yt.js @@ -1,7 +1,7 @@ -import * as html from "./lib/html.js"; -import * as conf from "./conf.js"; +import * as html from "../html.js"; +import * as conf from "../conf.js"; -import Component from "./component.js"; +import Component from "../component.js"; const decoder = new TextDecoder("utf-8"); diff --git a/app/js/lib/format.js b/app/js/format.js similarity index 100% rename from app/js/lib/format.js rename to app/js/format.js diff --git a/app/js/lib/html.js b/app/js/html.js similarity index 100% rename from app/js/lib/html.js rename to app/js/html.js diff --git a/app/js/lib/icons.js b/app/js/icons.js similarity index 97% rename from app/js/lib/icons.js rename to app/js/icons.js index 6f4de58..34d24eb 100644 --- a/app/js/lib/icons.js +++ b/app/js/icons.js @@ -1,27 +1,15 @@ let ICONS={}; -ICONS["playlist-music"] = ` - +ICONS["library-music"] = ` + +`; +ICONS["plus"] = ` + `; ICONS["folder"] = ` `; -ICONS["shuffle"] = ` - -`; -ICONS["artist"] = ` - -`; -ICONS["download"] = ` - -`; -ICONS["magnify"] = ` - -`; -ICONS["rewind"] = ` - -`; -ICONS["account-multiple"] = ` - +ICONS["playlist-music"] = ` + `; ICONS["settings"] = ` @@ -29,40 +17,55 @@ ICONS["settings"] = ` ICONS["pause"] = ` `; +ICONS["artist"] = ` + +`; ICONS["volume-off"] = ` `; -ICONS["close"] = ` - -`; -ICONS["music"] = ` - -`; -ICONS["minus"] = ` - -`; -ICONS["repeat"] = ` - -`; -ICONS["play"] = ` - -`; -ICONS["plus"] = ` - -`; -ICONS["content-save"] = ` - -`; -ICONS["library-music"] = ` - -`; ICONS["fast-forward"] = ` `; +ICONS["delete"] = ` + +`; ICONS["volume-high"] = ` `; +ICONS["minus"] = ` + +`; +ICONS["play"] = ` + +`; +ICONS["magnify"] = ` + +`; +ICONS["music"] = ` + +`; +ICONS["rewind"] = ` + +`; ICONS["album"] = ` `; +ICONS["download"] = ` + +`; +ICONS["account-multiple"] = ` + +`; +ICONS["close"] = ` + +`; +ICONS["content-save"] = ` + +`; +ICONS["shuffle"] = ` + +`; +ICONS["repeat"] = ` + +`; export default ICONS; diff --git a/app/js/lib/pubsub.js b/app/js/lib/pubsub.js deleted file mode 100644 index 92854aa..0000000 --- a/app/js/lib/pubsub.js +++ /dev/null @@ -1,17 +0,0 @@ -let storage = new Map(); - -export function publish(message, publisher, data) { - console.log(message, publisher, data); - if (!storage.has(message)) { return; } - storage.get(message).forEach(listener => listener(message, publisher, data)); -} - -export function subscribe(message, listener) { - if (!storage.has(message)) { storage.set(message, new Set()); } - storage.get(message).add(listener); -} - -export function unsubscribe(message, listener) { - if (!storage.has(message)) { storage.set(message, new Set()); } - storage.get(message).remove(listener); -} diff --git a/app/js/lib/mpd-mock.js b/app/js/mpd-mock.js similarity index 83% rename from app/js/lib/mpd-mock.js rename to app/js/mpd-mock.js index bb2409b..66808d1 100644 --- a/app/js/lib/mpd-mock.js +++ b/app/js/mpd-mock.js @@ -28,19 +28,18 @@ export function status() { export function listQueue() { return [ - {id:1, Track:5, Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30}, + {Id:1, Track:5, Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30}, status(), - {id:3, Track:7, Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230}, + {Id:3, Track:7, Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230}, ]; } export async function listPlaylists() { - let lines = await command("listplaylists"); - let parsed = parser.linesToStruct(lines); - - let list = parsed["playlist"]; - if (!list) { return []; } - return (list instanceof Array ? list : [list]); + return [ + "Playlist 1", + "Playlist 2", + "Playlist 3" + ]; } export async function enqueueByFilter(filter, sort = null) { diff --git a/app/js/lib/mpd.js b/app/js/mpd.js similarity index 100% rename from app/js/lib/mpd.js rename to app/js/mpd.js diff --git a/app/js/lib/parser.js b/app/js/parser.js similarity index 100% rename from app/js/lib/parser.js rename to app/js/parser.js diff --git a/app/js/lib/search.js b/app/js/search.js similarity index 92% rename from app/js/lib/search.js rename to app/js/search.js index 9bae8c6..b3e5d6c 100644 --- a/app/js/lib/search.js +++ b/app/js/search.js @@ -1,5 +1,5 @@ import * as html from "./html.js"; -import * as conf from "../conf.js"; +import * as conf from "./conf.js"; const OPEN = "open"; const collator = new Intl.Collator(conf.locale, {usage:"search", sensitivity:"base"}); @@ -16,7 +16,7 @@ export default class Search extends EventTarget { if (e.target == this._input) { return; } if (this._node.classList.contains(OPEN)) { this.reset(); - this.dispatchEvent(new Event("input")); + this.dispatchEvent(new Event("input")); } else { this._node.classList.add(OPEN); } diff --git a/app/js/lib/selection.js b/app/js/selection.js similarity index 76% rename from app/js/lib/selection.js rename to app/js/selection.js index 80448da..5dd6626 100644 --- a/app/js/lib/selection.js +++ b/app/js/selection.js @@ -5,21 +5,10 @@ export default class Selection { this._component = component; this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh? this._node = html.node("cyp-commands", {hidden:true}); - - const button = this.addCommand(_ => this.clear(), {icon:"close", label:"Clear"}); - button.classList.add("last"); } clear() { - while (this._items.length) { this._remove(this._items[0]); } - } - - toggle(node) { - if (this._items.includes(node)) { - this._remove(node); - } else { - this._add(node); - } + while (this._items.length) { this.remove(this._items[0]); } } addCommand(cb, options) { @@ -29,14 +18,35 @@ export default class Selection { return button; } - _add(node) { + addCommandAll(items) { + this.addCommand(_ => { + Array.from(this._component.children).forEach(node => this.add(node)); + }, {label:"Select all", icon:"plus"}); + } + + addCommandClear() { + const button = this.addCommand(_ => this.clear(), {icon:"close", label:"Clear"}); + button.classList.add("last"); + return button; + } + + toggle(node) { + if (this._items.includes(node)) { + this.remove(node); + } else { + this.add(node); + } + } + + add(node) { + if (this._items.includes(node)) { return; } const length = this._items.length; this._items.push(node); node.classList.add("selected"); if (length == 0) { this._show(); } } - _remove(node) { + remove(node) { const index = this._items.indexOf(node); this._items.splice(index, 1); node.classList.remove("selected"); diff --git a/app/js/lib/ui.js b/app/js/ui.js similarity index 98% rename from app/js/lib/ui.js rename to app/js/ui.js index 81ba532..397bee3 100644 --- a/app/js/lib/ui.js +++ b/app/js/ui.js @@ -1,9 +1,7 @@ import * as mpd from "./mpd.js"; import * as html from "./html.js"; -import * as pubsub from "./pubsub.js"; import * as format from "./format.js"; import * as art from "./art.js"; -import * as player from "../player.js"; export const CTX_FS = 1; export const CTX_QUEUE = 2;