Files
MarietjeDjango/marietje/queues/templates/queues/queue.html

752 lines
37 KiB
HTML

{% extends "marietje/base.html" %}
{% load static %}
{% block title %}Queue{% endblock %}
{% block content %}
<nav class="navbar navbar-expand navbar-default navbar-light border-bottom">
<div class="container-lg">
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="queue-tab" data-bs-toggle="tab" data-bs-target="#queue"
type="button" role="tab" aria-controls="queue" aria-selected="true">Queue
</button>
</li>
<li class="nav-item me-3" role="presentation">
<button onclick="request_vue.select_textinput()" class="nav-link" id="request-tab" data-bs-toggle="tab" data-bs-target="#request"
type="button" role="tab" aria-controls="request" aria-selected="false">Request
</button>
</li>
<li id="infobar-buttons" class="nav-item d-flex justify-content-center align-items-center">
{% if perms.queues.can_control_volume %}
<button type="button" id="mute" class="btn nav-btn btn-sm block-button" onclick="mute();">
<i class="fa-solid fa-volume-xmark"></i>
</button>
<button type="button" id="volume_down" class="btn navbar-btn btn-sm block-button"
onclick="volume_down();">
<i class="fa-solid fa-volume-low"></i>
</button>
<button type="button" id="volume_up" class="btn navbar-btn btn-sm block-button"
onclick="volume_up();">
<i class="fa-solid fa-volume-high"></i>
</button>
{% endif %}
{% if perms.queues.can_skip %}
<button type="button" id="skip" class="btn navbar-btn btn-sm block-button" onclick="skip();">
<i class="fa-solid fa-forward-fast"></i>
</button>
{% endif %}
</li>
</ul>
<ul id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<template v-if="infobar !== null && 'start_personal_queue' in infobar && infobar.start_personal_queue !== null">
<li v-if="infobar.start_personal_queue !== 0" class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts in ${ infobar.start_personal_queue.secondsToMMSS() }$
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts at ${ (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() }$
</p>
</li>
<li class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends in ${ infobar.end_personal_queue.secondsToMMSS() }$
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends at ${ (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() }$
</p>
</li>
<li class="nav-item">
<p class="navbar-text mb-0 duration-queue" v-bind:class="{danger: infobar.length_personal_queue > infobar.max_length * 60}">(${ infobar.length_personal_queue.secondsToMMSS() }$)</p>
</li>
</template>
</ul>
</div>
</nav>
<div class="container-lg">
<br><br>
<div class="alert-location">
</div>
<div class="tab-content">
<div class="tab-pane fade show active" id="queue" role="tabpanel" aria-labelledby="queue-tab">
<div id="queue-container">
<table class="table table-striped">
<thead>
<tr class="table-header-style underline_cell">
<td class="col-md-4">Artist</td>
<td class="col-md-4">Title</td>
<td class="col-md-2 d-sm-table-cell d-none">Requested By</td>
<td class="col-md-1 text-info d-sm-table-cell d-none" style="cursor: pointer;">
<span v-if="playsIn" class="btn btn-link p-0" v-on:click="playsIn = false">Plays In</span>
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays At</span>
</td>
<td class="col-md-1">
<span class="control-icons">Control</span>
<span v-if="playsIn" class="btn btn-link p-0 d-sm-none" v-on:click="toggle_details(song)">(Plays in)</span>
<span v-else class="btn btn-link p-0 d-sm-none" v-on:click="toggle_details(song)">(Plays At)</span>
</td>
</tr>
</thead>
<tbody class="queuebody">
<template v-for="(song, index) in queue">
<tr :class="{ marietjequeue: (song.user === null),
underline_cell: (index === queue[-1]),
currentsong: (index === 0),
ownsong: (this.user_data.id === song.user?.id && index !== 0),
}"
v-on:click="toggle_details(song)">
<td>
<span class="artist">${ song.song.artist }$</span>
<span v-if="show_details(song)" class="requested-by d-sm-none d-block small mt-3 fw-normal">
Requested by:<br>
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
${ song.user.name }$
</template>
</span>
</td>
<td>
<span class="title">${ song.song.title }$</span>
<span v-if="show_details(song) && song.time_until_song_seconds > 0" class="plays-at d-sm-none d-block small mt-3 fw-normal" style="text-align: right">
<span v-if="playsIn">Plays In:</span>
<span v-else>Plays At:</span>
<br>
<template v-if="song.time_until_song_seconds !== null && song.time_until_song_seconds > 0 && playsIn === true">
${ song.time_until_song_seconds.secondsToMMSS() }$
</template>
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
${ song.plays_at.timestampToHHMMSS() }$
</template>
</span>
</td>
<td class="d-sm-table-cell d-none requested-by">
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
${ song.user.name }$
</template>
</td>
<td class="d-sm-table-cell d-none plays-at" style="text-align: right">
<template v-if="song.time_until_song_seconds !== null && song.time_until_song_seconds > 0 && playsIn === true">
${ song.time_until_song_seconds.secondsToMMSS() }$
</template>
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
${ song.plays_at.timestampToHHMMSS() }$
</template>
</td>
<td>
<div class="d-flex flex-column">
<div class="d-flex flex-row">
<button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)"
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-arrow-up"></i></button>
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-arrow-up"></i></button>
<button v-if="song.can_move_down" v-on:click="move_down(song.id)"
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-arrow-down"></i></button>
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-arrow-down"></i></button>
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)"
class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
<button v-else class="btn btn-link invisible p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="request" role="tabpanel" aria-labelledby="request-tab">
<div id="request-container" class="table-responsive">
<table id="request-table" class="table table-striped">
<thead>
<tr>
<th>Artist</th>
<th>Title</th>
<th>Uploader</th>
<th>Length</th>
<th>Report</th>
</tr>
<tr>
<th colspan="5"><input id="search-all" class="search-input" type="text" ref="search_textinput"
v-model="search_input"/></th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="3" class="ts-pager form-horizontal">
<button v-if="page_number === 1" type="button" class="btn first disabled"><i
class="fa-solid fa-backward-fast"></i></button>
<button v-else v-on:click="update_page(1);" type="button" class="btn first"><i
class="fa-solid fa-backward-fast"></i></button>
<button v-if="page_number === 1" type="button" class="btn prev disabled"><i
class="fa-solid fa-backward"></i></button>
<button v-else v-on:click="update_page(page_number - 1);" type="button"
class="btn prev"><i class="fa-solid fa-backward"></i></button>
<button v-if="page_number === number_of_pages" type="button" class="btn next disabled">
<i class="fa-solid fa-forward"></i></button>
<button v-else v-on:click="update_page(page_number + 1);" type="button"
class="btn next"><i class="fa-solid fa-forward"></i></button>
<button v-if="page_number === number_of_pages" type="button" class="btn last disabled">
<i class="fa-solid fa-forward-fast"></i></button>
<button v-else v-on:click="update_page(number_of_pages);" type="button"
class="btn last"><i class="fa-solid fa-forward-fast"></i></button>
<select class="pagesize input-mini" title="Select page size" v-model="page_size">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
<template v-for="(i, index) in number_of_pages">
<option :value="i">${ i }$</option>
</template>
</select>
</th>
<th colspan="2"></th>
</tr>
</tfoot>
<tbody>
<template v-for="(song, index) in songs">
<tr>
<td>
${ song.artist }$
</td>
<td>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none" style="text-align: left">${ song.title }$</button>
</td>
<td>
<template v-if="song.user === null">
Marietje
</template>
<template v-else>
${ song.user.name }$
</template>
</td>
<td>
${ song.duration.secondsToMMSS() }$
</td>
<td>
<button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none">
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script type="text/javascript">
const CAN_SKIP = {{ perms.queues.can_skip|yesno:"1,0" }};
const CAN_CANCEL = {{ perms.queues.can_cancel|yesno:"1,0" }};
const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }};
</script>
<script>
const personal_queue_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
infobar: null,
}
},
}).mount('#personal-queue-container');
const queue_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
current_song: null,
queue: [],
user_data: null,
refreshing: true,
refreshTimer: null,
clockInterval: null,
started_at: null,
playsIn: true,
songs_show_details_on_mobile: [],
}
},
watch: {
playsIn: {
handler(val, oldVal) {
setCookie("PLAYS_IN", this.playsIn, 14);
}
},
},
mounted() {
this.clockInterval = setInterval(this.update_song_times, 1000);
const stored_playsIn = getCookie("PLAYS_IN");
this.playsIn = (stored_playsIn !== "false");
},
unmounted() {
clearInterval(this.clockInterval);
},
created() {
fetch('/api/v1/users/me/').then(response => {
if (response.status === 200) {
return response.json();
} else {
throw response;
}
}).then(data => {
this.user_data = data;
}).then(() => {
this.refreshing = false;
this.refresh();
}).catch(() => {
tata.error('', 'User details failed to fetch, please reload this page to try again.');
}).finally(() => {
this.refreshing = false;
});
},
computed: {
play_next_song_at() {
if (this.started_at !== null && this.current_song !== null) {
return this.started_at + this.current_song.song.duration;
}
return null;
}
},
methods: {
update_song_times() {
const now_in_seconds = Math.round((new Date()).getTime() / 1000);
let total_song_length = 0;
for (let i = 0; i < this.queue.length; i++) {
if (this.started_at === null) {
this.queue[i].time_until_song_seconds = null;
this.queue[i].plays_at = null;
this.queue[i].played = false;
} else {
this.queue[i].time_until_song_seconds = total_song_length;
this.queue[i].plays_at = now_in_seconds + total_song_length;
this.queue[i].played = this.queue[i].plays_at <= now_in_seconds;
if (i === 0) {
total_song_length += this.queue[i].song.duration - (now_in_seconds - this.started_at);
} else {
total_song_length += this.queue[i].song.duration;
}
}
}
this.update_infobar();
},
update_infobar() {
let infoBar = {
start_personal_queue: null,
length_personal_queue: 0,
length_total_queue: 0,
end_personal_queue: 0,
max_length: 45,
plays_in: this.playsIn,
now_in_seconds: 0,
}
infoBar.now_in_seconds = Math.round((new Date()).getTime() / 1000);
// If the current song is the current user's, their queue has started.
if (this.queue[0].user?.id === this.user_data.id) {
infoBar.start_personal_queue = 0;
}
for (let i = 0; i < this.queue.length; i++) {
const current_song = this.queue[i];
if (i === 0) {
const current_song_remaining_seconds = current_song.song.duration - this.queue[1].time_until_song_seconds;
infoBar['length_personal_queue'] -= current_song_remaining_seconds;
infoBar['length_total_queue'] -= current_song_remaining_seconds;
}
infoBar['length_total_queue'] += current_song.song.duration;
if (current_song.user !== null && current_song.user.id === this.user_data.id) {
infoBar['length_personal_queue'] += current_song.song.duration;
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
if (infoBar['start_personal_queue'] === null) {
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds;
}
}
}
personal_queue_vue.infobar = infoBar;
},
refresh() {
if (!this.refreshing) {
this.refreshing = true;
clearTimeout(this.refreshTimer);
fetch('/api/v1/queues/current/').then(response => {
if (response.status === 200) {
return response.json();
} else {
throw response;
}
}).then(data => {
this.current_song = data.current_song;
this.started_at = Math.round((new Date(data.started_at).getTime()) / 1000);
let newQueue = data.queue;
newQueue.unshift(this.current_song);
newQueue = this.annotateQueue(newQueue);
clearInterval(this.clockInterval);
this.queue = newQueue;
this.update_song_times();
this.clockInterval = setInterval(this.update_song_times, 1000);
}).finally(() => {
this.refreshing = false;
this.refreshTimer = setTimeout(this.refresh, 10000);
});
}
},
annotateQueue(queue) {
for (let i = 0; i < queue.length; i++) {
const can_delete_previous = i === 0 || i === 1 ? false : queue[i - 1].can_delete;
const previous_requested_by_user = i === 0 || i === 1 ? false : queue[i - 1].user !== null;
const requested_by_marietje = queue[i].user === null;
const next_is_marietje = i < queue.length - 1 && queue[i].user !== null && queue[i + 1].user === null;
queue[i].can_delete = i !== 0 && (CAN_MOVE || (queue[i].user !== null && queue[i].user.id === this.user_data.id));
queue[i].can_move_up = i !== 0 && ((CAN_MOVE && previous_requested_by_user && queue[i].user !== null) ||
(CAN_MOVE && !previous_requested_by_user && queue[i].user === null) ||
(can_delete_previous && !requested_by_marietje && previous_requested_by_user));
queue[i].can_move_down = i !== 0 && ((CAN_MOVE && !next_is_marietje && i < queue.length - 1) ||
(queue[i].can_delete && requested_by_marietje && next_is_marietje && i < queue.length - 1));
queue[i].plays_at = null;
queue[i].time_until_song_seconds = null;
queue[i].played = false;
}
return queue;
},
cancel_song(id) {
fetch(
'/api/v1/queues/playlist-song/' + id + '/cancel/',
{
method: "DELETE",
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}
).then(() => {
tata.success("", "Removed song from the queue.");
}).catch(() => {
tata.error("", "An error occurred while removing the song, please try again.");
}).finally(() => {
this.refresh();
});
},
move_down(id) {
fetch(
'/api/v1/queues/playlist-song/' + id + '/move-down/',
{
method: "PATCH",
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}
).then(() => {
tata.success("", "Song was moved successfully.");
}).catch(() => {
tata.error("", "An error occurred while moving the song, please try again.");
}).finally(() => {
this.refresh();
});
},
show_details(song) {
return this.songs_show_details_on_mobile.includes(song.id);
},
toggle_details(song) {
if (!this.show_details(song)) {
this.songs_show_details_on_mobile.push(song.id);
} else {
// Deze filter is gehaat door Kees, gemaakt door Olaf. Bedankt, Olaf. Duurde wel even.
this.songs_show_details_on_mobile = this.songs_show_details_on_mobile.filter(
value => value !== song.id
);
}
},
}
}).mount("#queue-container");
</script>
<script>
const request_vue = createApp({
delimiters: ['${', '}$'],
data() {
return {
songs: [],
total_songs: 0,
search_input: "",
typing_timer: null,
page_size: 10,
page_number: 1,
}
},
watch: {
search_input: {
handler(val, oldVal) {
clearTimeout(this.typing_timer);
if (this.search !== "") {
this.typing_timer = setTimeout(this.search, 200);
}
}
},
page_number: {
handler(val, oldVal) {
if (this.page_number <= 0) {
this.page_number = 1;
}
if (this.page_number > this.number_of_pages) {
this.page_number = this.number_of_pages;
}
this.search();
}
},
page_size: {
handler(val, oldVal) {
if (this.page_size <= 0) {
this.page_size = 10;
}
this.page_number = 1;
setCookie("REQUEST_PAGE_SIZE", this.page_size, 14);
this.search();
}
}
},
computed: {
number_of_pages: function () {
return Math.ceil(this.total_songs / this.page_size);
}
},
created() {
fetch(
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
).then(response => {
if (response.status === 200) {
return response.json();
} else {
throw response;
}
}).then(data => {
this.songs = data.results;
this.total_songs = data.count;
}).catch((e) => {
if (e instanceof Response) {
e.json().then(data => {
tata.error("", data.errorMessage);
});
} else {
tata.error("", "An unknown error occurred, please try again.")
}
});
const stored_page_size = parseInt(getCookie("REQUEST_PAGE_SIZE"));
if (stored_page_size !== Number.NaN && stored_page_size > 0) {
this.page_size = stored_page_size;
}
},
methods: {
search() {
fetch(
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
{
headers: {
"X-CSRFToken": CSRF_TOKEN,
}
}
).then(response => {
if (response.status === 200) {
return response.json();
} else {
throw response;
}
}).then(data => {
this.songs = data.results;
this.total_songs = data.count;
}).catch((e) => {
if (e instanceof Response) {
e.json().then(data => {
tata.error("", data.errorMessage);
});
} else {
tata.error("", "An unknown error occurred, please try again.")
}
});
},
request_song(song_id) {
fetch('/api/v1/queues/current/request/', {
method: 'POST',
body: JSON.stringify({
song: song_id
}),
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}).then(response => {
if (response.status === 200) {
return response.json();
} else {
throw response;
}
}).then(() => {
tata.success('', 'Song added to the queue.');
queue_vue.refresh();
}).catch(e => {
if (e instanceof Response) {
e.json().then(data => {
tata.error('', data.errorMessage);
})
} else {
tata.error('', "An unknown exception occurred.")
}
});
},
report_song(song_id) {
let message = prompt("What is wrong with the song?");
if (message === null) {
return;
}
if (message === "") {
tata.error('', 'Please enter a message.');
}
fetch('/api/v1/songs/report-notes/', {
method: 'POST',
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
body: JSON.stringify({
song: song_id,
note: message,
}),
}).then(response => {
if (response.status === 201) {
return response.json();
} else {
throw response;
}
}).then(() => {
tata.success("", "Successfully submitted report note.")
}).catch(e => {
if (e instanceof Response) {
e.json().then(data => {
tata.error("", data.errorMessage);
});
} else {
tata.error("", "An unknown error occurred, please try again.")
}
});
},
update_page(page_number) {
this.page_number = page_number;
},
select_textinput() {
this.$refs.search_textinput.select();
},
}
}).mount('#request-container');
</script>
<script>
function volume_down() {
fetch('/api/v1/queues/current/volume-down/', {
method: "POST",
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}).then(response => {
if (response.status !== 200) {
throw response;
}
}).catch((e) => {
if (e instanceof Response) {
tata.error("", e.errorMessage);
} else {
tata.error("", "An unknown error occurred.")
}
});
}
function volume_up() {
fetch('/api/v1/queues/current/volume-up/', {
method: "POST",
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}).then(response => {
if (response.status !== 200) {
throw response;
}
}).catch((e) => {
if (e instanceof Response) {
tata.error("", e.errorMessage);
} else {
tata.error("", "An unknown error occurred.")
}
});
}
function mute() {
fetch('/api/v1/queues/current/mute/', {
method: "POST",
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}).then(response => {
if (response.status !== 200) {
throw response;
}
}).catch((e) => {
if (e instanceof Response) {
tata.error("", e.errorMessage);
} else {
tata.error("", "An unknown error occurred.")
}
});
}
function skip() {
fetch('/api/v1/queues/current/skip/', {
method: "POST",
headers: {
"X-CSRFToken": CSRF_TOKEN,
"Accept": 'application/json',
"Content-Type": 'application/json',
},
}).then(response => {
if (response.status !== 200) {
throw response;
}
}).then(() => {
queue_vue.refresh();
}).catch((e) => {
if (e instanceof Response) {
tata.error("", e.errorMessage);
} else {
tata.error("", "An unknown error occurred.")
}
});
}
</script>
{% endblock %}