19 Commits

Author SHA1 Message Date
82583d21d8 fix: Poetry is picky and needs the module to match the folder 2024-02-21 15:33:39 +01:00
eb1581e41e fix: fix typo in maintainer information 2024-02-21 15:28:38 +01:00
ef250c72a6 Merge branch 'marietje-zuid' into 'feature/marietje-4.1-in-header'
# Conflicts:
#   marietje/queues/templates/queues/queue.html
2024-02-21 15:19:07 +01:00
6110cd6665 fix(songs): change new Vue to createApp 2024-02-21 13:54:41 +01:00
52b24e6b36 fix(queues): check response success besides status 2024-02-21 13:52:45 +01:00
2dd4dd3381 Merge branch 'fix/request-table' into 'marietje-zuid'
Other container layout and responsive queue container

Closes #47

See merge request technicie/MarietjeDjango!67
2024-02-21 12:28:25 +01:00
219af8fa1d Merge branch 'wkuijltjes/aesthetic-changes' into 'marietje-zuid'
Some aesthetic changes to the dark mode colors and the table borders

See merge request technicie/MarietjeDjango!81
2023-11-25 07:45:46 +01:00
98e43aa688 Some aesthetic changes to the dark mode colors and the table borders 2023-11-25 07:45:46 +01:00
831f479eec Merge branch 'wkuijltjes/minor-edits-stats-pages' into 'marietje-zuid'
Slightly improve stats pages

Closes #69

See merge request technicie/MarietjeDjango!80
2023-11-25 07:45:01 +01:00
6a9c22b7f8 Slightly improve stats pages 2023-11-25 07:45:01 +01:00
2d36ace60f Merge branch 'wkuijltjes/reduce-control-column-size' into 'marietje-zuid'
Reduce the width of buttons in the control column and recombine them into one row

Closes #68

See merge request technicie/MarietjeDjango!79
2023-11-25 07:44:45 +01:00
fbafcf1b06 Reduce the width of buttons in the control column and recombine them into one row 2023-11-25 07:44:45 +01:00
e738dc8ab5 Merge master 2023-10-25 17:35:45 +02:00
0e6eaa6076 Merge main 2023-10-14 09:37:37 +02:00
d79e3425f4 New song library 2023-10-12 17:25:26 +02:00
e280fd567d Merge branch 'marietje-zuid' into feature/marietje-4.1-in-header 2023-10-11 17:35:44 +02:00
1f831a6dab Fixed space 2023-10-11 17:32:12 +02:00
8a926f3924 Other container layout and responsive queue container 2023-10-04 20:31:49 +02:00
b2429f941b Marietje in header and fix load times for upload page 2023-10-04 20:00:18 +02:00
10 changed files with 274 additions and 195 deletions

View File

@ -31,19 +31,6 @@ a {
color: var(--text-color); color: var(--text-color);
} }
.table {
color: var(--text-color);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--background-shade);
color: var(--text-color);
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
color: var(--text-color);
}
input[type="text"], input[type="password"] { input[type="text"], input[type="password"] {
background-color: var(--background-shade-light); background-color: var(--background-shade-light);
border: 1px solid var(--background-shade); border: 1px solid var(--background-shade);
@ -82,6 +69,19 @@ footer {
text-align: center; text-align: center;
} }
.table {
color: var(--text-color);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--background-shade);
color: var(--text-color);
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
color: var(--text-color);
}
.marietjequeue { .marietjequeue {
color: #777777; color: #777777;
} }
@ -91,7 +91,24 @@ footer {
} }
.marietjequeue-pre-start td { .marietjequeue-pre-start td {
border-bottom: 3px double #777777; border-bottom: 3px double var(--text-color);
}
.marietjequeue-post-start td {
border-top: 3px double var(--text-color);
}
tr.requested_song{
border-left: 1px solid var(--text-color);
}
.currentsong {
border-bottom: 1px solid var(--text-color);
font-weight: bold;
}
.underline_cell {
border-bottom: 1px solid var(--text-color);
} }
.block-button { .block-button {
@ -100,13 +117,10 @@ footer {
transition: 1s transform ease-in-out; transition: 1s transform ease-in-out;
} }
.currentsong {
border-bottom: 1px solid #DDDDDD;
}
.navbar-text { .navbar-text {
color: var(--text-color); color: var(--text-color);
} }
.danger { .danger {
color: red !important; color: red !important;
} }

View File

@ -20,17 +20,19 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background-color: #202020; --background-color: #181818;
--background-shade: #404040; --background-shade: #282828;
--background-shade-light: #696969; --background-shade-light: #404040;
--card-background: #696969; --card-background: #404040;
--card-background-shade: #404040; --card-background-shade: #282828;
--card-background-contrast: #ffffff; --card-background-contrast: #dddddd;
--title-color: #000000; --title-color: #000000;
--sub-title-color: #dddddd; --sub-title-color: #dddddd;
--link-color: #007bff; --link-color: #007bff;
--text-color: #ffffff; --text-color: #dddddd;
--bs-border-color: #282828;
} }
} }

View File

@ -15,7 +15,10 @@
<link href="{% static 'fontawesomefree/css/all.min.css' %}" rel="stylesheet" type="text/css"> <link href="{% static 'fontawesomefree/css/all.min.css' %}" rel="stylesheet" type="text/css">
<!-- Vue JS --> <!-- Vue JS -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
</script>
<!-- TaTa.js notifications --> <!-- TaTa.js notifications -->
<script src="{% static 'marietje/js/tata.js' %}"></script> <script src="{% static 'marietje/js/tata.js' %}"></script>
@ -37,6 +40,7 @@
</section> </section>
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
<div class="container"> <div class="container">
<a class="navbar-brand d-block d-lg-none" href="{% url "index" %}">Marietje 4.1</a>
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="offcanvas" <button class="navbar-toggler ms-auto" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasNavbar" aria-controls="offcanvasNavbar"> data-bs-target="#offcanvasNavbar" aria-controls="offcanvasNavbar">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<nav class="navbar navbar-expand navbar-default navbar-light border-bottom"> <nav class="navbar navbar-expand navbar-default navbar-light border-bottom">
<div class="container"> <div class="container-lg">
<ul class="nav nav-pills" role="tablist"> <ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="queue-tab" data-bs-toggle="tab" data-bs-target="#queue" <button class="nav-link active" id="queue-tab" data-bs-toggle="tab" data-bs-target="#queue"
@ -39,30 +39,32 @@
</li> </li>
</ul> </ul>
<ul v-if="'start_personal_queue' in infobar && infobar.start_personal_queue !== null" id="personal-queue-container" class="navbar-nav navbar-right hidden-xs"> <ul id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<li v-if="infobar.start_personal_queue != 0" class="nav-item me-3"> <template v-if="infobar !== null && 'start_personal_queue' in infobar && infobar.start_personal_queue !== null">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs"> <li v-if="infobar.start_personal_queue !== 0" class="nav-item me-3">
First song starts in <% infobar.start_personal_queue.secondsToMMSS() %> <p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
</p> First song starts in ${ infobar.start_personal_queue.secondsToMMSS() }$
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs"> </p>
First song starts at <% (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() %> <p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
</p> First song starts at ${ (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() }$
</li> </p>
<li class="nav-item me-3"> </li>
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs"> <li class="nav-item me-3">
Last song ends in <% infobar.end_personal_queue.secondsToMMSS() %> <p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
</p> Last song ends in ${ infobar.end_personal_queue.secondsToMMSS() }$
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs"> </p>
Last song ends at <% (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() %> <p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
</p> Last song ends at ${ (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() }$
</li> </p>
<li class="nav-item"> </li>
<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 class="nav-item">
</li> <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> </ul>
</div> </div>
</nav> </nav>
<div class="container"> <div class="container-lg">
<br><br> <br><br>
<div class="alert-location"> <div class="alert-location">
</div> </div>
@ -71,7 +73,7 @@
<div id="queue-container"> <div id="queue-container">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr class="table-header-style"> <tr class="table-header-style underline_cell">
<td class="col-md-4">Artist</td> <td class="col-md-4">Artist</td>
<td class="col-md-4">Title</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-2 d-sm-table-cell d-none">Requested By</td>
@ -84,40 +86,41 @@
</thead> </thead>
<tbody class="queuebody"> <tbody class="queuebody">
<template v-for="(song, index) in queue"> <template v-for="(song, index) in queue">
<tr :class="{ marietjequeue: (song.user === null), currentsong: (index === 0), 'fw-bold': (index === 0) }"> <tr :class="{ marietjequeue: (song.user === null),
<td class="artist"><% song.song.artist %></td> underline_cell: (index === queue[-1]),
<td class="title"><% song.song.title %></td> currentsong: (index === 0),}">
<td class="artist">${ song.song.artist }$</td>
<td class="title">${ song.song.title }$</td>
<td class="d-sm-table-cell d-none requested-by"> <td class="d-sm-table-cell d-none requested-by">
<template v-if="song.user === null"> <template v-if="song.user === null">
Marietje Marietje
</template> </template>
<template v-else> <template v-else>
<% song.user.name %> ${ song.user.name }$
</template> </template>
</td> </td>
<td class="d-sm-table-cell d-none plays-at" style="text-align: right"> <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"> <template v-if="song.time_until_song_seconds !== null && song.time_until_song_seconds > 0 && playsIn === true">
<% song.time_until_song_seconds.secondsToMMSS() %> ${ song.time_until_song_seconds.secondsToMMSS() }$
</template> </template>
<template v-else-if="playsIn === false && song.plays_at !== null && song.played === false"> <template v-else-if="playsIn === false && song.plays_at !== null && song.played === false">
<% song.plays_at.timestampToHHMMSS() %> ${ song.plays_at.timestampToHHMMSS() }$
</template> </template>
</td> </td>
<td> <td>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div class="d-flex flex-row"> <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"><i <button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)"
class="fa-solid fa-arrow-up"></i></button> 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"><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"><i <button v-if="song.can_move_down" v-on:click="move_down(song.id)"
class="fa-solid fa-arrow-down"></i></button> 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"><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>
</div>
<div class="d-flex flex-row"> <button v-if="song.can_delete" v-on:click="cancel_song(song.id)"
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)" class="btn btn-link"><i class="btn btn-link p-1 p-md-2"><i class="fa-solid fa-trash-can"></i></button>
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>
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-trash-can"></i></button>
</div> </div>
</div> </div>
</td> </td>
@ -128,7 +131,7 @@
</div> </div>
</div> </div>
<div class="tab-pane fade" id="request" role="tabpanel" aria-labelledby="request-tab"> <div class="tab-pane fade" id="request" role="tabpanel" aria-labelledby="request-tab">
<div id="request-container"> <div id="request-container" class="table-responsive">
<table id="request-table" class="table table-striped"> <table id="request-table" class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -176,7 +179,7 @@
</select> </select>
<select class="pagenum input-mini" title="Select page number" v-model="page_number"> <select class="pagenum input-mini" title="Select page number" v-model="page_number">
<template v-for="(i, index) in number_of_pages"> <template v-for="(i, index) in number_of_pages">
<option :value="i"><% i %></option> <option :value="i">${ i }$</option>
</template> </template>
</select> </select>
</th> </th>
@ -187,21 +190,21 @@
<template v-for="(song, index) in songs"> <template v-for="(song, index) in songs">
<tr> <tr>
<td> <td>
<% song.artist %> ${ song.artist }$
</td> </td>
<td> <td>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none"><% song.title %></button> <button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none">${ song.title }$</button>
</td> </td>
<td> <td>
<template v-if="song.user === null"> <template v-if="song.user === null">
Marietje Marietje
</template> </template>
<template v-else> <template v-else>
<% song.user.name %> ${ song.user.name }$
</template> </template>
</td> </td>
<td> <td>
<% song.duration.secondsToMMSS() %> ${ song.duration.secondsToMMSS() }$
</td> </td>
<td> <td>
<button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none"> <button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none">
@ -225,18 +228,34 @@
const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }}; const CAN_MOVE = {{ perms.queues.can_move|yesno:"1,0" }};
</script> </script>
<script> <script>
const queue_vue = new Vue({ const personal_queue_vue = createApp({
el: '#queue-container', delimiters: ['${', '}$'],
delimiters: ['<%', '%>'], data() {
data: { return {
current_song: null, infobar: null,
queue: [], }
user_data: null, },
refreshing: true, }).mount('#personal-queue-container');
refreshTimer: null, const queue_vue = createApp({
clockInterval: null, delimiters: ['${', '}$'],
started_at: null, data() {
playsIn: true, return {
current_song: null,
queue: [],
user_data: null,
refreshing: true,
refreshTimer: null,
clockInterval: null,
started_at: null,
playsIn: true,
}
},
watch: {
playsIn: {
handler(val, oldVal) {
this.update_infobar();
}
},
}, },
mounted() { mounted() {
this.clockInterval = setInterval(this.update_song_times, 1000); this.clockInterval = setInterval(this.update_song_times, 1000);
@ -302,16 +321,14 @@
plays_in: this.playsIn, plays_in: this.playsIn,
now_in_seconds: 0, now_in_seconds: 0,
} }
const now_in_seconds = Math.round((new Date()).getTime() / 1000); infoBar.now_in_seconds = Math.round((new Date()).getTime() / 1000);
infoBar.now_in_seconds = now_in_seconds;
let current_song_played = now_in_seconds - this.queue[0].started_at;
// If the current song is the current user's, their queue has started. // If the current song is the current user's, their queue has started.
if (this.queue[0].user.id == this.user_data.id) { if (this.queue[0].user.id === this.user_data.id) {
infoBar.start_personal_queue = 0; infoBar.start_personal_queue = 0;
} }
for (let i = 0; i < this.queue.length; i++) { for (let i = 0; i < this.queue.length; i++) {
const current_song = this.queue[i]; const current_song = this.queue[i];
if (i == 0) { if (i === 0) {
const current_song_remaining_seconds = current_song.song.duration - this.queue[1].time_until_song_seconds; 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_personal_queue'] -= current_song_remaining_seconds;
infoBar['length_total_queue'] -= current_song_remaining_seconds; infoBar['length_total_queue'] -= current_song_remaining_seconds;
@ -321,11 +338,11 @@
infoBar['length_personal_queue'] += current_song.song.duration; infoBar['length_personal_queue'] += current_song.song.duration;
infoBar['end_personal_queue'] = infoBar['length_total_queue']; infoBar['end_personal_queue'] = infoBar['length_total_queue'];
if (infoBar['start_personal_queue'] === null) { 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 infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds;
} }
} }
} }
this.$emit("infobar", infoBar); personal_queue_vue.infobar = infoBar;
}, },
refresh() { refresh() {
if (!this.refreshing) { if (!this.refreshing) {
@ -411,29 +428,20 @@
}); });
}, },
} }
}); }).mount("#queue-container");
const personal_queue_vue = new Vue({
el: '#personal-queue-container',
delimiters: ['<%', '%>'],
data: {
infobar: [],
},
mounted() {
queue_vue.$on("infobar", infoBar => this.infobar = infoBar);
}
});
</script> </script>
<script> <script>
const request_vue = new Vue({ const request_vue = createApp({
el: '#request-container', delimiters: ['${', '}$'],
delimiters: ['<%', '%>'], data() {
data: { return {
songs: [], songs: [],
total_songs: 0, total_songs: 0,
search_input: "", search_input: "",
typing_timer: null, typing_timer: null,
page_size: 10, page_size: 10,
page_number: 1, page_number: 1,
}
}, },
watch: { watch: {
search_input: { search_input: {
@ -537,7 +545,8 @@
"Content-Type": 'application/json', "Content-Type": 'application/json',
}, },
}).then(response => { }).then(response => {
if (response.status === 200) { // TODO: Communicate failure through HTTP error codes (403) instead of checking response.success.
if (response.status === 200 && response.success) {
return response.json(); return response.json();
} else { } else {
throw response; throw response;
@ -596,7 +605,7 @@
this.page_number = page_number; this.page_number = page_number;
} }
} }
}); }).mount('#request-container');
</script> </script>
<script> <script>
function volume_down() { function volume_down() {

View File

@ -86,7 +86,7 @@ class SongUploadAPIView(APIView):
song = upload_file(file, artist, title, request.user) song = upload_file(file, artist, title, request.user)
upload_counter.inc() upload_counter.inc()
return Response(status=200, data=self.serializer_class(song).data) return Response(status=200, data=self.serializer_class(song).data)
except UploadException: except (UploadException, ConnectionRefusedError):
return Response( return Response(
status=500, status=500,
data={ data={

View File

@ -4,7 +4,7 @@
{% block title %}Manage{% endblock %} {% block title %}Manage{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container-lg">
<div class="table-responsive mt-5"> <div class="table-responsive mt-5">
<table id="request-table" class="table table-striped"> <table id="request-table" class="table table-striped">
<thead> <thead>
@ -41,7 +41,7 @@
</select> </select>
<select class="pagenum input-mini" title="Select page number" v-model="page_number"> <select class="pagenum input-mini" title="Select page number" v-model="page_number">
<template v-for="(i, index) in number_of_pages"> <template v-for="(i, index) in number_of_pages">
<option :value="i"><% i %></option> <option :value="i">${ i }$</option>
</template> </template>
</select> </select>
</th> </th>
@ -52,10 +52,10 @@
<template v-for="(song, index) in songs"> <template v-for="(song, index) in songs">
<tr> <tr>
<td> <td>
<% song.artist %> ${ song.artist }$
</td> </td>
<td> <td>
<a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);"><% song.title %></a> <a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);">${ song.title }$</a>
</td> </td>
</tr> </tr>
</template> </template>
@ -64,17 +64,18 @@
</div> </div>
</div> </div>
<script> <script>
let manage_vue = new Vue({ let manage_vue = createApp({
el: '#request-table', delimiters: ['${', '}$'],
delimiters: ['<%', '%>'], data() {
data: { return {
songs: [], songs: [],
total_songs: 0, total_songs: 0,
search_input: "", search_input: "",
typing_timer: null, typing_timer: null,
page_size: 10, page_size: 10,
page_number: 1, page_number: 1,
user_data: null, user_data: null,
}
}, },
watch: { watch: {
search_input: { search_input: {
@ -167,6 +168,6 @@
this.page_number = page_number; this.page_number = page_number;
} }
} }
}); }).mount('#request-table');
</script> </script>
{% endblock %} {% endblock %}

View File

@ -16,9 +16,12 @@
{% csrf_token %} {% csrf_token %}
<div class="fileupload fileupload-new" data-provides="fileupload"> <div class="fileupload fileupload-new" data-provides="fileupload">
<span class="btn btn-primary btn-file"> <span class="btn btn-primary btn-file">
<span v-if="fileObjects.length === 0"> <span v-if="fileObjects.length === 0 && !files_loading">
Select files Select files
</span> </span>
<span v-else-if="files_loading">
Loading new files...
</span>
<span v-else> <span v-else>
Change Change
</span> </span>
@ -29,26 +32,35 @@
<div class="songs"> <div class="songs">
<div v-for="fileObject in fileObjects" class="song-container card mb-3"> <div v-for="fileObject in fileObjects" class="song-container card mb-3">
<div class="card-header"> <div class="card-header">
<h3><% fileObject.name %></h3> <h3>${ fileObject.name }$</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group mb-3"> <div class="form-group mb-3">
<div v-if="fileObject.artist === '' || fileObject.artist === null" class="alert alert-danger">Please enter an artist for this song.</div> <div v-if="fileObject.artist === '' || fileObject.artist === null"
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]" class="form-control input-sm artist" disabled class="alert alert-danger">Please enter an artist for this song.
</div>
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]"
class="form-control input-sm artist" disabled
placeholder="Artist" v-model="fileObject.artist"/> placeholder="Artist" v-model="fileObject.artist"/>
<input v-else type="text" name="artist[]" class="form-control input-sm artist" <input v-else type="text" name="artist[]"
class="form-control input-sm artist"
placeholder="Artist" v-model="fileObject.artist"/> placeholder="Artist" v-model="fileObject.artist"/>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<div v-if="fileObject.title === '' || fileObject.title === null" class="alert alert-danger">Please enter a title for this song.</div> <div v-if="fileObject.title === '' || fileObject.title === null"
<input v-if="upload_in_progress || uploaded" type="text" name="title[]" class="form-control input-sm title" disabled class="alert alert-danger">Please enter a title for this song.
</div>
<input v-if="upload_in_progress || uploaded" type="text" name="title[]"
class="form-control input-sm title" disabled
placeholder="Title" v-model="fileObject.title"/> placeholder="Title" v-model="fileObject.title"/>
<input v-else type="text" name="title[]" class="form-control input-sm title" <input v-else type="text" name="title[]" class="form-control input-sm title"
placeholder="Title" v-model="fileObject.title"/> placeholder="Title" v-model="fileObject.title"/>
</div> </div>
<template v-if="fileObject.upload_finished === true"> <template v-if="fileObject.upload_finished === true">
<div v-if="fileObject.success === true" class="alert alert-success">Upload finished successfully.</div> <div v-if="fileObject.success === true" class="alert alert-success">Upload
<div v-else class="alert alert-danger"><% fileObject.error_message %></div> finished successfully.
</div>
<div v-else class="alert alert-danger">${ fileObject.error_message }$</div>
</template> </template>
</div> </div>
</div> </div>
@ -56,14 +68,20 @@
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="progress mt-2 mb-3"> <div class="progress mt-2 mb-3">
<div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }" class="progress-bar progress-bar-striped" role="progressbar" :style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div> <div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }"
class="progress-bar progress-bar-striped" role="progressbar"
:style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0"
aria-valuemax="100"></div>
</div> </div>
<template v-if="upload_in_progress || uploaded"> <template v-if="upload_in_progress || uploaded">
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">Clear</button> <button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">
Clear
</button>
<button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button> <button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button>
</template> </template>
<template v-else> <template v-else>
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100" type="submit" value="Upload" v-on:click="upload"/> <input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100"
type="submit" value="Upload" v-on:click="upload"/>
<button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button> <button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button>
</template> </template>
</div> </div>
@ -73,20 +91,21 @@
</div> </div>
</div> </div>
<link rel="stylesheet" href="{% static 'songs/css/upload.css' %}"/> <link rel="stylesheet" href="{% static 'songs/css/upload.css' %}"/>
<script type="module"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
import * as id3 from '//unpkg.com/id3js@^2/lib/id3.js'; <script>
let upload_vue = createApp({
let upload_vue = new Vue({ delimiters: ['${', '}$'],
el: '#uploadform', data() {
delimiters: ['<%', '%>'], return {
data: { files: [],
files: [], fileObjects: [],
fileObjects: [], uploaded: false,
uploaded: false, upload_in_progress: false,
upload_in_progress: false, files_loading: false,
}
}, },
computed: { computed: {
ready_for_upload: function() { ready_for_upload: function () {
if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) { if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) {
return false; return false;
} else { } else {
@ -98,14 +117,14 @@
return true; return true;
} }
}, },
everything_successfully_uploaded: function() { everything_successfully_uploaded: function () {
return this.fileObjects.map((fileObject) => { return this.fileObjects.map((fileObject) => {
return fileObject.upload_finished === true && fileObject.success === true; return fileObject.upload_finished === true && fileObject.success === true;
}).reduce((previousValue, currentValue) => { }).reduce((previousValue, currentValue) => {
return previousValue && currentValue; return previousValue && currentValue;
}, true); }, true);
}, },
progress_bar_width: function() { progress_bar_width: function () {
if (this.fileObjects.length === 0) { if (this.fileObjects.length === 0) {
return 0; return 0;
} }
@ -158,14 +177,20 @@
}).then(() => { }).then(() => {
this.fileObjects[i].success = true; this.fileObjects[i].success = true;
}).catch(e => { }).catch(e => {
console.log(e);
if (e instanceof Response) { if (e instanceof Response) {
e.json().then(data => { try {
this.fileObjects.error_message = data.errorMessage; e.json().then(data => {
this.fileObjects.success = false; this.fileObjects[i].error_message = data.errorMessage;
}); this.fileObjects[i].success = false;
});
} catch {
this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects[i].success = false;
}
} else { } else {
this.fileObjects.error_message = "An exception occurred while uploading this file, please try again."; this.fileObjects[i].error_message = "An exception occurred while uploading this file, please try again.";
this.fileObjects.success = false; this.fileObjects[i].success = false;
} }
}).finally(() => { }).finally(() => {
this.fileObjects[i].upload_finished = true; this.fileObjects[i].upload_finished = true;
@ -177,23 +202,25 @@
}); });
}, },
async set_new_files(event) { async set_new_files(event) {
this.files_loading = true;
this.uploaded = false;
this.upload_in_progress = false;
this.files = event.target.files; this.files = event.target.files;
let newFileObjects = []; let newFileObjects = [];
for (let i = 0; i < this.files.length; i++) { for (let i = 0; i < this.files.length; i++) {
try { await this.parseSong(this.files[i]).then((song) => {
const tags = await this.parseSong(this.files[i]);
newFileObjects.push( newFileObjects.push(
{ {
"file": this.files[i], "file": this.files[i],
"name": this.files[i].name, "name": this.files[i].name,
"artist": tags.artist, "artist": song.artist,
"title": tags.title, "title": song.title,
"success": null, "success": null,
"error_message": null, "error_message": null,
"upload_finished": false, "upload_finished": false,
} }
); );
} catch { }).catch(() => {
newFileObjects.push( newFileObjects.push(
{ {
"file": this.files[i], "file": this.files[i],
@ -205,14 +232,28 @@
"upload_finished": false, "upload_finished": false,
} }
) )
} });
} }
this.fileObjects = newFileObjects; this.fileObjects = newFileObjects;
this.files_loading = false;
}, },
parseSong(file) { async parseSong(file) {
return id3.fromFile(file); let jsMediaTags = window.jsmediatags;
const tags = await new Promise((resolve, reject) => {
jsMediaTags.read(file, {
onSuccess: function (tag) {
resolve(tag);
},
onError: function (error) {
reject(error);
}
});
});
return tags.tags;
} }
} }
}); }).mount('#uploadform');
</script> </script>
{% endblock %} {% endblock %}

View File

@ -28,6 +28,7 @@
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th style="text-align: right;"># Songs</th> <th style="text-align: right;"># Songs</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -54,6 +55,7 @@
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th style="text-align: right;"># Requests</th> <th style="text-align: right;"># Requests</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -109,6 +111,7 @@
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th style="text-align: right;"># Unique</th> <th style="text-align: right;"># Unique</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -176,7 +179,9 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played uploaders</h2> <h2>Most played uploaders</h2>
<p>These are the {{ stats.stats_top_count }} people whose songs are requested most often by other people, as shown in the left column. The right column shows how many times that person has queued his own songs.</p> <p>These are the {{ stats.stats_top_count }} people whose songs are requested most often by other
people, as shown in the left column. The right column shows how many times that person has queued
their own songs.</p>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -202,7 +207,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played songs last 14 days</h2> <h2>Most played songs last 14 days</h2>
<p>These songs are played the {{ stats.stats_top_count }} most in the last two weeks.</p> <p>These {{ stats.stats_top_count }} songs have been requested the most in the last two weeks.</p>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>

View File

@ -21,9 +21,10 @@
{% endif %} {% endif %}
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played songs</h2> <h2>Most played songs</h2>
<p>You have requested <strong> {{ stats.unique_requests }} </strong> different <p>You have requested <strong> {{ stats.unique_requests }} </strong> different songs a total of
songs a total of <strong> {{ stats.total_requests }} </strong> times. This <strong> {{ stats.total_requests }} </strong> times. This means
means <strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests have been unique. </p> <strong> {% widthratio stats.unique_requests stats.total_requests 100 %}% </strong> of your requests
have been unique. These are the song you have requested the most.</p>
<h4>Top {{ stats.stats_top_count }}:</h4> <h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -50,6 +51,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played artists</h2> <h2>Most played artists</h2>
<p>These are the artists you have requested the most.</p>
<h4>Top {{ stats.stats_top_count }}:</h4> <h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -57,7 +59,7 @@
<tr> <tr>
<th>#</th> <th>#</th>
<th>Artist</th> <th>Artist</th>
<th style="text-align: right;"># Requests</th> <th style="white-space:nowrap; text-align: right;"># Requests</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -74,11 +76,11 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Uploads requested</h2> <h2>Uploads requested</h2>
<p> You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column <p>You have uploaded a total of <strong> {{stats.total_uploads }} </strong> songs. The left column
shows how many times these have been requested by other people. The right column shows shows how many times these have been requested by other people. The right column shows how many times
how many times you requested your own songs. In total your songs you requested your own songs. In total your songs have been queued
have been queued <strong> {{stats.total_played_uploads }} </strong> times by others and <strong> {{stats.total_played_uploads }} </strong> times by others and
<strong> {{stats.total_played_user_uploads }} </strong> by yourself. <strong> {{stats.total_played_user_uploads }} </strong> times by yourself.</p>
<h4>Top {{ stats.stats_top_count }}:</h4> <h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -87,8 +89,8 @@
<th>#</th> <th>#</th>
<th>Artist</th> <th>Artist</th>
<th>Title</th> <th>Title</th>
<th style="text-align: right;">Others</th> <th style="white-space:nowrap; text-align: right;"># Others</th>
<th>You</th> <th style="white-space:nowrap;"># You</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -107,8 +109,8 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Upload artists requested</h2> <h2>Upload artists requested</h2>
<p> The left column shows how many times songs from artists uploaded by you have been requested by <p>The left column shows how many times songs from artists uploaded by you have been requested by
other people. The right column shows how many times you requested those songs. other people. The right column shows how many times you requested those songs.</p>
<h4>Top {{ stats.stats_top_count }}:</h4> <h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -116,8 +118,8 @@
<tr> <tr>
<th>#</th> <th>#</th>
<th>Artist</th> <th>Artist</th>
<th style="text-align: right;">Others</th> <th style="white-space:nowrap; text-align: right;"># Others</th>
<th>You</th> <th style="white-space:nowrap;"># You</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -135,14 +137,15 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played uploaders</h2> <h2>Most played uploaders</h2>
<p> The people whose songs you have queued the most are:</p> <p>These are the people whose songs you have requested the most.</p>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>Uploader</th> <th>Uploader</th>
<th style="text-align: right;"># Requests</th> <th style="white-space:nowrap; text-align: right;"># Requests</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -160,14 +163,14 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Biggest fans</h2> <h2>Biggest fans</h2>
<p> The people that queued your songs the most are:</p> <p>These are the people that have requested your songs the most.</p>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th style="text-align: right;"># Requests</th> <th style="white-space:nowrap; text-align: right;"># Requests</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -1,5 +1,5 @@
[tool.poetry] [tool.poetry]
name = "MarietjeDjango" name = "marietje"
version = "4.1.0" version = "4.1.0"
description = "A music player for the south canteen of the Huygens building" description = "A music player for the south canteen of the Huygens building"
authors = [ authors = [
@ -11,7 +11,7 @@ authors = [
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>", "Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
] ]
maintainers = [ maintainers = [
"Kees van Kempen <ru@keesvankempen.nl", "Kees van Kempen <ru@keesvankempen.nl>",
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>", "Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
] ]
readme = "README.md" readme = "README.md"