site_liveq/static/index.html

1243 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<title>Rhythm Game Live Queue</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<style>
html,
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: #4C6D28;
}
a,
a:visited,
a:active,
a:hover {
color: #C7DB5E;
}
.btn,
a.btn,
a.btn:visited,
a.btn:active,
a.btn:hover {
color: #FFFFFF;
text-decoration: none;
padding: 0.75rem;
border-radius: 0.35rem;
border: 0.1rem solid #00000080;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.link-like {
text-decoration: underline;
cursor: pointer;
}
.sleeper-link-like {
cursor: pointer;
}
.btn-square {
width: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
input.btn-height {
height: 3rem;
}
button.btn-square {
height: 3rem;
width: 3rem;
}
.btn-gray {
color: #FFFFFF;
background-color: #484848;
}
.btn-purple {
color: #FFFFFF;
background-color: #6D2AB0;
}
.btn-danger {
color: #FFFFFF;
background-color: #B02A2A;
}
.btn-success {
color: #FFFFFF;
background-color: #6DB02A;
}
.btn-info {
color: #FFFFFF;
background-color: #2AB0B0;
}
.btn-primary {
color: #FFFFFF;
background-color: #203FBF;
}
.btn-alert {
color: #FFFFFF;
background-color: #BFA021;
}
.seamless-join-left {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.seamless-join-right {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
header {
background-color: #00000080;
padding-top: 1rem;
padding-bottom: 1rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
height: 3rem;
}
header h1 {
margin-top: 0;
margin-bottom: 0;
color: #FFFFFF;
}
.container {
padding-left: calc(50% - 30rem);
max-width: 60rem;
}
.dflex {
display: flex;
}
.jcsb {
justify-content: space-between;
}
aside#notificationArea {
position: fixed;
right: max(calc(50vw - 30rem + 0.40rem), 0.1rem);
top: 5rem;
width: 15rem;
}
aside#notificationArea>div {
z-index: 9999;
transition-property: all;
transition-duration: 500ms;
transition-timing-function: ease-in-out;
transition-delay: 0ms;
background-color: #00000080;
border: 0.1rem solid #00000080;
color: #F9EBBE;
border-radius: 0.5rem;
width: calc(100% - 2rem - 0.25rem);
overflow: hidden;
padding-left: 1rem;
padding-right: 1rem;
display: flex;
align-items: center;
justify-content: left;
}
aside#notificationArea>div>i.fa {
margin-right: 0.75rem;
}
aside#notificationArea>div:not(.height-none-2d) {
margin-top: 0.25rem;
padding-top: 1rem;
padding-bottom: 1rem;
height: 4rem;
}
aside#notificationArea>div.height-none-2d {
height: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
aside#notificationArea>div:not(.opacity-none) {
opacity: 1;
}
aside#notificationArea>div.opacity-none {
opacity: 0;
}
main {
padding-left: max(calc(50% - 30rem), 0.25rem);
max-width: calc(min(100vw, 60rem) - 0.5rem);
height: calc(100vh - 5rem - 0.5rem);
display: flex;
align-items: stretch;
justify-content: stretch;
flex-direction: column;
padding-top: 0.25rem;
}
main>section {
margin-bottom: 0.25rem;
background-color: #00000080;
border-radius: 0.75rem;
height: calc(calc(100vh - 5rem - 1rem) / 2);
border: 0.1rem solid #00000080;
}
main>section#songs {
display: flex;
flex-direction: column;
}
main>section#songs>h2 {
color: #C7DB5E;
padding-top: 0;
margin-top: 0;
padding-bottom: 0;
margin-bottom: 0;
height: 3rem;
display: flex;
align-items: center;
margin-left: 0.5rem;
}
main>section#songs>div {
flex-grow: 1;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
margin-left: 0.25rem;
margin-right: 0.25rem;
border: 0.1rem solid #00000080;
background-color: #00000040;
overflow-y: scroll;
border-radius: 0.25rem;
}
main>section#songs>div>div {
color: #C7DB5E;
height: 9rem;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
main>section#songs>div>div>img {
width: 8.75rem;
height: 8.75rem;
border: 0.1rem solid #00000080;
border-radius: 0.25rem;
}
main>section#songs>div>div>div {
width: calc(100% - 8.75rem - 0.75rem);
margin-left: 0.5rem;
display: flex;
}
main>section#songs>div>div>div>div {
width: 50%;
white-space: nowrap;
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: space-around;
}
main>section#search>div:first-child {
display: flex;
align-items: center;
margin: 0.25rem;
}
main>section#search>div:first-child>input {
flex-grow: 1;
height: 2.8rem;
padding-top: 0;
padding-bottom: 0;
padding-left: 0.75rem;
padding-right: 0.75rem;
margin: 0;
border-top: 0.1rem solid #00000080;
border-bottom: 0.1rem solid #00000080;
border-left: 0 none black;
border-right: 0 none black;
background-color: #00000080;
color: #C7DB5E;
}
main>section#search>div:not(:first-child) {
flex-grow: 1;
margin-bottom: 0.25rem;
margin-left: 0.25rem;
margin-right: 0.25rem;
border: 0.1rem solid #00000080;
background-color: #00000040;
overflow-y: scroll;
border-radius: 0.25rem;
}
main>section#search {
display: flex;
flex-direction: column;
}
main>section#search>div:not(:first-child)>div {
color: #C7DB5E;
height: 9rem;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
main>section#search>div:not(:first-child)>div>img {
width: 8.75rem;
height: 8.75rem;
border: 0.1rem solid #00000080;
border-radius: 0.25rem;
}
main>section#search>div:not(:first-child)>div>div {
width: calc(100% - 8.75rem - 0.75rem);
margin-left: 0.5rem;
display: flex;
}
main>section#search>div:not(:first-child)>div>div>div {
width: 50%;
white-space: nowrap;
overflow-x: hidden;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.difficulty-too-easy {
color: #5dc5da;
}
.difficulty-okay {
color: #C7DB5E;
}
.difficulty-too-hard {
color: #da5dc5;
}
.difficulty-disabled {
color: #9c9c9c;
}
.difficulty-unsuitable {
font-weight: 200;
color: #9c9c9c;
}
.difficulty-suitable {
font-weight: 900;
color: #F9EBBE;
}
.quarter-opacity {
opacity: 0.25;
}
.half-opacity {
opacity: 0.5;
}
</style>
<link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
</head>
<body>
<main>
<h1>JavaScript required</h1>
<p>JavaScript is required to run this app</p>
</main>
<script defer>
(function () {
while (document.body.firstChild !== null && document.body.firstChild !== undefined) {
document.body.removeChild(document.body.firstChild);
}
})();
/**
* @param {string} mtd
* @param {string} uri
* @param {string?} authorization
* @param {string?} content
* @param {string?} content_type
* @returns {Promise<object?>}
*/
function doReqParsingJson(mtd, uri, authorization, content, content_type) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.addEventListener("load", function (ev) {
if (xhr.readyState == 4) {
if (xhr.status < 200 || xhr.status >= 300) {
let resp = `${xhr.status} ${xhr.statusText}`;
try {
let r2 = JSON.parse(xhr.response)
resp = r2.detail || resp;
} catch (e) { }
reject(resp);
}
else
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
reject(e);
}
}
});
xhr.addEventListener("abort", reject);
xhr.addEventListener("error", reject);
xhr.open(mtd || "GET", uri);
if (authorization)
xhr.setRequestHeader('Authorization', authorization);
if (content_type)
xhr.setRequestHeader('Content-Type', content_type)
xhr.send(content);
})
}
/**
* @param {number} ms
* @returns {Promise<void>}
*/
function asyncSleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
class AsyncLazyEval {
/**
* @param {Promise<Object>} promise
*/
constructor(promise) {
this._promise = promise;
this._evaluating = false;
this._evaluated = false;
this._thrown = false;
this._value = null;
}
get eval() {
if (this._evaluated)
return new Promise((resolve, reject) => {
if (!this._thrown)
resolve(this._value)
else
reject(this._value)
});
else if (!this._evaluating) {
this._evaluating = true;
return new Promise((resolve, reject) => {
this._promise.then((r) => {
this._evaluating = false;
this._evaluated = true;
this._thrown = false;
this._value = r;
resolve(r);
}).catch((r) => {
this._evaluating = false;
this._evaluated = true;
this._thrown = true;
this._value = r;
reject(r)
})
})
} else {
let p = new Promise(async (resolve, reject) => {
while (!this._evaluated)
await asyncSleep(100);
if (!this._thrown)
resolve(this._value)
else
reject(this._value)
});
return p;
}
}
}
class StringTranslator {
constructor(o) {
this.o = o;
}
/**
* @param {string} s
* @return {string}
*/
t(s) {
let k = String(s).toLowerCase().replace(/\ /g, '_');
return this.o[k] || s;
}
}
class OnScreenNotification {
/**
* @param {string} selector
* @param {string} icon
* @param {string} text
*/
constructor(selector, icon, text) {
this.selector = String(selector);
this.icon = String(icon);
this.text = String(text);
}
/**
* @returns {Promise}
*/
async show() {
let element = document.querySelector(this.selector);
let container = document.createElement('div');
let sicon = document.createElement('i');
let stext = document.createElement('span');
stext.innerText = this.text;
sicon.setAttribute('aria-hidden', 'true');
sicon.classList.add('fa');
sicon.classList.add('fa-2x');
sicon.classList.add(`fa-${this.icon}`);
container.appendChild(sicon);
container.appendChild(stext);
container.classList.add('opacity-none');
element.appendChild(container);
await asyncSleep(100);
container.classList.remove('opacity-none');
await asyncSleep(500);
await asyncSleep(2000);
container.classList.add('opacity-none');
container.classList.add('height-none-2d');
await asyncSleep(500);
element.removeChild(container);
}
}
class OnScreenNotificationSpawner {
/**
* @param {string} selector
*/
constructor(selector) {
this.selector = selector;
}
/**
* @param {string} icon
* @param {string} text
* @return {Promise}
*/
spawn(icon, text) {
return new OnScreenNotification(
this.selector, icon, text
).show();
}
}
class OnScreenTranslatedNotificationSpawner {
/**
* @param {string} selector
* @param {()=>StringTranslator} i18n_getter
* @param {AsyncLazyEval} i18n_awaiter
*/
constructor(selector, i18n_getter, i18n_awaiter) {
this.selector = selector;
this.i18n_getter = i18n_getter;
this.i18n_awaiter = i18n_awaiter;
}
/**
* @param {string} icon
* @param {string} text
* @return {Promise}
*/
spawn(icon, text) {
return this.i18n_awaiter.eval.finally(async () => {
return await new OnScreenNotification(
this.selector, icon, this.i18n_getter().t(text)
).show();
});
}
/**
* @param {string} text
* @return {Promise}
*/
info(text) { return this.spawn('info-circle', text); }
/**
* @param {string} text
* @return {Promise}
*/
warn(text) { return this.spawn('warning', text); }
/**
* @param {string} text
* @return {Promise}
*/
error(text) { return this.spawn('times-circle', text); }
}
class SongListDisplayable {
/**
* @param {Object} config
* @param {Object[]} songs
* @param {OnScreenTranslatedNotificationSpawner} notifier
* @param {StringTranslator} translator
* @param {Object} games
* @param {HTMLDivElement} itembox
*/
constructor(config, user, songs, notifier, translator, games, itembox) {
this.config = config;
this.user = user;
this.songs = songs;
this.notifier = notifier;
this.translator = translator;
this.games = games;
this.itembox = itembox;
}
_gen_fa_keypair(icon, text) {
let info_part_c = document.createElement('div');
let info_part_c_i = document.createElement('i');
let info_part_c_s = document.createElement('span');
info_part_c.appendChild(info_part_c_i);
info_part_c.appendChild(info_part_c_s);
info_part_c_i.classList.add('fa');
info_part_c_i.classList.add('fa-' + icon);
info_part_c_s.innerText = ' ' + text;
return info_part_c
}
_gen_a_href(href, text) {
let a = document.createElement('a');
a.setAttribute('target', '_blank');
a.setAttribute('href', href);
a.innerText = text;
return a
}
_gen_fa_keypair_elem(icon, elem) {
let info_part_c = document.createElement('div');
let info_part_c_i = document.createElement('i');
let info_part_c_s = document.createElement('span');
info_part_c.appendChild(info_part_c_i);
info_part_c.appendChild(info_part_c_s);
info_part_c.appendChild(elem);
info_part_c_i.classList.add('fa');
info_part_c_i.classList.add('fa-' + icon);
info_part_c_s.innerText = ' ';
return info_part_c
}
_gen_player(href, text_play, text_stop) {
let l = document.createElement('span');
let a = document.createElement('audio');
let s = document.createElement('source');
let b = document.createTextNode('Your browser does not support the audio tag.')
let c = document.createTextNode(text_play);
let p = document.createElement('i');
a.setAttribute('muted', 'false');
a.setAttribute('preload', 'none');
s.setAttribute('src', href);
a.appendChild(s);
a.appendChild(b);
l.appendChild(a);
l.appendChild(c);
l.appendChild(p);
l.classList.add('link-like');
p.classList.add('fa');
p.setAttribute('aria-hidden', 'true');
p.setAttribute('class', 'fa fa-battery-0');
p.style.opacity = 0;
let playing = false;
a.addEventListener('timeupdate', (ev) => {
let v = Math.floor((5 * a.currentTime / Math.max(0.00001, a.duration)) % 5);
v = Math.max(0, Math.min(4, v));
let classname = `fa fa-battery-${v}`;
p.setAttribute('class', classname);
})
a.addEventListener('play', () => {
playing = true;
c.data = text_stop + ' ';
p.style.opacity = 1;
})
a.addEventListener('pause', () => {
playing = false;
c.data = text_play;
p.style.opacity = 0;
})
a.addEventListener('ended', () => {
playing = false;
c.data = text_play;
p.style.opacity = 0;
})
l.onclick = () => {
if (!playing) {
[...document.querySelectorAll('audio')].forEach(a => {
a.pause();
})
a.fastSeek(0);
a.play();
} else {
a.pause();
a.fastSeek(0);
}
}
return l
}
_gen_fa_keypair_sleeper_searcher(icon, text) {
let info_part_c = document.createElement('div');
let info_part_c_i = document.createElement('i');
let info_part_c_s = document.createElement('span');
info_part_c.appendChild(info_part_c_i);
info_part_c.appendChild(info_part_c_s);
info_part_c_i.classList.add('fa');
info_part_c_i.classList.add('fa-' + icon);
info_part_c_s.classList.add('sleeper-link-like');
info_part_c_s.innerText = ' ' + text;
info_part_c_s.onclick = () => {
this.ui_set(text);
this.ui_search(text);
};
return info_part_c
}
_gen_searcher(sid, text) {
let a = document.createElement('span');
a.classList.add('link-like');
a.innerText = text;
a.onclick = () => {
this.ui_set(`${sid}`);
this.ui_search(`${sid}`);
};
return a
}
_gen_queue_manipulator(action, id, text) {
let a = document.createElement('span');
a.classList.add('link-like');
a.innerText = text;
a.onclick = () => {
let url_root = `${window.location.protocol}//${window.location.host}`;
let url_api = `${url_root}/api`;
let url_queue_add = `${url_api}/queue/${action}`;
let url_queue_add_this = `${url_queue_add}?beatmap=${id}`;
doReqParsingJson("POST", url_queue_add_this, window.localStorage.token).then(v => {
this.notifier.info(v.detail);
return v;
}).catch(v => {
this.notifier.error(v);
return v;
}).finally(() => this.onchange.forEach(i => i()))
};
return a
}
_gen_enqueuer(id, text) {
return this._gen_queue_manipulator('add', id, text);
}
_gen_dequeuer(id, text) {
return this._gen_queue_manipulator('delete', id, text);
}
_gen_qplay(id, text) {
return this._gen_queue_manipulator('play', id, text);
}
_gen_qreplay(id, text) {
return this._gen_queue_manipulator('unplay', id, text);
}
_calc_deviance(difficulty, allowed_difficulty) {
if (typeof difficulty !== 'number')
return null;
let difficulty_pair = String(allowed_difficulty).split('-').map(x => parseFloat(x)).filter(x => !isNaN(x));
if (difficulty_pair.length != 2)
return null;
if (difficulty < difficulty_pair[0])
return difficulty - difficulty_pair[0];
if (difficulty > difficulty_pair[1])
return difficulty - difficulty_pair[1];
return 0;
}
_calc_deviance_fmt(difficulty, allowed_difficulty) {
let a = this._calc_deviance(difficulty, allowed_difficulty);
if (a === null) return '';
if (a < 0) return a.toFixed(2);
if (a > 0) return '+' + a.toFixed(2);
return '0';
}
_evaluate_difficulty(difficulty, allowed_difficulty) {
let a = this._calc_deviance(difficulty, allowed_difficulty);
if (a === null) return 0;
if (a < 0) return 1;
if (a > 0) return 2;
return 3;
}
_is_my_queue_full() {
if (this.user?.permissions?.is_admin)
return false;
let maxq = parseInt(this.config?.app?.maxqueue);
let viewq = parseInt(this.config?.app?.maxbyviewer);
let qtc = 0;
let qmc = 0;
for (let item of this.queue) {
if (!item.played) {
qtc++;
if (this.user?.user?.id == item.requester_uid && this.user?.user?.platform == item.requester_platform)
qmc++;
}
}
return (qtc >= maxq) || (qmc >= viewq);
}
_can_add_to_queue(song, is_queue_full) {
let queueitem = this.queue_id2obj[song.id];
if (queueitem) return false;
if (this.user?.permissions?.is_admin)
return true;
return !is_queue_full;
}
async _update_search_display() {
this.itembox.scrollTop = 0;
while (!!this.itembox.firstChild)
this.itembox.removeChild(this.itembox.firstChild)
let queue_full = this._is_my_queue_full();
let i = 600;
for (let song of this.display_songs) {
let root = document.createElement('div');
let thumb = document.createElement('img');
let info = document.createElement('div');
let infol = document.createElement('div');
let infor = document.createElement('div');
thumb.setAttribute('loading', 'lazy');
thumb.setAttribute('src', `/${song.thumbnail}`);
thumb.classList.add('sleeper-link-like');
root.appendChild(thumb);
root.appendChild(info);
info.appendChild(infol);
info.appendChild(infor);
let difficulty = song.difficulty;
let allowed_difficulty = this.config?.difficulties?.[String(this.games.v2n[String(song.gamemode)])] || null;
let difficulty_rank = this._evaluate_difficulty(difficulty, allowed_difficulty);
let game_enabled = this.config?.modules?.[String(this.games.v2n[String(song.gamemode)])] == '1';
let deviance = (difficulty_rank == 1 || difficulty_rank == 2) ? ` (${this._calc_deviance_fmt(difficulty, allowed_difficulty)})` : '';
let elem_difficulty = this._gen_fa_keypair('road', `${song.difficulty.toFixed(2)}${deviance} - ${song.difficulty_name}`);
let elem_game = this._gen_fa_keypair('gamepad', this.translator.t(String(this.games.v2n[String(song.gamemode)])));
if (!game_enabled) elem_game.classList.add('difficulty-disabled');
switch (difficulty_rank) {
case 1: elem_difficulty.classList.add('difficulty-too-easy'); break;
case 2: elem_difficulty.classList.add('difficulty-too-hard'); break;
case 3: elem_difficulty.classList.add('difficulty-okay'); break;
default: elem_difficulty.classList.add('difficulty-disabled');
}
infol.appendChild(this._gen_fa_keypair_sleeper_searcher('music', song.title));
infol.appendChild(this._gen_fa_keypair_sleeper_searcher('user', song.artist));
infol.appendChild(this._gen_fa_keypair_sleeper_searcher('map-o', song.mapper));
infol.appendChild(elem_difficulty);
infol.appendChild(elem_game);
let player = this._gen_player(`/${song.preview}`, this.translator.t('preview'), this.translator.t('stop_preview'));
thumb.onclick = () => { player.click(); };
infor.appendChild(this._gen_fa_keypair_elem('search', this._gen_searcher(song.tid, this.translator.t('all_variants'))));
infor.appendChild(this._gen_fa_keypair_elem('headphones', player));
infor.appendChild(this._gen_fa_keypair_elem('download', this._gen_a_href(song.url_reference, this.translator.t('download'))));
if (this._can_add_to_queue(song, queue_full)) {
let elem_play = this._gen_fa_keypair_elem('play', this._gen_enqueuer(song.id, this.translator.t('queue_add')));
elem_play.classList.add((!game_enabled || difficulty_rank != 3) ? 'difficulty-unsuitable' : 'difficulty-suitable');
infor.appendChild(elem_play);
}
this.itembox.appendChild(root)
if (--i < 0) break;
}
}
/**
* @param {Object} queued
* @return {boolean}
*/
_is_owner_of(queued) {
if (this.user?.permissions?.is_admin)
return true;
return this.user?.user?.id == queued.requester_uid && this.user?.user?.platform == queued.requester_platform;
}
async _update_queue_display() {
while (!!this.itembox.firstChild)
this.itembox.removeChild(this.itembox.firstChild)
let i = 600;
for (let song of this.display_songs) {
let root = document.createElement('div');
let thumb = document.createElement('img');
let info = document.createElement('div');
let infol = document.createElement('div');
let infor = document.createElement('div');
thumb.setAttribute('loading', 'lazy');
thumb.setAttribute('src', `/${song.thumbnail}`);
thumb.classList.add('sleeper-link-like');
root.appendChild(thumb);
root.appendChild(info);
info.appendChild(infol);
info.appendChild(infor);
let difficulty = song.difficulty;
let allowed_difficulty = this.config?.difficulties?.[String(this.games.v2n[String(song.gamemode)])] || null;
let difficulty_rank = this._evaluate_difficulty(difficulty, allowed_difficulty);
let game_enabled = this.config?.modules?.[String(this.games.v2n[String(song.gamemode)])] == '1';
let deviance = (difficulty_rank == 1 || difficulty_rank == 2) ? ` (${this._calc_deviance_fmt(difficulty, allowed_difficulty)})` : '';
let elem_difficulty = this._gen_fa_keypair('road', `${song.difficulty.toFixed(2)}${deviance} - ${song.difficulty_name}`);
let elem_game = this._gen_fa_keypair('gamepad', this.translator.t(String(this.games.v2n[String(song.gamemode)])));
if (!game_enabled) elem_game.classList.add('difficulty-disabled');
switch (difficulty_rank) {
case 1: elem_difficulty.classList.add('difficulty-too-easy'); break;
case 2: elem_difficulty.classList.add('difficulty-too-hard'); break;
case 3: elem_difficulty.classList.add('difficulty-okay'); break;
default: elem_difficulty.classList.add('difficulty-disabled');
}
infol.appendChild(this._gen_fa_keypair_sleeper_searcher('music', song.title));
infol.appendChild(this._gen_fa_keypair_sleeper_searcher('user', song.artist));
infol.appendChild(this._gen_fa_keypair_sleeper_searcher('map-o', song.mapper));
infol.appendChild(elem_difficulty);
infol.appendChild(elem_game);
let player = this._gen_player(`/${song.preview}`, this.translator.t('preview'), this.translator.t('stop_preview'));
thumb.onclick = () => { player.click(); };
infol.appendChild(this._gen_fa_keypair_elem('search', this._gen_searcher(song.tid, this.translator.t('all_variants'))));
let queueitem = this.queue_id2obj[song.id];
infor.appendChild(this._gen_fa_keypair('user', `[${queueitem.requester_platform}] ${queueitem.requester_display}`));
infor.appendChild(this._gen_fa_keypair_elem('headphones', player));
if (this.user?.permissions?.is_admin) {
if (!queueitem.played) infor.appendChild(this._gen_fa_keypair_elem('play', this._gen_qplay(song.id, this.translator.t('queue_played'))));
else infor.appendChild(this._gen_fa_keypair_elem('refresh', this._gen_qreplay(song.id, this.translator.t('queue_unplayed'))));
}
if (this.user?.permissions?.is_admin || (this._is_owner_of(queueitem) && !queueitem?.played))
infor.appendChild(this._gen_fa_keypair_elem('times', this._gen_dequeuer(song.id, this.translator.t('queue_remove'))));
if (queueitem.played) root.classList.add('quarter-opacity');
this.itembox.appendChild(root)
if (--i < 0) break;
}
if (!!this.onrerendered)
this.onrerendered();
}
}
class SongQueueDisplay extends SongListDisplayable {
/**
* @param {Object} config
* @param {Object} user
* @param {SongSearchField} ssf
* @param {Object[]} queue
* @param {Object[]} songs
* @param {OnScreenTranslatedNotificationSpawner} notifier
* @param {StringTranslator} translator
* @param {Object} games
* @param {HTMLDivElement} itembox
*/
constructor(config, user, ssf, queue, songs, notifier, translator, games, itembox) {
super(config, user, songs, notifier, translator, games, itembox);
this.queue = queue;
this.display_songs = [];
this.song_id2obj = {};
for (let song of songs)
this.song_id2obj[song.id] = song;
this.queue_id2obj = {};
this.ssf = ssf;
this.last_render = '';
this._update_display_songs();
this._update_queue_display();
}
ui_set(value) { return this.ssf.ui_set(value); }
ui_clear(value) { return this.ssf.ui_clear(value); }
ui_search(value) { return this.ssf.ui_search(value); }
get onchange() { return this.ssf.onchange; }
async go_fetch_changes() {
let url_root = `${window.location.protocol}//${window.location.host}`;
let url_api = `${url_root}/api`;
let url_queue = `${url_api}/queue`;
doReqParsingJson('GET', url_queue).then(q => {
this.queue = q;
this._update_display_songs();
})
}
_update_display_songs() {
this.queue_id2obj = {};
for (let item of this.queue)
this.queue_id2obj[item.id] = item;
this.display_songs = [
...this.queue.filter(x => !x.played).map(x => this.song_id2obj[x.id]),
...this.queue.filter(x => x.played).map(x => this.song_id2obj[x.id]).reverse(),
];
let render = this.display_songs.map(x => `${x.id}${this.queue_id2obj[x.id]?.played}`).join('');
if (render != this.last_render) {
this.last_render = render;
this._update_queue_display();
}
}
}
class SongSearchField extends SongListDisplayable {
/**
* @param {Object} config
* @param {Object} user
* @param {Object[]} songs
* @param {OnScreenTranslatedNotificationSpawner} notifier
* @param {StringTranslator} translator
* @param {Object} games
* @param {HTMLDivElement} itembox
*/
constructor(config, user, songs, notifier, translator, games, itembox) {
super(config, user, songs, notifier, translator, games, itembox);
this.queue_mgr = null;
this.ui_set = async (value) => { };
this.search_tokens = new Set('');
this.onchange = [];
this.display_songs = songs;
this.lastSkip = setTimeout(() => { }, 100);
this.song_id2obj = {};
this.song_word2id = {};
this.song_sgmt2id = {};
for (let song of songs) {
this.song_id2obj[song.id] = song;
for (let word of this._split_query(`${song.title} ${song.artist} ${song.mapper} ${song.tid} ${song.sid} ${song.id}`)) {
if (!(word in this.song_word2id))
this.song_word2id[word] = [];
this.song_word2id[word].push(song.id);
for (let i = word.length; i >= 3; i--) {
let subword = word.substring(0, i);
if (!(subword in this.song_sgmt2id))
this.song_sgmt2id[subword] = [];
this.song_sgmt2id[subword].push(song.id);
}
}
}
}
get queue() { return this.queue_mgr?.queue || []; }
get queue_id2obj() { return this.queue_mgr?.queue_id2obj || {}; }
/**
* @param {string} value
* @return {Set<string>}
*/
_split_query(value) {
return new Set([...(value || '').toLowerCase().matchAll(/[a-z0-9]+/g)].map(x => x[0]).filter(x => x.length));
}
async ui_clear() {
return await this.ui_set('')
}
async ui_shuffle() {
this.search_tokens = new Set()
await this.ui_clear()
let randomly_sorted = this.songs.filter(x => (this.config?.modules?.[String(this.games.v2n[String(x.gamemode)])] || 'h') != 'h').map(x => [Math.random(), x])
randomly_sorted.sort((a, b) => a[0] - b[0])
this.display_songs = randomly_sorted.map(x => x[1])
await this._update_search_display()
}
async ui_search(value) {
let s = this._split_query(value)
let u = new Set([...s, this.search_tokens])
if (s.size == 0) return;
if (s.size != this.search_tokens.size || u.size != s.size || u.size != this.search_tokens.size) {
this.search_tokens = s
clearTimeout(this.lastSkip);
this.lastSkip = setTimeout(() => { this._rank_search_results(); }, 500);
}
}
async _rank_search_results() {
let id2ctr = {}
for (let search_token of this.search_tokens) {
let wordids = (this.song_word2id[search_token] || []);
let sgmtids = (this.song_sgmt2id[search_token] || []);
for (let wordid of wordids) {
if (!(wordid in id2ctr)) id2ctr[wordid] = 0;
id2ctr[wordid] += 4;
}
for (let sgmtid of sgmtids) {
if (!(sgmtid in id2ctr)) id2ctr[sgmtid] = 0;
id2ctr[sgmtid] += 1;
}
}
let ctrentries = Object.entries(id2ctr);
ctrentries.sort((a, b) => (b[1] - a[1]) != 0 ? (b[1] - a[1]) : (a[0] - b[0]));
this.display_songs = ctrentries.map(x => this.song_id2obj[x[0]]).filter(x => (this.config?.modules?.[String(this.games.v2n[String(x.gamemode)])] || 'h') != 'h');
await this._update_search_display();
}
}
(async function () {
let i18n = new StringTranslator({});
let url_root = `${window.location.protocol}//${window.location.host}`;
let url_api = `${url_root}/api`;
let url_user = `${url_api}/user`;
let url_config = `${url_api}/config`;
let url_songs = `${url_api}/songs`;
let url_queue = `${url_api}/queue`;
let url_games = `${url_api}/games`;
let ale_user = new AsyncLazyEval(doReqParsingJson("GET", url_user, window.localStorage.token));
let ale_config = new AsyncLazyEval(doReqParsingJson("GET", url_config));
let ale_songs = new AsyncLazyEval(doReqParsingJson("GET", url_songs));
let ale_queue = new AsyncLazyEval(doReqParsingJson("GET", url_queue));
let ale_games = new AsyncLazyEval(doReqParsingJson("GET", url_games));
let ale_translator_ready = new AsyncLazyEval(ale_config.eval.then(v => {
i18n = new StringTranslator(v.localization);
}));
let notifier = new OnScreenTranslatedNotificationSpawner('aside#notificationArea', () => i18n, ale_translator_ready);
let el_notification = document.createElement('aside');
el_notification.setAttribute('id', 'notificationArea');
document.body.appendChild(el_notification);
let el_header = document.createElement('header');
let el_header_container = document.createElement('div');
el_header_container.classList.add('container');
el_header_container.classList.add('dflex');
el_header_container.classList.add('jcsb');
let el_header_title = document.createElement('h1');
el_header_title.innerText = 'LiveQ';
let el_header_login = document.createElement('a');
el_header_login.href = `${url_api}/login/twitch`;
el_header_login.innerText = '...';
el_header_login.classList.add('self-align-right');
el_header_login.classList.add('btn');
el_header_login.classList.add('btn-gray');
el_header_container.appendChild(el_header_title);
el_header_container.appendChild(el_header_login);
el_header.appendChild(el_header_container);
document.body.appendChild(el_header);
let el_content = document.createElement('main');
let el_songs = document.createElement('section');
let el_search = document.createElement('section');
el_songs.setAttribute('id', 'songs');
el_search.setAttribute('id', 'search');
let el_songs_h2 = document.createElement('h2');
let el_songs_d = document.createElement('div');
el_songs_h2.innerText = 'Queue'
el_songs.appendChild(el_songs_h2);
el_songs.appendChild(el_songs_d);
let el_search_d = document.createElement('div');
let el_search_b = document.createElement('div');
let el_search_d_r = document.createElement('button');
let el_search_d_i = document.createElement('input');
let el_search_d_s = document.createElement('button');
let el_search_d_r_i = document.createElement('i');
let el_search_d_s_i = document.createElement('i');
el_search_d_i.classList.add('seamless-join-left');
el_search_d_i.classList.add('seamless-join-right');
el_search_d_r.classList.add('seamless-join-right');
el_search_d_r.classList.add('btn');
el_search_d_r.classList.add('btn-gray');
el_search_d_r.classList.add('btn-square');
el_search_d_r.classList.add('refresh');
el_search_d_s.classList.add('seamless-join-left');
el_search_d_s.classList.add('btn');
el_search_d_s.classList.add('btn-success');
el_search_d_s.classList.add('btn-square');
el_search_d_s.classList.add('search');
el_search_d_i.classList.add('btn-height');
el_search_d_r_i.classList.add('fa');
el_search_d_r_i.classList.add('fa-refresh');
el_search_d_s_i.classList.add('fa');
el_search_d_s_i.classList.add('fa-search');
el_search_d_r_i.setAttribute('aria-hidden', 'true');
el_search_d_s_i.setAttribute('aria-hidden', 'true');
el_search_d_i.setAttribute('placeholder', 'Search...');
el_search_d_r.setAttribute('title', 'Shuffle');
el_search_d_s.setAttribute('title', 'Search');
el_search_d_r.appendChild(el_search_d_r_i);
el_search_d_s.appendChild(el_search_d_s_i);
el_search_d.appendChild(el_search_d_r);
el_search_d.appendChild(el_search_d_i);
el_search_d.appendChild(el_search_d_s);
el_search.appendChild(el_search_d);
el_search.appendChild(el_search_b);
el_content.appendChild(el_songs);
el_content.appendChild(el_search);
document.body.appendChild(el_content);
ale_user.eval.then(v => {
el_header_login.href = `${url_api}/static/logout.html`
el_header_login.classList.remove('btn-gray');
el_header_login.classList.add('btn-danger');
ale_translator_ready.eval.finally(() => {
el_header_login.innerText = `${i18n.t('logout_username')} ${v?.user?.display}`;
el_header_login.onclick = (ev) => {
ev.preventDefault();
window.localStorage.removeItem('token');
window.location.reload();
}
});
}).catch(v => {
el_header_login.classList.remove('btn-gray');
el_header_login.classList.add('btn-purple');
ale_translator_ready.eval.finally(() => {
el_header_login.innerText = i18n.t('login_with_twitch');
});
});
ale_config.eval.then(cfg => {
el_header_title.innerText = cfg.app.title;
document.title = cfg.app.title;
})
ale_translator_ready.eval.finally(() => {
el_search_d_r.setAttribute('title', i18n.t('shuffle'));
el_search_d_s.setAttribute('title', i18n.t('search'));
el_search_d_i.setAttribute('placeholder', `${i18n.t('search')}...`);
el_songs_h2.innerText = i18n.t('playqueue')
})
let user_nothrow = new Promise((resolve) => ale_user.eval.then(resolve).catch(() => resolve(null)));
user_nothrow.then(async user => {
ale_config.eval.then(async config => {
ale_games.eval.then(async games => {
ale_queue.eval.then(async queue => {
ale_songs.eval.then(async songs => {
let ssf = new SongSearchField(config, user, songs, notifier, i18n, games, el_search_b);
let sqd = new SongQueueDisplay(config, user, ssf, queue, songs, notifier, i18n, games, el_songs_d);
ssf.queue_mgr = sqd;
sqd.onrerendered = () => { ssf._update_search_display(); }
el_search_d_r.onclick = () => ssf.ui_shuffle();
el_search_d_s.onclick = () => ssf.ui_search(el_search_d_i.value);
el_search_d_i.onchange = () => ssf.ui_search(el_search_d_i.value);
el_search_d_i.onkeyup = () => ssf.ui_search(el_search_d_i.value);
el_search_d_i.onkeydown = () => ssf.ui_search(el_search_d_i.value);
el_search_d_i.onkeypress = () => ssf.ui_search(el_search_d_i.value);
ssf.ui_set = async (value) => { el_search_d_i.value = value };
ssf.onchange.push(() => sqd.go_fetch_changes())
ssf.ui_shuffle()
while (true) {
await asyncSleep(15000);
sqd.go_fetch_changes();
}
}).catch(() => {
window.location.reload();
})
}).catch(() => {
window.location.reload();
})
}).catch(() => {
window.location.reload();
})
}).catch(() => {
window.location.reload();
})
}).catch(() => {
window.location.reload();
})
})();
</script>
</body>
</html>