diff --git a/routes.go b/routes.go index d00144c..90cb2f3 100644 --- a/routes.go +++ b/routes.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "io" "log" @@ -22,6 +23,8 @@ import ( "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/discord" "golang.org/x/exp/slices" + + _ "net/http/pprof" ) const COOKIE_NAME = "_dndmusicbot" @@ -50,6 +53,8 @@ func init() { app.router = httprouter.New() app.router.GET("/", auth(app.Index)) + app.router.GET("/playlists", auth(app.Web_Playlists)) + app.router.GET("/ambiance", auth(app.Web_Ambiance)) app.router.GET("/play/:playlist", auth(app.Play)) app.router.GET("/reset", auth(app.Reset)) app.router.GET("/public/*js", auth(app.ServeFiles)) @@ -71,7 +76,11 @@ func init() { })) go func() { - log.Fatal(http.ListenAndServe(":8824", app.router)) + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + + go func() { + log.Fatal(http.ListenAndServe(":"+config.GetString("web.port"), app.router)) }() } @@ -155,6 +164,10 @@ func (app App) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprouter. func auth(n httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if os.Getenv("APP_ENV") == "test" { + n(w, r, ps) + return + } values := r.URL.Query() values.Add("provider", "discord") r.URL.RawQuery = values.Encode() @@ -232,6 +245,30 @@ func (app App) Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params } } +func (app App) Web_Playlists(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + playlists, err := app.GetPlaylists() + if err != nil { + http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError) + } + + err = json.NewEncoder(w).Encode(playlists) + if err != nil { + http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError) + } +} + +func (app App) Web_Ambiance(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + ambiance, err := GetAmbiances() + if err != nil { + http.Error(w, "Unable to get ambiance. "+err.Error(), http.StatusInternalServerError) + } + + err = json.NewEncoder(w).Encode(ambiance) + if err != nil { + http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError) + } +} + func (app *App) Play(w http.ResponseWriter, r *http.Request, p httprouter.Params) { plname := p.ByName("playlist") diff --git a/src/app/ambiance.tsx b/src/app/ambiance.tsx new file mode 100644 index 0000000..859723f --- /dev/null +++ b/src/app/ambiance.tsx @@ -0,0 +1,188 @@ +import React, { useEffect, useState } from "react"; +import CSS from "csstype"; +import { on } from "./events"; +import ws from "./ws"; +import SortableList, { SortableItem } from "react-easy-sort"; +import { arrayMoveImmutable } from "array-move"; + +export default function Ambiance() { + interface Ambiance { + Id: string; + Title: string; + } + + const [ambiance, setAmbiance] = useState([]); + const [active, setActive] = useState(""); + const [title, setTitle] = useState(""); + const [url, setUrl] = useState(""); + const [percent, setPercent] = useState(0); + const [running, setRunning] = useState(false); + + const onSortEnd = (oldIndex: number, newIndex: number) => { + setAmbiance((array) => arrayMoveImmutable(array, oldIndex, newIndex)); + }; + + const activeStyle: CSS.Properties = { + backgroundColor: "burlywood", + }; + + const getOrder = (name: string) => { + var order = localStorage.getItem("dndmusicbot-" + name); + return order ? order.split("|") : []; + }; + + const setOrder = (name: string) => { + var order = ambiance.map((a) => a.Id); + localStorage.setItem("dndmusicbot-" + name, order.join("|")); + }; + + const fetchAmbiance = () => { + const order = getOrder("ambiance"); + + fetch("/ambiance") + .then((response) => { + return response.json(); + }) + .then((data) => { + data.sort( + (a: Ambiance, b: Ambiance) => + order.indexOf(a.Id) - order.indexOf(b.Id) + ); + setAmbiance(data); + }); + }; + + useEffect(() => { + fetchAmbiance(); + }, []); + + useEffect(() => { + setOrder("ambiance"); + }, [ambiance]); + + const Play = (e: any) => { + ws.send( + JSON.stringify({ + event: "ambiance_play", + payload: e.target.dataset.id, + }) + ); + }; + + const Stop = () => { + ws.send( + JSON.stringify({ + event: "ambiance_stop", + }) + ); + }; + + const AddAmbiance = () => { + if (title == "" || url == "") { + return; + } + + ws.send( + JSON.stringify({ + event: "ambiance_add", + payload: { + title: title, + url: url, + }, + }) + ); + }; + + on("dnd:ambiance_add", () => fetchAmbiance()); + on("dnd:ambiance_add_start", () => setRunning(true)); + on("dnd:ambiance_add_finish", () => { + setRunning(false); + setPercent(0); + setTitle(""); + setUrl(""); + }); + on("dnd:ambiance_play", (e: any) => setActive(e.detail.id)); + on("dnd:ambiance_stop", () => setActive("")); + on("dnd:ambiance_encode_finish", () => setPercent(0)); + on("dnd:ambiance_encode_progress", (e: any) => { + setRunning(true); + + const p = parseInt(e.detail.percent, 10); + if (!Number.isNaN(p)) { + setPercent(p); + } + }); + + return ( + <> +

Ambiance

+
+ + {ambiance.map((item) => { + return ( + +
+ {item.Title} +
+
+ ); + })} +
+ Stop +
+
+
+
+
+ + {percent}% + +
+
+
+
+ setTitle(e.target.value)} + disabled={running} + /> + setUrl(e.target.value)} + disabled={running} + /> + +
+
+ + ); +} diff --git a/src/app/controls.tsx b/src/app/controls.tsx new file mode 100644 index 0000000..b497394 --- /dev/null +++ b/src/app/controls.tsx @@ -0,0 +1,98 @@ +import React, { useState } from "react"; +import CSS from "csstype"; +import { on } from "./events"; +import ws from "./ws"; + +export default function Controls() { + const [channel, setChannel] = useState(""); + const [title, setTitle] = useState(""); + const [pos, setPos] = useState(0); + const [len, setLen] = useState(0); + const [song, setSong] = useState(""); + const [pause, setPause] = useState(true); + + const linkStyle: CSS.Properties = { + textDecoration: "none", + }; + + const Next = () => { + ws.send( + JSON.stringify({ + event: "next", + }) + ); + }; + + const Prev = () => { + ws.send( + JSON.stringify({ + event: "next", + }) + ); + }; + + const msToTime = (duration: number) => { + var milliseconds = (duration % 1000) / 100, + seconds = Math.floor((duration / 1000) % 60), + minutes = Math.floor((duration / (1000 * 60)) % 60), + minutes = minutes < 10 ? 0 + minutes : minutes; + seconds = seconds < 10 ? 0 + seconds : seconds; + + return ( + minutes.toString().padStart(2, "0") + + ":" + + seconds.toString().padStart(2, "0") + ); + }; + + on("dnd:song_info", (e: any) => { + setChannel(e.detail.channel); + setTitle(e.detail.current); + setPos(e.detail.position); + setLen(e.detail.len); + setSong("https://youtu.be/" + e.detail.song); + setPause(e.detail.pause); + }); + + on("dnd:song_position", (e: any) => { + setPos(e.detail.position); + setLen(e.detail.len); + }); + + return ( +
+
+ + {channel} + - + {title} + + +
+ + + {msToTime(pos)} / {msToTime(len)} + + +
+
+
+ ); +} diff --git a/src/app/events.tsx b/src/app/events.tsx new file mode 100644 index 0000000..2e852b2 --- /dev/null +++ b/src/app/events.tsx @@ -0,0 +1,23 @@ +function on(eventType: any, listener: { (event: any): void; (this: Document, ev: any): any; }) { + document.addEventListener(eventType, listener); + } + + function off(eventType: any, listener: { (event: any): void; (this: Document, ev: any): any; }) { + document.removeEventListener(eventType, listener); + } + + function once(eventType: any, listener: (arg0: any) => void) { + on(eventType, handleEventOnce); + + function handleEventOnce(event: any) { + listener(event); + off(eventType, handleEventOnce); + } + } + +function trigger(eventType: string, data: any) { + const event = new CustomEvent(eventType, { detail: data }); + document.dispatchEvent(event); +} + +export { on, once, off, trigger }; \ No newline at end of file diff --git a/src/app/hooks/useDebounce.tsx b/src/app/hooks/useDebounce.tsx new file mode 100644 index 0000000..1a05984 --- /dev/null +++ b/src/app/hooks/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useState, useEffect } from "react"; + +export const useDebounce = (value: any, milliSeconds: number) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, milliSeconds); + + return () => { + clearTimeout(handler); + }; + }, [value, milliSeconds]); + + return debouncedValue; +}; diff --git a/src/app/modal.tsx b/src/app/modal.tsx new file mode 100644 index 0000000..1adba93 --- /dev/null +++ b/src/app/modal.tsx @@ -0,0 +1,26 @@ +import React, { useState } from "react"; +import ws from "./ws"; + +export default function Modal() { + const [waiting, setWaiting] = useState(true); + + ws.onopen = () => { + console.log("false"); + setWaiting(false); + }; + + ws.onclose = () => { + console.log("true"); + setWaiting(true); + }; + + return ( + <> +
+
+

Waiting for bot..

+
+
+ + ); +} diff --git a/src/app/playlist.tsx b/src/app/playlist.tsx new file mode 100644 index 0000000..99bba66 --- /dev/null +++ b/src/app/playlist.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useState } from "react"; +import CSS from "csstype"; +import { on } from "./events"; +import ws from "./ws"; +import SortableList, { SortableItem } from "react-easy-sort"; +import { arrayMoveImmutable } from "array-move"; + +export default function Playlists() { + interface Playlist { + Id: string; + Title: string; + } + + const [playlists, setPlaylists] = useState([]); + const [active, setActive] = useState(""); + const [title, setTitle] = useState(""); + const [url, setUrl] = useState(""); + const [output, setOutput] = useState(""); + + const onSortEnd = (oldIndex: number, newIndex: number) => { + setPlaylists((array) => arrayMoveImmutable(array, oldIndex, newIndex)); + }; + + const activeStyle: CSS.Properties = { + backgroundColor: "burlywood", + }; + + const getOrder = (name: string) => { + var order = localStorage.getItem("dndmusicbot-" + name); + return order ? order.split("|") : []; + }; + + const setOrder = (name: string) => { + var order = playlists.map((a) => a.Id); + localStorage.setItem("dndmusicbot-" + name, order.join("|")); + }; + + const fetchPlaylists = () => { + const order = getOrder("playlist"); + + fetch("/playlists") + .then((response) => { + return response.json(); + }) + .then((data) => { + data.sort( + (a: Playlist, b: Playlist) => + order.indexOf(a.Id) - order.indexOf(b.Id) + ); + setPlaylists(data); + }); + }; + + useEffect(() => { + fetchPlaylists(); + }, []); + + useEffect(() => { + setOrder("playlist"); + }, [playlists]); + + const Play = (e: any) => { + ws.send( + JSON.stringify({ + event: "load_playlist", + payload: e.target.dataset.id, + }) + ); + }; + + const Stop = () => { + ws.send( + JSON.stringify({ + event: "stop", + }) + ); + }; + + const AddPlaylist = () => { + if (title == "" || url == "") { + setOutput("Title or Url is empty!"); + return; + } + + ws.send( + JSON.stringify({ + event: "add_playlist", + payload: { + title: title, + url: url, + }, + }) + ); + }; + + on("dnd:song_info", (e: any) => { + setActive(e.detail.playlist); + }); + + on("dnd:new_playlist", (e: any) => { + fetchPlaylists(); + setOutput("New playlist was added: " + e.details.title); + }); + + return ( + <> +

Playlists

+
+ + {playlists.map((playlist) => { + return ( + +
+ {playlist.Title} +
+
+ ); + })} +
+ Stop +
+
+
+
+
+ setTitle(e.target.value)} + /> + setUrl(e.target.value)} + /> + +
+
+
+

{output}

+
+ + ); +} diff --git a/src/app/root.tsx b/src/app/root.tsx new file mode 100644 index 0000000..8000b8c --- /dev/null +++ b/src/app/root.tsx @@ -0,0 +1,30 @@ +import React, { useState } from "react"; +import { createRoot } from "react-dom/client"; +import { trigger, on } from "./events"; +import Playlists from "./playlist"; +import ws from "./ws"; +import Controls from "./controls"; +import Ambiance from "./ambiance"; +import Volumes from "./volume"; +import Modal from "./modal"; + +function Content() { + ws.onmessage = (e) => { + const data = JSON.parse(e.data); + trigger("dnd:" + data.event, data.payload); + }; + + return ( + <> + + + + + + + ); +} + +const domNode = document.getElementById("content"); +const root = createRoot(domNode!); +root.render(); diff --git a/src/app/volume.tsx b/src/app/volume.tsx new file mode 100644 index 0000000..4b8b39c --- /dev/null +++ b/src/app/volume.tsx @@ -0,0 +1,111 @@ +import React, { useState } from "react"; +import { on } from "./events"; +import ws from "./ws"; +import { useEffect, useRef } from "react"; +import { useDebounce } from "./hooks/useDebounce"; + +export default function Volumes() { + const [playlist, setPlaylist] = useState(0); + const [ambiance, setAmbiance] = useState(0); + + const plfirstUpdate = useRef(true); + const ambfirstUpdate = useRef(true); + const plsend = useDebounce(playlist, 100); + const ambsend = useDebounce(ambiance, 100); + + on("dnd:volume", (e: any) => { + setPlaylist(e.detail.playlist); + setAmbiance(e.detail.ambiance); + }); + + useEffect(() => { + if (plfirstUpdate.current) { + plfirstUpdate.current = false; + return; + } + + ws.send( + JSON.stringify({ + event: "vol_set", + payload: { + type: "playlist", + vol: plsend.toString(), + }, + }) + ); + }, [plsend]); + + useEffect(() => { + if (ambfirstUpdate.current) { + ambfirstUpdate.current = false; + return; + } + ws.send( + JSON.stringify({ + event: "vol_set", + payload: { + type: "ambiance", + vol: ambsend.toString(), + }, + }) + ); + }, [ambsend]); + + const PlaylistVol = (e: any) => { + setPlaylist(parseFloat(e.target.value)); + }; + const AmbianceVol = (e: any) => { + setAmbiance(parseFloat(e.target.value)); + }; + + return ( + <> +
+
+ + + +
+
+ + + +
+
+ + ); +} diff --git a/src/app/ws.tsx b/src/app/ws.tsx new file mode 100644 index 0000000..06b9e8e --- /dev/null +++ b/src/app/ws.tsx @@ -0,0 +1,9 @@ +import ReconnectingWebSocket from "reconnecting-websocket"; + +const ws = new ReconnectingWebSocket( + (window.location.protocol === "https:" ? "wss://" : "ws://") + + window.location.host + + "/ws" +); + +export default ws; diff --git a/src/package.json b/src/package.json index 5e65b04..6d51755 100644 --- a/src/package.json +++ b/src/package.json @@ -1,9 +1,20 @@ { "scripts": { - "build": "esbuild script.js --bundle --outdir=/public/" + "build": "esbuild app/root.tsx --bundle --outfile=../public_test/script.js" }, "dependencies": { + "@types/react": "^18.2.33", + "@types/react-dom": "^18.2.14", + "array-move": "^4.0.0", "esbuild": "^0.15.14", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-debounce-input": "^3.3.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-easy-sort": "^1.6.0", + "react-sortable-hoc": "^2.0.0", "reconnecting-websocket": "^4.4.0", "sortablejs": "^1.15.0" } diff --git a/src/script.js b/src/script.js index 318c40b..c5ac09f 100644 --- a/src/script.js +++ b/src/script.js @@ -147,19 +147,19 @@ window.onload = function () { avol.value = data.payload.ambiance avol.nextElementSibling.value = data.payload.ambiance break - case "ambiance_download_start": + case "ambiance_encode_start": var progress = document.querySelector("#progressambiance progress") progress.style.display = "initial" progress.style.width = "100%" progress.value = 0 break - case "ambiance_download_progress": + case "ambiance_encode_progress": var progress = document.querySelector("#progressambiance progress") progress.style.display = "initial" progress.value = data.payload.percent console.log(data) break - case "ambiance_download_complete": + case "ambiance_encode_complete": var progress = document.querySelector("#progressambiance progress") progress.style.display = "initial" progress.value = 100 diff --git a/src/yarn.lock b/src/yarn.lock index edb50a8..1daf036 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.9.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + "@esbuild/android-arm@0.15.14": version "0.15.14" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.14.tgz#5d0027f920eeeac313c01fd6ecb8af50c306a466" @@ -12,6 +19,96 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.14.tgz#1221684955c44385f8af34f7240088b7dc08d19d" integrity sha512-eQi9rosGNVQFJyJWV0HCA5WZae/qWIQME7s8/j8DMvnylfBv62Pbu+zJ2eUDqNf2O4u3WB+OEXyfkpBoe194sg== +"@react-dnd/asap@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" + integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A== + +"@react-dnd/invariant@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df" + integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw== + +"@react-dnd/shallowequal@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4" + integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA== + +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.4" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz#cc477ce0283bb9d19ea0cbfa2941fe2c8493a1be" + integrity sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/prop-types@*": + version "15.7.9" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" + integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g== + +"@types/react-dom@^18.2.14": + version "18.2.14" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539" + integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ== + dependencies: + "@types/react" "*" + +"@types/react-redux@^7.1.20": + version "7.1.28" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.28.tgz#30a44303c7daceb6ede9cfb4aaf72e64f1dde4de" + integrity sha512-EQr7cChVzVUuqbA+J8ArWK1H0hLAHKOs21SIMrskKZ3nHNeE+LFYA+IsoZGhVOT8Ktjn3M20v4rnZKN3fLbypw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*", "@types/react@^18.2.33": + version "18.2.33" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877" + integrity sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.5" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af" + integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw== + +array-move@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/array-move/-/array-move-3.0.1.tgz#179645cc0987b65953a4fc06b6df9045e4ba9618" + integrity sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg== + +array-move@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/array-move/-/array-move-4.0.0.tgz#2c3730f056cc926f62a59769a5a8cda2fb6d8c55" + integrity sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ== + +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + +csstype@^3.0.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +dnd-core@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" + integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng== + dependencies: + "@react-dnd/asap" "^5.0.1" + "@react-dnd/invariant" "^4.0.1" + redux "^4.2.0" + esbuild-android-64@0.15.14: version "0.15.14" resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.14.tgz#114e55b0d58fb7b45d7fa3d93516bd13fc8869cc" @@ -140,12 +237,199 @@ esbuild@^0.15.14: esbuild-windows-64 "0.15.14" esbuild-windows-arm64 "0.15.14" +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +lodash.debounce@^4: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +prop-types@^15.5.7, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + +react-beautiful-dnd@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + +react-debounce-input@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/react-debounce-input/-/react-debounce-input-3.3.0.tgz#85e3ebcaa41f2016e50613134a1ec9fe3cdb422e" + integrity sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA== + dependencies: + lodash.debounce "^4" + prop-types "^15.8.1" + +react-dnd-html5-backend@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6" + integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw== + dependencies: + dnd-core "^16.0.1" + +react-dnd@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37" + integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q== + dependencies: + "@react-dnd/invariant" "^4.0.1" + "@react-dnd/shallowequal" "^4.0.1" + dnd-core "^16.0.1" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-easy-sort@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/react-easy-sort/-/react-easy-sort-1.6.0.tgz#b40cce827913f0640c1b2e5438dd4d007e26db32" + integrity sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg== + dependencies: + array-move "^3.0.1" + tslib "2.0.1" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-redux@^7.2.0: + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + +react-sortable-hoc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7" + integrity sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg== + dependencies: + "@babel/runtime" "^7.2.0" + invariant "^2.2.4" + prop-types "^15.5.7" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + reconnecting-websocket@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== +redux@^4.0.0, redux@^4.0.4, redux@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + sortablejs@^1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a" integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w== + +tiny-invariant@^1.0.6: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tslib@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== diff --git a/tmpl/index.tmpl b/tmpl/index.tmpl index 49a7e88..318017c 100644 --- a/tmpl/index.tmpl +++ b/tmpl/index.tmpl @@ -10,90 +10,7 @@ -
-

Playlists

- -
-
- {{ range .Playlists }} -
{{ .Title }}
- {{ end}} -
Stop
-
-
- -
-
- - - -
-
- -
-

- -
- - - - - - - -
- - - -
-
-
- -

Ambiance

- -
-
- {{ range .Ambiance }} -
{{ .Title }}
- {{ end}} -
Stop
-
-
- -
-
- 0% -
-
- -
-
- - - -
-
- -
-
- - - -
-
- - - -
-
- - - -
- +
+ - + \ No newline at end of file