Accessibility enhancements: catching a11y errors with WAVE (#180)

* Added alt text for all images

* Added aria labels for most buttons that needed them. Added html language label

* Added remaining button labels

* Added remaining missing labels. Tagged <input> where labels made no sense as "aria-hidden=true"

* Fixed some of the low color contrast issues for Light theme

* Replaced broken ARIA link with a new ARIA label

* Fixed skipped heading levels

* Fixed missing fieldset and one orphaned label (combined the two)

* Changed the other orphaned label to a fieldset legend

* Removed color changes

* Added dynamic ARIA label for main Play/Pause button.
Removed the two FIXMEs in index.html

* Added default content to table header

* Changed input type from text to hidden for hidden inputs

* Added proper alt text for cover art

* Final version of the template row

* Added semantic markup to Page Regions

* Missing alt text + truncated alt text
- Added missing alt text for cover images in library and floating player.
- Added JS to truncate alt text for cover images that was too long.

* Sanitize the code

Co-authored-by: Terry Geng <terry@terriex.com>
This commit is contained in:
Félix Fischer 2020-07-05 05:42:47 +02:00 committed by GitHub
parent fd009eabe8
commit e981782dd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 412 additions and 290 deletions

File diff suppressed because one or more lines are too long

View File

@ -10,9 +10,10 @@ import {
isOverflown, isOverflown,
setProgressBar, setProgressBar,
secondsToStr, secondsToStr,
coverArtString,
} from './util'; } from './util';
$('#uploadSelectFile').on('change', function() { $('#uploadSelectFile').on('change', function () {
// get the file name // get the file name
const fileName = $(this).val().replace('C:\\fakepath\\', ' '); const fileName = $(this).val().replace('C:\\fakepath\\', ' ');
// replace the "Choose a file" label // replace the "Choose a file" label
@ -83,7 +84,7 @@ fastForwardBtn.on('click', () => {
}); });
document.getElementById('clear-playlist-btn').addEventListener('click', () => { document.getElementById('clear-playlist-btn').addEventListener('click', () => {
request('post', {action: 'clear'}); request('post', { action: 'clear' });
}); });
// eslint-disable-next-line guard-for-in // eslint-disable-next-line guard-for-in
@ -100,14 +101,14 @@ function request(_url, _data, refresh = false) {
url: _url, url: _url,
data: _data, data: _data,
statusCode: { statusCode: {
200: function(data) { 200: function (data) {
if (data.ver !== playlist_ver) { if (data.ver !== playlist_ver) {
checkForPlaylistUpdate(); checkForPlaylistUpdate();
} }
updateControls(data.empty, data.play, data.mode, data.volume); updateControls(data.empty, data.play, data.mode, data.volume);
updatePlayerPlayhead(data.playhead); updatePlayerPlayhead(data.playhead);
}, },
403: function() { 403: function () {
location.reload(true); location.reload(true);
}, },
}, },
@ -123,6 +124,7 @@ function addPlaylistItem(item) {
pl_title_element.html(item.title); pl_title_element.html(item.title);
pl_artist_element.html(item.artist); pl_artist_element.html(item.artist);
pl_thumb_element.attr('src', item.thumbnail); pl_thumb_element.attr('src', item.thumbnail);
pl_thumb_element.attr('alt', coverArtString(item.title));
pl_type_element.html(item.type); pl_type_element.html(item.type);
pl_path_element.html(item.path); pl_path_element.html(item.path);
@ -134,13 +136,13 @@ function addPlaylistItem(item) {
tags.empty(); tags.empty();
const tag_edit_copy = pl_tag_edit_element.clone(); const tag_edit_copy = pl_tag_edit_element.clone();
tag_edit_copy.click(function() { tag_edit_copy.click(function () {
addTagModalShow(item.id, item.title, item.tags); addTagModalShow(item.id, item.title, item.tags);
}); });
tag_edit_copy.appendTo(tags); tag_edit_copy.appendTo(tags);
if (item.tags.length > 0) { if (item.tags.length > 0) {
item.tags.forEach(function(tag_tuple) { item.tags.forEach(function (tag_tuple) {
const tag_copy = tag_element.clone(); const tag_copy = tag_element.clone();
tag_copy.html(tag_tuple[0]); tag_copy.html(tag_tuple[0]);
tag_copy.addClass('badge-' + tag_tuple[1]); tag_copy.addClass('badge-' + tag_tuple[1]);
@ -157,7 +159,7 @@ function addPlaylistItem(item) {
function displayPlaylist(data) { function displayPlaylist(data) {
playlist_table.animate({ playlist_table.animate({
opacity: 0, opacity: 0,
}, 200, function() { }, 200, function () {
playlist_loading.hide(); playlist_loading.hide();
$('.playlist-item').remove(); $('.playlist-item').remove();
const items = data.items; const items = data.items;
@ -180,9 +182,9 @@ function displayPlaylist(data) {
} }
items.forEach( items.forEach(
function(item) { function (item) {
addPlaylistItem(item); addPlaylistItem(item);
}, },
); );
if (items.length < length && start_from + items.length < length) { if (items.length < length && start_from + items.length < length) {
@ -221,7 +223,7 @@ function insertExpandPrompt(real_from, real_to, display_from, display_to, total_
expand_copy.addClass('playlist-item'); expand_copy.addClass('playlist-item');
expand_copy.appendTo(playlist_table); expand_copy.appendTo(playlist_table);
expand_copy.click(function() { expand_copy.click(function () {
playlist_range_from = real_from; playlist_range_from = real_from;
playlist_range_to = real_to; playlist_range_to = real_to;
updatePlaylist(); updatePlaylist();
@ -231,7 +233,7 @@ function insertExpandPrompt(real_from, real_to, display_from, display_to, total_
function updatePlaylist() { function updatePlaylist() {
playlist_table.animate({ playlist_table.animate({
opacity: 0, opacity: 0,
}, 200, function() { }, 200, function () {
playlist_empty.addClass('d-none'); playlist_empty.addClass('d-none');
playlist_loading.show(); playlist_loading.show();
playlist_table.find('.playlist-item').css('opacity', 0); playlist_table.find('.playlist-item').css('opacity', 0);
@ -248,7 +250,7 @@ function updatePlaylist() {
data: data, data: data,
statusCode: { statusCode: {
200: displayPlaylist, 200: displayPlaylist,
204: function() { 204: function () {
playlist_loading.hide(); playlist_loading.hide();
playlist_empty.removeClass('d-none'); playlist_empty.removeClass('d-none');
$('.playlist-item').remove(); $('.playlist-item').remove();
@ -266,7 +268,7 @@ function checkForPlaylistUpdate() {
type: 'POST', type: 'POST',
url: 'post', url: 'post',
statusCode: { statusCode: {
200: function(data) { 200: function (data) {
if (data.ver !== playlist_ver) { if (data.ver !== playlist_ver) {
playlist_ver = data.ver; playlist_ver = data.ver;
playlist_range_from = 0; playlist_range_from = 0;
@ -297,18 +299,18 @@ function checkForPlaylistUpdate() {
function bindPlaylistEvent() { function bindPlaylistEvent() {
$('.playlist-item-play').unbind().click( $('.playlist-item-play').unbind().click(
function(e) { function (e) {
request('post', { request('post', {
'play_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1), 'play_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1),
}); });
}, },
); );
$('.playlist-item-trash').unbind().click( $('.playlist-item-trash').unbind().click(
function(e) { function (e) {
request('post', { request('post', {
'delete_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1), 'delete_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1),
}); });
}, },
); );
} }
@ -323,9 +325,15 @@ function updateControls(empty, play, mode, volume) {
if (play) { if (play) {
playing = true; playing = true;
playPauseBtn.find('[data-fa-i2svg]').removeClass('fa-play').addClass('fa-pause'); playPauseBtn.find('[data-fa-i2svg]').removeClass('fa-play').addClass('fa-pause');
// PR #180: Since this button changes behavior dynamically, we change its
// ARIA labels in JS instead of only adding them statically in the HTML
playPauseBtn.attr('aria-label', 'Pause');
} else { } else {
playing = false; playing = false;
playPauseBtn.find('[data-fa-i2svg]').removeClass('fa-pause').addClass('fa-play'); playPauseBtn.find('[data-fa-i2svg]').removeClass('fa-pause').addClass('fa-play');
// PR #180: Since this button changes behavior dynamically, we change its
// ARIA labels in JS instead of only adding them statically in the HTML
playPauseBtn.attr('aria-label', 'Play');
} }
} }
@ -409,7 +417,7 @@ function setFilterType(event, type) {
} }
// Bind Event // Bind Event
$('.filter-tag').click(function(e) { $('.filter-tag').click(function (e) {
const tag = $(e.currentTarget); const tag = $(e.currentTarget);
if (!tag.hasClass('tag-clicked')) { if (!tag.hasClass('tag-clicked')) {
tag.addClass('tag-clicked'); tag.addClass('tag-clicked');
@ -421,10 +429,10 @@ $('.filter-tag').click(function(e) {
updateResults(); updateResults();
}); });
filter_dir.change(function() { filter_dir.change(function () {
updateResults(); updateResults();
}); });
filter_keywords.change(function() { filter_keywords.change(function () {
updateResults(); updateResults();
}); });
@ -432,64 +440,64 @@ const item_template = $('#library-item');
function bindLibraryResultEvent() { function bindLibraryResultEvent() {
$('.library-thumb-col').unbind().hover( $('.library-thumb-col').unbind().hover(
function(e) { function (e) {
$(e.currentTarget).find('.library-thumb-grp').addClass('library-thumb-grp-hover'); $(e.currentTarget).find('.library-thumb-grp').addClass('library-thumb-grp-hover');
}, },
function(e) { function (e) {
$(e.currentTarget).find('.library-thumb-grp').removeClass('library-thumb-grp-hover'); $(e.currentTarget).find('.library-thumb-grp').removeClass('library-thumb-grp-hover');
}, },
); );
$('.library-info-title').unbind().hover( $('.library-info-title').unbind().hover(
function(e) { function (e) {
$(e.currentTarget).parent().find('.library-thumb-grp').addClass('library-thumb-grp-hover'); $(e.currentTarget).parent().find('.library-thumb-grp').addClass('library-thumb-grp-hover');
}, },
function(e) { function (e) {
$(e.currentTarget).parent().find('.library-thumb-grp').removeClass('library-thumb-grp-hover'); $(e.currentTarget).parent().find('.library-thumb-grp').removeClass('library-thumb-grp-hover');
}, },
); );
$('.library-item-play').unbind().click( $('.library-item-play').unbind().click(
function(e) { function (e) {
request('post', { request('post', {
'add_item_at_once': $(e.currentTarget).parent().parent().parent().find('.library-item-id').val(), 'add_item_at_once': $(e.currentTarget).parent().parent().parent().find('.library-item-id').val(),
}); });
}, },
); );
$('.library-item-trash').unbind().click( $('.library-item-trash').unbind().click(
function(e) { function (e) {
request('post', { request('post', {
'delete_item_from_library': $(e.currentTarget).parent().parent().find('.library-item-id').val(), 'delete_item_from_library': $(e.currentTarget).parent().parent().find('.library-item-id').val(),
}); });
updateResults(active_page); updateResults(active_page);
}, },
); );
$('.library-item-download').unbind().click( $('.library-item-download').unbind().click(
function(e) { function (e) {
const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
// window.open('/download?id=' + id); // window.open('/download?id=' + id);
downloadId(id); downloadId(id);
}, },
); );
$('.library-item-add-next').unbind().click( $('.library-item-add-next').unbind().click(
function(e) { function (e) {
const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
request('post', { request('post', {
'add_item_next': id, 'add_item_next': id,
}); });
}, },
); );
$('.library-item-add-bottom').unbind().click( $('.library-item-add-bottom').unbind().click(
function(e) { function (e) {
const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
request('post', { request('post', {
'add_item_bottom': id, 'add_item_bottom': id,
}); });
}, },
); );
} }
@ -510,6 +518,7 @@ function addResultItem(item) {
title_element.html(item.title); title_element.html(item.title);
artist_element.html(item.artist ? ('- ' + item.artist) : ''); artist_element.html(item.artist ? ('- ' + item.artist) : '');
thumb_element.attr('src', item.thumb); thumb_element.attr('src', item.thumb);
thumb_element.attr('alt', coverArtString(item.title));
type_element.html('[' + item.type + ']'); type_element.html('[' + item.type + ']');
path_element.html(item.path); path_element.html(item.path);
@ -520,13 +529,13 @@ function addResultItem(item) {
tags.empty(); tags.empty();
const tag_edit_copy = tag_edit_element.clone(); const tag_edit_copy = tag_edit_element.clone();
tag_edit_copy.click(function() { tag_edit_copy.click(function () {
addTagModalShow(item.id, item.title, item.tags); addTagModalShow(item.id, item.title, item.tags);
}); });
tag_edit_copy.appendTo(tags); tag_edit_copy.appendTo(tags);
if (item.tags.length > 0) { if (item.tags.length > 0) {
item.tags.forEach(function(tag_tuple) { item.tags.forEach(function (tag_tuple) {
const tag_copy = tag_element.clone(); const tag_copy = tag_element.clone();
tag_copy.html(tag_tuple[0]); tag_copy.html(tag_tuple[0]);
tag_copy.addClass('badge-' + tag_tuple[1]); tag_copy.addClass('badge-' + tag_tuple[1]);
@ -544,7 +553,7 @@ function addResultItem(item) {
function getFilters(dest_page = 1) { function getFilters(dest_page = 1) {
const tags = $('.tag-clicked'); const tags = $('.tag-clicked');
const tags_list = []; const tags_list = [];
tags.each(function(index, tag) { tags.each(function (index, tag) {
tags_list.push(tag.innerHTML); tags_list.push(tag.innerHTML);
}); });
@ -575,19 +584,19 @@ function updateResults(dest_page = 1) {
lib_group.animate({ lib_group.animate({
opacity: 0, opacity: 0,
}, 200, function() { }, 200, function () {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: 'library', url: 'library',
data: data, data: data,
statusCode: { statusCode: {
200: processResults, 200: processResults,
204: function() { 204: function () {
lib_loading.hide(); lib_loading.hide();
lib_empty.show(); lib_empty.show();
page_ul.empty(); page_ul.empty();
}, },
403: function() { 403: function () {
location.reload(true); location.reload(true);
}, },
}, },
@ -651,7 +660,7 @@ document.getElementById('library-download-btn').addEventListener('click', () =>
}); });
document.getElementById('library-rescan-btn').addEventListener('click', () => { document.getElementById('library-rescan-btn').addEventListener('click', () => {
request('post', {action: 'rescan'}); request('post', { action: 'rescan' });
updateResults(); updateResults();
}); });
@ -671,16 +680,16 @@ const page_no = $('.library-page-no');
function processResults(data) { function processResults(data) {
lib_group.animate({ lib_group.animate({
opacity: 0, opacity: 0,
}, 200, function() { }, 200, function () {
lib_loading.hide(); lib_loading.hide();
const total_pages = data.total_pages; const total_pages = data.total_pages;
const active_page = data.active_page; const active_page = data.active_page;
const items = data.items; const items = data.items;
items.forEach( items.forEach(
function(item) { function (item) {
addResultItem(item); addResultItem(item);
bindLibraryResultEvent(); bindLibraryResultEvent();
}, },
); );
page_ul.empty(); page_ul.empty();
@ -698,7 +707,7 @@ function processResults(data) {
page_no_copy = page_no.clone(); page_no_copy = page_no.clone();
page_no_copy.html('&laquo;'); page_no_copy.html('&laquo;');
page_no_copy.click(function(e) { page_no_copy.click(function (e) {
updateResults(1); updateResults(1);
}); });
@ -714,7 +723,7 @@ function processResults(data) {
if (active_page === i) { if (active_page === i) {
page_li_copy.addClass('active'); page_li_copy.addClass('active');
} else { } else {
page_no_copy.click(function(e) { page_no_copy.click(function (e) {
const _page_no = $(e.currentTarget).html(); const _page_no = $(e.currentTarget).html();
updateResults(_page_no); updateResults(_page_no);
}); });
@ -728,7 +737,7 @@ function processResults(data) {
page_no_copy = page_no.clone(); page_no_copy = page_no.clone();
page_no_copy.html('&raquo;'); page_no_copy.html('&raquo;');
page_no_copy.click(function(e) { page_no_copy.click(function (e) {
updateResults(total_pages); updateResults(total_pages);
}); });
@ -757,11 +766,11 @@ function addTagModalShow(_id, _title, _tag_tuples) {
add_tag_modal_title.html('Edit tags for ' + _title); add_tag_modal_title.html('Edit tags for ' + _title);
add_tag_modal_item_id.val(_id); add_tag_modal_item_id.val(_id);
add_tag_modal_tags.empty(); add_tag_modal_tags.empty();
_tag_tuples.forEach(function(tag_tuple) { _tag_tuples.forEach(function (tag_tuple) {
modal_tag_text.html(tag_tuple[0]); modal_tag_text.html(tag_tuple[0]);
const tag_copy = modal_tag.clone(); const tag_copy = modal_tag.clone();
const modal_tag_remove = tag_copy.find('.modal-tag-remove'); const modal_tag_remove = tag_copy.find('.modal-tag-remove');
modal_tag_remove.click(function(e) { modal_tag_remove.click(function (e) {
$(e.currentTarget).parent().remove(); $(e.currentTarget).parent().remove();
}); });
tag_copy.show(); tag_copy.show();
@ -772,14 +781,14 @@ function addTagModalShow(_id, _title, _tag_tuples) {
} }
document.getElementById('addTagModalAddBtn').addEventListener('click', () => { document.getElementById('addTagModalAddBtn').addEventListener('click', () => {
const new_tags = add_tag_modal_input.val().split(',').map(function(str) { const new_tags = add_tag_modal_input.val().split(',').map(function (str) {
return str.trim(); return str.trim();
}); });
new_tags.forEach(function(tag) { new_tags.forEach(function (tag) {
modal_tag_text.html(tag); modal_tag_text.html(tag);
const tag_copy = modal_tag.clone(); const tag_copy = modal_tag.clone();
const modal_tag_remove = tag_copy.find('.modal-tag-remove'); const modal_tag_remove = tag_copy.find('.modal-tag-remove');
modal_tag_remove.click(function(e) { modal_tag_remove.click(function (e) {
$(e.currentTarget).parent().remove(); $(e.currentTarget).parent().remove();
}); });
tag_copy.show(); tag_copy.show();
@ -792,7 +801,7 @@ document.getElementById('addTagModalAddBtn').addEventListener('click', () => {
document.getElementById('addTagModalSubmit').addEventListener('click', () => { document.getElementById('addTagModalSubmit').addEventListener('click', () => {
const all_tags = $('.modal-tag-text'); const all_tags = $('.modal-tag-text');
const tags = []; const tags = [];
all_tags.each(function(i, element) { all_tags.each(function (i, element) {
if (element.innerHTML) { if (element.innerHTML) {
tags.push(element.innerHTML); tags.push(element.innerHTML);
} }
@ -820,7 +829,7 @@ let volume_popover_instance = null;
let volume_popover_show = false; let volume_popover_show = false;
let volume_update_timer; let volume_update_timer;
volumePopoverBtn.addEventListener('click', function(e) { volumePopoverBtn.addEventListener('click', function (e) {
e.stopPropagation(); e.stopPropagation();
if (!volume_popover_show) { if (!volume_popover_show) {
@ -842,7 +851,7 @@ volumePopoverBtn.addEventListener('click', function(e) {
} }
volume_popover_show = !volume_popover_show; volume_popover_show = !volume_popover_show;
document.addEventListener('click', function() { document.addEventListener('click', function () {
volumePopoverDiv.removeAttribute('data-show'); volumePopoverDiv.removeAttribute('data-show');
if (volume_popover_instance) { if (volume_popover_instance) {
volume_popover_instance.destroy(); volume_popover_instance.destroy();
@ -854,7 +863,7 @@ volumePopoverBtn.addEventListener('click', function(e) {
}); });
}); });
volumePopoverBtn.addEventListener('click', function(e) { volumePopoverBtn.addEventListener('click', function (e) {
e.stopPropagation(); e.stopPropagation();
}); });
@ -870,11 +879,11 @@ volumeSlider.addEventListener('change', (e) => {
}); });
document.getElementById('volume-down-btn').addEventListener('click', () => { document.getElementById('volume-down-btn').addEventListener('click', () => {
request('post', {action: 'volume_down'}); request('post', { action: 'volume_down' });
}); });
document.getElementById('volume-up-btn').addEventListener('click', () => { document.getElementById('volume-up-btn').addEventListener('click', () => {
request('post', {action: 'volume_up'}); request('post', { action: 'volume_up' });
}); });
// --------------------- // ---------------------
@ -970,7 +979,7 @@ function uploadNextFile() {
const file = filesToProceed.shift(); const file = filesToProceed.shift();
const file_progress_item = filesProgressItem[file.name]; const file_progress_item = filesProgressItem[file.name];
req.addEventListener('load', function() { req.addEventListener('load', function () {
if (this.status === 200) { if (this.status === 200) {
setProgressBar(file_progress_item.progress, 1); setProgressBar(file_progress_item.progress, 1);
file_progress_item.progress.classList.add('bg-success'); file_progress_item.progress.classList.add('bg-success');
@ -1003,7 +1012,7 @@ function uploadNextFile() {
} }
}); });
req.upload.addEventListener('progress', function(e) { req.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) { if (e.lengthComputable) {
const percent = e.loaded / e.total; const percent = e.loaded / e.total;
setProgressBar(file_progress_item.progress, percent, Math.floor(percent * 100) + '%'); setProgressBar(file_progress_item.progress, percent, Math.floor(percent * 100) + '%');
@ -1050,12 +1059,12 @@ const musicUrlInput = document.getElementById('music-url-input');
const radioUrlInput = document.getElementById('radio-url-input'); const radioUrlInput = document.getElementById('radio-url-input');
document.getElementById('add-music-url').querySelector('button').addEventListener('click', () => { document.getElementById('add-music-url').querySelector('button').addEventListener('click', () => {
request('post', {add_url: musicUrlInput.value}); request('post', { add_url: musicUrlInput.value });
musicUrlInput.value = ''; musicUrlInput.value = '';
}); });
document.getElementById('add-radio-url').querySelector('button').addEventListener('click', () => { document.getElementById('add-radio-url').querySelector('button').addEventListener('click', () => {
request('post', {add_radio: radioUrlInput.value}); request('post', { add_radio: radioUrlInput.value });
radioUrlInput.value = ''; radioUrlInput.value = '';
}); });
@ -1077,15 +1086,15 @@ const playerSkipBtn = document.getElementById('playerSkipBtn');
let currentPlayingItem = null; let currentPlayingItem = null;
playerPlayBtn.addEventListener('click', () => { playerPlayBtn.addEventListener('click', () => {
request('post', {action: 'resume'}); request('post', { action: 'resume' });
}); });
playerPauseBtn.addEventListener('click', () => { playerPauseBtn.addEventListener('click', () => {
request('post', {action: 'pause'}); request('post', { action: 'pause' });
}); });
playerSkipBtn.addEventListener('click', () => { playerSkipBtn.addEventListener('click', () => {
request('post', {action: 'next'}); request('post', { action: 'next' });
}); });
document.getElementById('player-toast').addEventListener('click', () => { document.getElementById('player-toast').addEventListener('click', () => {
@ -1111,6 +1120,7 @@ function updatePlayerInfo(item) {
playerTitle.textContent = item.title; playerTitle.textContent = item.title;
playerArtist.textContent = item.artist; playerArtist.textContent = item.artist;
playerArtwork.setAttribute('src', item.thumbnail); playerArtwork.setAttribute('src', item.thumbnail);
playerArtwork.setAttribute('alt', coverArtString(item.title));
if (isOverflown(playerTitle)) { if (isOverflown(playerTitle)) {
playerTitle.classList.add('scrolling'); playerTitle.classList.add('scrolling');
@ -1159,7 +1169,7 @@ function updatePlayerPlayhead(playhead) {
player_playhead_position = playhead; player_playhead_position = playhead;
setProgressBar(playerBar, player_playhead_position / currentPlayingItem.duration, secondsToStr(player_playhead_position)); setProgressBar(playerBar, player_playhead_position / currentPlayingItem.duration, secondsToStr(player_playhead_position));
if (playing) { if (playing) {
playhead_timer = setInterval(function() { playhead_timer = setInterval(function () {
player_playhead_position += 0.3; player_playhead_position += 0.3;
setProgressBar(playerBar, player_playhead_position / currentPlayingItem.duration, secondsToStr(player_playhead_position)); setProgressBar(playerBar, player_playhead_position / currentPlayingItem.duration, secondsToStr(player_playhead_position));
}, 300); // delay in milliseconds }, 300); // delay in milliseconds
@ -1174,7 +1184,7 @@ function updatePlayerPlayhead(playhead) {
} }
} }
playerBarBox.addEventListener('mousedown', function() { playerBarBox.addEventListener('mousedown', function () {
if (currentPlayingItem && currentPlayingItem.duration > 0) { if (currentPlayingItem && currentPlayingItem.duration > 0) {
playerBarBox.addEventListener('mousemove', playheadDragged); playerBarBox.addEventListener('mousemove', playheadDragged);
clearInterval(playhead_timer); clearInterval(playhead_timer);
@ -1182,7 +1192,7 @@ playerBarBox.addEventListener('mousedown', function() {
} }
}); });
playerBarBox.addEventListener('mouseup', function(event) { playerBarBox.addEventListener('mouseup', function (event) {
playerBarBox.removeEventListener('mousemove', playheadDragged); playerBarBox.removeEventListener('mousemove', playheadDragged);
const percent = (event.clientX - playerBarBox.getBoundingClientRect().x) / playerBarBox.clientWidth; const percent = (event.clientX - playerBarBox.getBoundingClientRect().x) / playerBarBox.clientWidth;
request('post', { request('post', {

View File

@ -16,3 +16,20 @@ export function secondsToStr(seconds) {
const secs = seconds % 60; const secs = seconds % 60;
return ('00' + mins).slice(-2) + ':' + ('00' + secs).slice(-2); return ('00' + mins).slice(-2) + ':' + ('00' + secs).slice(-2);
} }
export function coverArtString(title) {
let nameOfSong = "";
// The maximum length before we start truncating
const maxLength = 50;
if (title.length > maxLength) {
// Name = longTitleTooLongToBeAGoodAltTex...
nameOfSong = title.substr(0, maxLength) + "\u2026";
} else {
// Name = shortTitle
nameOfSong = title;
}
return 'Cover art for ' + nameOfSong;
}

View File

@ -2,4 +2,4 @@
@import '~bootstrap/scss/bootstrap'; @import '~bootstrap/scss/bootstrap';
@import '~bootswatch/dist/darkly/bootswatch'; @import '~bootswatch/dist/darkly/bootswatch';
@import './main'; @import './main';

View File

@ -1,3 +1,3 @@
@import '~bootstrap/scss/bootstrap'; @import '~bootstrap/scss/bootstrap';
@import './main'; @import './main';

View File

@ -1,117 +1,141 @@
.btn-space {margin-right: 5px;} .btn-space {
margin-right: 5px;
}
/* Playlist */ /* Playlist */
.playlist-item {transition: all 0.2s ease-in-out;} .playlist-item {
.playlist-artwork { transition: all 0.2s ease-in-out;
float: left;
margin-left: 10px;
white-space: nowrap;
overflow: hidden;
} }
.playlist-artwork {
float: left;
margin-left: 10px;
white-space: nowrap;
overflow: hidden;
}
.tag-space { .tag-space {
margin-right: 3px; margin-right: 3px;
} }
.tag-click { .tag-click {
cursor: pointer; cursor: pointer;
transition: 400ms; transition: 400ms;
} }
.tag-unclicked { .tag-unclicked {
opacity: 0.6; opacity: 0.6;
} }
.tag-clicked { .tag-clicked {
box-shadow: 2px 4px 10px #777777; box-shadow: 2px 4px 10px #777777;
transform: scale(1.2); transform: scale(1.2);
opacity: 1; opacity: 1;
margin: 5px; margin: 5px;
} }
.library-item { .library-item {
display: flex; display: flex;
padding: .5rem .5rem .5rem 0; padding: .5rem .5rem .5rem 0;
height: 72px; height: 72px;
transition: ease-in-out 200ms; transition: ease-in-out 200ms;
} }
.library-thumb-img { .library-thumb-img {
width: 70px; width: 70px;
height: 70px; height: 70px;
border-radius: 5px; border-radius: 5px;
} }
.library-thumb-col { .library-thumb-col {
position: relative; position: relative;
padding-left: 0; padding-left: 0;
overflow: hidden; overflow: hidden;
margin: -0.5rem 1rem -0.5rem 0; margin: -0.5rem 1rem -0.5rem 0;
} }
.library-thumb-grp { .library-thumb-grp {
position: absolute; position: absolute;
top: 0; top: 0;
left: -95px; left: -95px;
width: 70px; width: 70px;
margin-left: 15px; margin-left: 15px;
transition: left 300ms; transition: left 300ms;
border-radius: 5px; border-radius: 5px;
opacity: 0.7; opacity: 0.7;
font-weight: 300; font-weight: 300;
} }
.library-thumb-grp-hover { .library-thumb-grp-hover {
left: -15px; left: -15px;
} }
.library-thumb-btn-up { .library-thumb-btn-up {
position: absolute !important; position: absolute !important;
top: 0; top: 0;
height: 70px; height: 70px;
font-size: 2em; font-size: 2em;
padding-top: 10px; padding-top: 10px;
} }
.library-btn-svg { .library-btn-svg {
width: 1rem; width: 1rem;
fill: currentColor; fill: currentColor;
} }
.library-info-col { .library-info-col {
margin-right: 1rem; margin-right: 1rem;
padding: 3px 0; padding: 3px 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.library-info-col .small { .library-info-col .small {
font-weight: 300; font-weight: 300;
} }
.library-action { .library-action {
margin-left: auto; margin-left: auto;
} }
.library-info-col .path { .library-info-col .path {
font-style: italic !important; font-style: italic !important;
font-weight: 300; font-weight: 300;
} }
/* Theme changer and player button */ /* Theme changer and player button */
.floating-button { .floating-button {
width: 50px; width: 50px;
height: 50px; height: 50px;
background-color: #aaaaaa40; background-color: #aaaaaa40;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 6px 10px 0 #66666647; box-shadow: 0 6px 10px 0 #66666647;
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
font-size: 25px; font-size: 25px;
color: #9896967a; color: #9896967a;
text-align: center; text-align: center;
line-height: 52px; line-height: 52px;
position: fixed; position: fixed;
right: 50px; right: 50px;
} }
.floating-button:hover { .floating-button:hover {
background-color: hsl(0, 0%, 43%); background-color: hsl(0, 0%, 43%);
color: white; color: white;
} }
#volume-slider { #volume-slider {
margin-top: 4px; margin-top: 4px;
margin-right: 5px; margin-right: 5px;
} }
.dropdown { .dropdown {
display: inline-block; display: inline-block;
} }
#volume-popover { #volume-popover {
position: relative; position: relative;
background: #333; background: #333;
@ -122,45 +146,53 @@
border-radius: 4px; border-radius: 4px;
display: none; display: none;
} }
#volume-popover[data-show] { #volume-popover[data-show] {
display: flex; display: flex;
} }
#volume-popover a { #volume-popover a {
cursor: pointer; cursor: pointer;
} }
#volume-popover-arrow, #volume-popover-arrow,
#volume-popover-arrow::before { #volume-popover-arrow::before {
position: absolute; position: absolute;
width: 10px; width: 10px;
height: 10px; height: 10px;
z-index: -1; z-index: -1;
top: 16px; top: 16px;
left: 46px; left: 46px;
} }
#volume-popover-arrow::before { #volume-popover-arrow::before {
content: ''; content: '';
transform: rotate(45deg); transform: rotate(45deg);
background: #333; background: #333;
} }
#volume-popover[data-popper-placement^='top'] > #volume-popover-arrow {
bottom: -4px; #volume-popover[data-popper-placement^='top']>#volume-popover-arrow {
bottom: -4px;
} }
#playerToast { #playerToast {
position: fixed; position: fixed;
right: 20px; right: 20px;
top: 20px; top: 20px;
max-width: 800px; max-width: 800px;
} }
#playerContainer { #playerContainer {
display: flex; display: flex;
height: 105px; height: 105px;
} }
#playerArtwork { #playerArtwork {
width: 80px; width: 80px;
height: 80px; height: 80px;
border-radius: 5px; border-radius: 5px;
} }
#playerArtworkIdle { #playerArtworkIdle {
width: 80px; width: 80px;
height: 80px; height: 80px;
@ -168,6 +200,7 @@
margin: auto; margin: auto;
padding: 15px; padding: 15px;
} }
#playerInfo { #playerInfo {
position: relative; position: relative;
padding-top: 6px; padding-top: 6px;
@ -175,40 +208,58 @@
height: 80px; height: 80px;
font-size: 15px; font-size: 15px;
} }
#playerTitle { #playerTitle {
display: block; display: block;
white-space: nowrap; white-space: nowrap;
} }
#playerArtist { #playerArtist {
display: block; display: block;
white-space: nowrap; white-space: nowrap;
min-height: 20px; min-height: 20px;
} }
#playerActionBox { #playerActionBox {
margin-top: 5px; margin-top: 5px;
display: flex; display: flex;
float: right; float: right;
} }
#playerBarBox { #playerBarBox {
margin-top: 5px; margin-top: 5px;
height: 15px; height: 15px;
width: 400px; width: 400px;
cursor: pointer; cursor: pointer;
} }
.scrolling { .scrolling {
animation: scrolling 8s linear infinite; animation: scrolling 8s linear infinite;
} }
@keyframes scrolling { @keyframes scrolling {
0% { 0% {
transform: translateX(100%); transform: translateX(100%);
opacity: 1; opacity: 1;
} }
95%{
95% {
transform: translateX(-90%); transform: translateX(-90%);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateX(-100%); transform: translateX(-100%);
opacity: 0; opacity: 0;
} }
} }
// Allows us to have H3 with the size of an H5
h3 {
font-size: 1.25rem;
}
// Makes legend match the size of other labels
legend {
font-size: 1rem;
}

View File

@ -1,5 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -12,28 +14,31 @@
</head> </head>
<body> <body>
<div class="container page-header mb-5" id="banner"> <header class="container page-header mb-5" id="banner">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<img src="static/image/logo.png" height="200px"> <img src="static/image/logo.png" height="200px"
alt="Botamusique Logo: a person with two headphones, enjoying the music">
</div> </div>
<div class="col my-auto"> <div class="col my-auto">
<h1>botamusique Web Interface</h1> <h1>botamusique Web Interface</h1>
</div> </div>
</div> </div>
</div> </header>
<div id="playlist" class="container mb-5"> <main id="playlist" class="container mb-5">
<div class="btn-toolbar mb-2" role="toolbar" aria-label="Playlist controls"> <div class="btn-toolbar mb-2" role="toolbar" aria-label="Playlist controls">
<button type="button" id="play-pause-btn" class="btn btn-info mb-2 btn-space"> <button type="button" id="play-pause-btn" class="btn btn-info mb-2 btn-space" aria-label="Play">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
<button type="button" id="fast-forward-btn" class="btn btn-info mb-2"> <button type="button" id="fast-forward-btn" class="btn btn-info mb-2" aria-label="Skip Track">
<i class="fas fa-fast-forward"></i> <i class="fas fa-fast-forward"></i>
</button> </button>
<div class="ml-auto"> <div class="ml-auto">
<div class="dropdown mr-2"> <div class="dropdown mr-2">
<button class="btn btn-secondary dropdown-toggle" type="button" id="play-mode" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button class="btn btn-secondary dropdown-toggle" type="button" id="play-mode"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
aria-label="Change Playback Mode">
<i class="fas fa-tasks mr-2" aria-hidden="true" id="modeIndicator"></i> <i class="fas fa-tasks mr-2" aria-hidden="true" id="modeIndicator"></i>
</button> </button>
<div class="dropdown-menu" aria-labelledby="play-mode"> <div class="dropdown-menu" aria-labelledby="play-mode">
@ -51,7 +56,8 @@
</a> </a>
</div> </div>
</div> </div>
<button type="button" id="volume-popover-btn" class="btn btn-warning ml-1"> <button type="button" id="volume-popover-btn" class="btn btn-warning ml-1"
aria-label="Open Volume Controls">
<i class="fa fa-volume-up" aria-hidden="true"></i> <i class="fa fa-volume-up" aria-hidden="true"></i>
</button> </button>
@ -60,7 +66,8 @@
<i class="fa fa-volume-down" aria-hidden="true"></i> <i class="fa fa-volume-down" aria-hidden="true"></i>
</a> </a>
<input type="range" class="custom-range ml-1" id="volume-slider" min="0" max="1" step="0.01" value="0.5" /> <input type="range" class="custom-range ml-1" id="volume-slider" min="0" max="1" step="0.01"
value="0.5" aria-label="Volume Slider" />
<a id="volume-up-btn"> <a id="volume-up-btn">
<i class="fa fa-volume-up" aria-hidden="true"></i> <i class="fa fa-volume-up" aria-hidden="true"></i>
@ -83,25 +90,29 @@
<tbody id="playlist-table" class="playlist-table"> <tbody id="playlist-table" class="playlist-table">
<tr id="playlist-loading"> <tr id="playlist-loading">
<td colspan="4" class="text-center"> <td colspan="4" class="text-center">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" /> <img style="margin: auto; width: 35px;" src="static/image/loading.svg"
alt="A loading spinner" />
</td> </td>
</tr> </tr>
<tr id="playlist-empty" class="d-none"> <tr id="playlist-empty" class="d-none">
<td colspan="4" class="text-center"> <td colspan="4" class="text-center">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg" /> <img style="margin: auto; width: 35px;" src="static/image/empty_box.svg"
alt="A drawing of an empty box." />
</td> </td>
</tr> </tr>
<tr class="playlist-expand table-dark d-none"> <tr class="playlist-expand table-dark d-none">
<td colspan="4" class="text-center"> <td colspan="4" class="text-center">
<a class="text-muted" href="javascript:">See item <span class="playlist-expand-item-range"></span> on the playlist.</a> <a class="text-muted" href="javascript:">See item <span
class="playlist-expand-item-range"></span> on the playlist.</a>
</td> </td>
</tr> </tr>
<tr class="playlist-item-template d-none"> <tr class="playlist-item-template d-none" aria-hidden="true">
<th scope="row" class="playlist-item-index d-none d-md-table-cell"></th> <th scope="row" class="playlist-item-index d-none d-md-table-cell">1</th>
<td> <td>
<input hidden type="text" class="playlist-item-id" value="" /> <input hidden type="hidden" class="playlist-item-id" value="" />
<div class="float-left"> <div class="float-left">
<img width="80" class="playlist-item-thumbnail" src="static/image/unknown-album.png" /> <img width="80" class="playlist-item-thumbnail" src="static/image/unknown-album.png"
alt="A black square with two eight notes beamed together." />
</div> </div>
<div class="playlist-artwork"> <div class="playlist-artwork">
<b class="playlist-item-title"></b> <b class="playlist-item-title"></b>
@ -122,10 +133,12 @@
</td> </td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
<button type="button" class="playlist-item-play btn btn-info btn-sm"> <button type="button" class="playlist-item-play btn btn-info btn-sm"
aria-label="Skip current song and play this song right now">
<i class="fas fa-play" aria-hidden="true"></i> <i class="fas fa-play" aria-hidden="true"></i>
</button> </button>
<button type="button" class="playlist-item-trash btn btn-danger btn-sm ml-1"> <button type="button" class="playlist-item-trash btn btn-danger btn-sm ml-1"
aria-label="Remove this song from the current playlist">
<i class="fas fa-trash-alt" aria-hidden="true"></i> <i class="fas fa-trash-alt" aria-hidden="true"></i>
</button> </button>
</div> </div>
@ -140,19 +153,19 @@
<i class="fas fa-trash-alt" aria-hidden="true"></i> Clear Playlist <i class="fas fa-trash-alt" aria-hidden="true"></i> Clear Playlist
</button> </button>
</div> </div>
</div> </main>
<div class="container mb-3"> <div class="container mb-3">
<h2 id="forms">Music Library</h2> <h2 id="forms">Music Library</h2>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<h5 class="card-title">Filters</h5> <h3 class="card-title">Filters</h3>
<hr> <hr>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<label>Type</label> <fieldset id="filter-type" class="mb-2">
<div id="filter-type" class="input-group mb-2"> <legend>Type</legend>
<div class="btn-group btn-group-sm btn-group-toggle"> <div class="btn-group btn-group-sm btn-group-toggle">
<label id="filter-type-file" class="btn btn-secondary"> <label id="filter-type-file" class="btn btn-secondary">
<input type="checkbox" name="options"> File <input type="checkbox" name="options"> File
@ -164,7 +177,7 @@
<input type="checkbox" name="options"> Radio <input type="checkbox" name="options"> Radio
</label> </label>
</div> </div>
</div> </fieldset>
<label for="filter-dir">Directory</label> <label for="filter-dir">Directory</label>
<div id="filter-path" class="input-group mb-2"> <div id="filter-path" class="input-group mb-2">
@ -178,34 +191,39 @@
<label for="filter-keywords">Keywords</label> <label for="filter-keywords">Keywords</label>
<div id="filter-path" class="input-group mb-2"> <div id="filter-path" class="input-group mb-2">
<input class="form-control form-control-sm" id="filter-keywords" name="keywords" placeholder="Keywords..." style="margin-top:5px;" /> <input class="form-control form-control-sm" id="filter-keywords" name="keywords"
placeholder="Keywords..." style="margin-top:5px;" />
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<label for="filter-tag">Tags</label> <fieldset id="filter-type mb-2">
<div id="filter-type mb-2"> <legend>Tags</legend>
{% for tag in tags_color_lookup.keys() %} {% for tag in tags_color_lookup.keys() %}
<span id="filter-tag" class="filter-tag tag-unclicked tag-click badge badge-{{ tags_color_lookup[tag] }}">{{ tag }}</span> <span id="filter-tag"
class="filter-tag tag-unclicked tag-click badge badge-{{ tags_color_lookup[tag] }}">{{ tag }}</span>
{% endfor %} {% endfor %}
</div> </fieldset>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="library-group" class="list-group library-group" style="overflow: auto;"> <div id="library-group" class="list-group library-group" style="overflow: auto;">
<div id="library-item-loading" class="list-group-item library-item"> <div id="library-item-loading" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" /> <img style="margin: auto; width: 35px;" src="static/image/loading.svg"
alt="A loading spinner" />
</div> </div>
<div id="library-item-empty" style="display: none" class="list-group-item library-item"> <div id="library-item-empty" style="display: none" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg" /> <img style="margin: auto; width: 35px;" src="static/image/empty_box.svg"
alt="A drawing of an empty box." />
</div> </div>
<div id="library-item" style="display: none;" class="list-group-item library-item"> <div id="library-item" style="display: none;" class="list-group-item library-item">
<input hidden type="text" class="library-item-id" value="" /> <input hidden type="hidden" class="library-item-id" value="" />
<div class="library-thumb-col"> <div class="library-thumb-col">
<div class="library-thumb-img"> <div class="library-thumb-img">
<img class="library-item-thumb library-thumb-img" src="static/image/unknown-album.png" /> <img class="library-item-thumb library-thumb-img" src="static/image/unknown-album.png"
alt="A black square with two eight notes beamed together." />
</div> </div>
<div class="btn-group-vertical library-thumb-grp"> <div class="btn-group-vertical library-thumb-grp">
<div class="library-item-play btn btn-secondary library-thumb-btn-up" title="Play"> <div class="library-item-play btn btn-secondary library-thumb-btn-up" title="Play">
@ -225,28 +243,40 @@
<div class="library-info-col col-4 d-none d-md-flex" style="padding: 3px;"> <div class="library-info-col col-4 d-none d-md-flex" style="padding: 3px;">
<span class="library-item-path text-muted path">Path/to/the/file</span> <span class="library-item-path text-muted path">Path/to/the/file</span>
<div class="library-item-tags"> <div class="library-item-tags">
<a class="tag-space tag-click library-item-edit"><i class="fas fa-edit" style="color: #AAAAAA"></i></a> <a class="tag-space tag-click library-item-edit"><i class="fas fa-edit"
style="color: #AAAAAA"></i></a>
<span class="library-item-notag badge badge-light text-muted font-italic">No tag</span> <span class="library-item-notag badge badge-light text-muted font-italic">No tag</span>
<span class="library-item-tag tag-space badge">Tag</span> <span class="library-item-tag tag-space badge">Tag</span>
</div> </div>
</div> </div>
<div class="btn-group library-action"> <div class="btn-group library-action">
<button class="library-item-add-next btn btn-info btn-sm btn-space" type="button" title="Next to play"> <button class="library-item-add-next btn btn-info btn-sm btn-space" type="button"
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="5 5 17 17"> title="Next to play" aria-label="Add to playlist right after current song">
<path d="m5.700245,3.92964l0,14.150376l11.451127,-7.075188l-11.451127,-7.075188z"></path> <svg class="library-btn-svg" style="width: 1rem; fill: currentColor;"
<path d="m20.942859,18.221072l-3.323292,0l0,3.323292l-1.107764,0l0,-3.323292l-3.323292,0l0,-1.107764l3.323292,0l0,-3.323292l1.107764,0l0,3.323292l3.323292,0l0,1.107764z"></path> viewBox="5 5 17 17">
<path d="m5.700245,3.92964l0,14.150376l11.451127,-7.075188l-11.451127,-7.075188z">
</path>
<path
d="m20.942859,18.221072l-3.323292,0l0,3.323292l-1.107764,0l0,-3.323292l-3.323292,0l0,-1.107764l3.323292,0l0,-3.323292l1.107764,0l0,3.323292l3.323292,0l0,1.107764z">
</path>
</svg> </svg>
</button> </button>
<button class="library-item-add-bottom library-btn btn btn-info btn-sm btn-space" type="button" title="Add to bottom"> <button class="library-item-add-bottom library-btn btn btn-info btn-sm btn-space"
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="2 2 20 20"> type="button" title="Add to bottom" aria-label="Add to bottom of current playlist">
<path d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z"></path> <svg class="library-btn-svg" style="width: 1rem; fill: currentColor;"
viewBox="2 2 20 20">
<path
d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z">
</path>
</svg> </svg>
</button> </button>
<button class="library-item-download btn btn-primary btn-sm btn-space" type="button"> <button class="library-item-download btn btn-primary btn-sm btn-space" type="button"
aria-label="Download song from library">
<i class="fas fa-download" aria-hidden="true"></i> <i class="fas fa-download" aria-hidden="true"></i>
</button> </button>
<button class="library-item-trash btn btn-danger btn-sm btn-space" type="button"> <button class="library-item-trash btn btn-danger btn-sm btn-space" type="button"
aria-label="Remove song from library">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
@ -255,56 +285,59 @@
</div> </div>
<div class="list-group"> <div class="list-group">
<div id="library-pagination" style="margin-left: auto; margin-top: 10px;"> <div id="library-pagination" style="margin-left: auto; margin-top: 10px;">
<ul id="library-page-ul" class="pagination pagination"> <ul id="library-page-ul" class="pagination pagination">
<li class="library-page-li page-item active"> <li class="library-page-li page-item active">
<a class="library-page-no page-link">1</a> <a class="library-page-no page-link">1</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="btn-group mb-2" role="group"> <div class="btn-group mb-2" role="group">
<button id="add-to-playlist-btn" type="button" class="btn btn-secondary mr-1"> <button id="add-to-playlist-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-plus" aria-hidden="true"></i> Add All <i class="fas fa-plus" aria-hidden="true"></i> Add All
</button> </button>
<button id="library-rescan-btn" type="button" class="btn btn-secondary mr-1"> <button id="library-rescan-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files <i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files
</button> </button>
<button id="library-download-btn" type="button" class="btn btn-secondary mr-1"> <button id="library-download-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-download" aria-hidden="true"></i> Download All <i class="fas fa-download" aria-hidden="true"></i> Download All
</button> </button>
<button type="button" class="btn btn-danger mr-1" data-toggle="modal" data-target="#deleteWarningModal"> <button type="button" class="btn btn-danger mr-1" data-toggle="modal"
<i class="fas fa-trash-alt" aria-hidden="true"></i> Delete All data-target="#deleteWarningModal">
</button> <i class="fas fa-trash-alt" aria-hidden="true"></i> Delete All
</button>
</div> </div>
<div class="modal fade" id="deleteWarningModal" tabindex="-1" role="dialog" aria-labelledby="Warning-Delete-File" aria-hidden="true"> <!-- QUESTION: should this div have aria-hidden as true?? -->
<div class="modal-dialog" role="document"> <div class="modal fade" id="deleteWarningModal" tabindex="-1" role="dialog"
<div class="modal-content"> aria-label="Modal Window for warning about deletion of files." aria-hidden="true">
<div class="modal-header"> <div class="modal-dialog" role="document">
<h5 class="modal-title" id="deleteWarningModalLabel">Are you really sure?</h5> <div class="modal-content">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <div class="modal-header">
<span aria-hidden="true">&times;</span> <h3 class="modal-title" id="deleteWarningModalLabel">Are you really sure?</h3>
</button> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
</div> <span aria-hidden="true">&times;</span>
<div class="modal-body"> </button>
All files listed here, include files on other pages, will be deleted from your </div>
hard-drive. <div class="modal-body">
Is that what you want? All files listed here, include files on other pages, will be deleted from your
</div> hard-drive.
<div class="modal-footer"> Is that what you want?
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> </div>
<button id="library-delete-btn" type="button" class="btn btn-danger" data-dismiss="modal">Delete All Listed Files</button> <div class="modal-footer">
</div> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="library-delete-btn" type="button" class="btn btn-danger"
data-dismiss="modal">Delete All Listed Files</button>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- beautify ignore:start -->
{% if upload_enabled %} {% if upload_enabled %}
<div id="upload" class="container mb-3"> <div id="upload" class="container mb-3">
{% else %} {% else %}
@ -313,7 +346,7 @@
{% endif %} {% endif %}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title">Upload File</h5> <h3 class="card-title">Upload File</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="./upload" method="post" enctype="multipart/form-data"> <form action="./upload" method="post" enctype="multipart/form-data">
@ -333,7 +366,7 @@
<div class="col"> <div class="col">
<div class="input-group mb-3"> <div class="input-group mb-3">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">Upload To</span> <label for="uploadTargetDir" class="input-group-text">Upload To</label>
</div> </div>
<input class="form-control" list="targetdirs" id="uploadTargetDir" name="targetdir" placeholder="uploads" /> <input class="form-control" list="targetdirs" id="uploadTargetDir" name="targetdir" placeholder="uploads" />
<datalist id="targetdirs"> <datalist id="targetdirs">
@ -353,13 +386,12 @@
</div> </div>
</div> </div>
</div> </div>
<!-- beautify ignore:end -->
<div class="container mb-5"> <div class="container mb-5">
<div class="card-deck"> <div class="card-deck">
<div id="add-music-url" class="card"> <div id="add-music-url" class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title">Add URL</h5> <h3 class="card-title">Add URL</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<label for="music-url-input">Add Youtube or Soundcloud URL</label> <label for="music-url-input">Add Youtube or Soundcloud URL</label>
@ -367,13 +399,13 @@
<input class="form-control" type="text" id="music-url-input" placeholder="URL..."> <input class="form-control" type="text" id="music-url-input" placeholder="URL...">
</div> </div>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
Add URL Add URL
</button> </button>
</div> </div>
</div> </div>
<div id="add-radio-url" class="card"> <div id="add-radio-url" class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title">Add Radio</h5> <h3 class="card-title">Add Radio</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<label for="radio-url-input">Add Radio URL</label> <label for="radio-url-input">Add Radio URL</label>
@ -381,7 +413,7 @@
<input id="radio-url-input" class="form-control" type="text" placeholder="Radio Address..."> <input id="radio-url-input" class="form-control" type="text" placeholder="Radio Address...">
</div> </div>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
Add Radio Add Radio
</button> </button>
</div> </div>
</div> </div>
@ -405,17 +437,20 @@
</button> </button>
</div> </div>
<div class="toast-body" id="playerContainer"> <div class="toast-body" id="playerContainer">
<img id="playerArtworkIdle" src="static/image/empty_box.svg" /> <img id="playerArtworkIdle" src="static/image/empty_box.svg" alt="A drawing of an empty box." />
<img id="playerArtwork" src="static/image/unknown-album.png" style="display: none;" /> <img id="playerArtwork" src="static/image/unknown-album.png" style="display: none;"
alt="A black square with two eight notes beamed together." />
<div id="playerInfo"> <div id="playerInfo">
<div id="playerActionBox"> <div id="playerActionBox">
<button id="playerPlayBtn" class="btn btn-primary btn-sm btn-space" style="display: none"> <button id="playerPlayBtn" class="btn btn-primary btn-sm btn-space" style="display: none"
aria-label="Play">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
<button id="playerPauseBtn" class="btn btn-primary btn-sm btn-space" style="display: none"> <button id="playerPauseBtn" class="btn btn-primary btn-sm btn-space" style="display: none"
aria-label="Pause">
<i class="fas fa-pause"></i> <i class="fas fa-pause"></i>
</button> </button>
<button id="playerSkipBtn" class="btn btn-primary btn-sm"> <button id="playerSkipBtn" class="btn btn-primary btn-sm" aria-label="Skip to next track">
<i class="fas fa-fast-forward"></i> <i class="fas fa-fast-forward"></i>
</button> </button>
</div> </div>
@ -425,7 +460,9 @@
</div> </div>
<span id="playerArtist">Artist</span> <span id="playerArtist">Artist</span>
<div id="playerBarBox" class="progress"> <div id="playerBarBox" class="progress">
<div id="playerBar" class="progress-bar pr-2" role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 100%; text-align: right; transform: translateX(-100%);"></div> <div id="playerBar" class="progress-bar pr-2" role="progressbar" aria-valuenow="50"
aria-valuemin="0" aria-valuemax="100"
style="width: 100%; text-align: right; transform: translateX(-100%);"></div>
</div> </div>
</div> </div>
</div> </div>
@ -434,33 +471,35 @@
<div id="footer" style="height:50px; width: 100%; margin-top: 100px;"></div> <div id="footer" style="height:50px; width: 100%; margin-top: 100px;"></div>
<form id="download-form" action="download" method="GET" target="_blank"> <form id="download-form" action="download" method="GET" target="_blank">
<input hidden type="text" name="id" value=""> <input hidden type="hidden" name="id" value="">
<input hidden type="text" name="type" value=""> <input hidden type="hidden" name="type" value="">
<input hidden type="text" name="dir" value=""> <input hidden type="hidden" name="dir" value="">
<input hidden type="text" name="tags" value=""> <input hidden type="hidden" name="tags" value="">
<input hidden type="text" name="keywords" value=""> <input hidden type="hidden" name="keywords" value="">
</form> </form>
<!-- Add tags modal --> <!-- Add tags modal -->
<div class="modal fade" id="addTagModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" id="addTagModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="addTagModalTitle">Edit tags for ?</h5> <h3 class="modal-title" id="addTagModalTitle">Edit tags for ?</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div id="addTagModalBody" class="modal-body"> <div id="addTagModalBody" class="modal-body">
<input hidden type="text" id="addTagModalItemId" name="id" value=""> <input hidden type="hidden" id="addTagModalItemId" name="id" value="">
<div class="modal-tag" style="display: none; width: 100%;"> <div class="modal-tag" style="display: none; width: 100%;">
<span class="modal-tag-text tag-space badge badge-pill badge-dark">Tag</span> <span class="modal-tag-text tag-space badge badge-pill badge-dark">Tag</span>
<a class="modal-tag-remove tag-click small"><i class="fas fa-times-circle btn-outline-danger"></i></a> <a class="modal-tag-remove tag-click small"><i
class="fas fa-times-circle btn-outline-danger"></i></a>
</div> </div>
<div id="addTagModalTags" style="margin-left: 5px; margin-bottom: 10px;"> <div id="addTagModalTags" style="margin-left: 5px; margin-bottom: 10px;">
</div> </div>
<div class="input-group"> <div class="input-group">
<input class="form-control form-control-sm btn-space" type="text" id="addTagModalInput" placeholder="tag1,tag2,..."> <input class="form-control form-control-sm btn-space" type="text" id="addTagModalInput"
placeholder="tag1,tag2,..." aria-label="Tags to add">
<button id="addTagModalAddBtn" type="button" class="btn btn-primary btn-sm"> <button id="addTagModalAddBtn" type="button" class="btn btn-primary btn-sm">
<i class="fas fa-plus" aria-hidden="true"></i> <i class="fas fa-plus" aria-hidden="true"></i>
Add Add
@ -469,18 +508,19 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="addTagModalSubmit" type="button" class="btn btn-success" data-dismiss="modal">Edit!</button> <button id="addTagModalSubmit" type="button" class="btn btn-success"
data-dismiss="modal">Edit!</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Upload files modal --> <!-- Upload files modal -->
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" id="uploadModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="uploadTitle"><i class="fas fa-upload mr-1"></i>Uploading files...</h5> <h3 class="modal-title" id="uploadTitle"><i class="fas fa-upload mr-1"></i>Uploading files...</h3>
</div> </div>
<div id="uploadModalBody" class="modal-body"> <div id="uploadModalBody" class="modal-body">
<div id="uploadSuccessAlert" class="alert alert-success" role="alert" style="display: none"> <div id="uploadSuccessAlert" class="alert alert-success" role="alert" style="display: none">
@ -493,7 +533,9 @@
<span class="uploadItemTitle mr-3"></span> <span class="uploadItemTitle mr-3"></span>
<span class="uploadItemError text-danger"></span> <span class="uploadItemError text-danger"></span>
<div class="progress" style="margin-top: 5px; height: 10px;"> <div class="progress" style="margin-top: 5px; height: 10px;">
<div class="uploadProgress progress-bar pr-2" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 100%; text-align: right; transform: translateX(-100%);"></div> <div class="uploadProgress progress-bar pr-2" role="progressbar" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"
style="width: 100%; text-align: right; transform: translateX(-100%);"></div>
</div> </div>
</div> </div>
</div> </div>
@ -501,7 +543,9 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="uploadClose" class="btn btn-success" data-dismiss="modal"> <button type="button" id="uploadClose" class="btn btn-success" data-dismiss="modal">
<i class="fas fa-times mr-1"></i> Close</button> <i class="fas fa-times mr-1"></i> Close</button>
<button type="button" id="uploadCancel" class="btn btn-danger" data-toggle="tooltip" data-html="true" title="<strong>Are you really sure?</strong> <br /> Click again to abort uploading."> <button type="button" id="uploadCancel" class="btn btn-danger" data-toggle="tooltip"
data-html="true"
title="<strong>Are you really sure?</strong> <br /> Click again to abort uploading.">
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Cancel</button> <i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Cancel</button>
</div> </div>
</div> </div>