1243 lines
50 KiB
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> |