From ff5b1cb1eefc3bd0f71ff91f008c0baa61d226fc Mon Sep 17 00:00:00 2001 From: Tyler Vigario Date: Wed, 25 Nov 2020 06:08:12 -0800 Subject: [PATCH] Upgrade web assets (#219) * Update assets * Upgrade linting and other improvments * Correct linting * Correction and type check improvements * Correct type check lib * Fix lint pathing for VSCode * Remove duplicate babel config * Remove editorconfig root attribute from web subdir * Use double quotes around message * Simplify ESLint config * Update web assets * Allow AMD loader in WebPack * Bump web dependencies * Only include FA icons in-use --- configuration.default.ini | 8 +- configuration.example.ini | 2 +- web/.editorconfig | 2 - web/.eslintrc.json | 30 +- web/.gitattributes | 1 + web/.gitignore | 3 +- web/babel.config.json | 5 + web/js/app.mjs | 20 +- web/js/lib/text.mjs | 42 + web/js/{ => lib}/theme.mjs | 2 +- web/js/lib/type.mjs | 65 + web/js/{util.js => lib/util.mjs} | 42 +- web/js/main.mjs | 292 +- web/package-lock.json | 13661 +++++++++++++++++------------ web/package.json | 55 +- web/vscode.eslintrc.json | 7 + web/webpack.config.cjs | 30 +- 17 files changed, 8520 insertions(+), 5747 deletions(-) create mode 100644 web/.gitattributes create mode 100644 web/babel.config.json create mode 100644 web/js/lib/text.mjs rename web/js/{ => lib}/theme.mjs (92%) create mode 100644 web/js/lib/type.mjs rename web/js/{util.js => lib/util.mjs} (60%) create mode 100644 web/vscode.eslintrc.json diff --git a/configuration.default.ini b/configuration.default.ini index fc5c39ec..f26a266b 100644 --- a/configuration.default.ini +++ b/configuration.default.ini @@ -28,7 +28,7 @@ certificate = [bot] username = botamusique -comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun! +comment = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!" # default volume from 0 to 1. volume = 0.1 stereo = True @@ -126,10 +126,10 @@ jazz = http://jazz-wr04.ice.infomaniak.ch/jazz-wr04-128.mp3 "Jazz Yeah !" -# ======================================================== +# ========================================================= # WARNING: WE DO NOT SUGGEST YOU MODIFY THE FOLLOWING -# PARTS, EXCEPT YOU KNOW WHAT YOU ARE DOING. -# ======================================================== +# PARTS, EXCEPT IF YOU KNOW WHAT YOU ARE DOING. +# ========================================================= [commands] # This is a list of characters the bot recognizes as command prefix. command_symbol = !:! diff --git a/configuration.example.ini b/configuration.example.ini index 27b68afd..2b5ee62f 100644 --- a/configuration.example.ini +++ b/configuration.example.ini @@ -22,7 +22,7 @@ port = 64738 # 'username' is the user name of the bot. # 'comment' is the comment displayed by the bot. #username = botamusique -#comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun! +#comment = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!" # 'language': Available languages can be found inside lang/ folder. #language=en_US diff --git a/web/.editorconfig b/web/.editorconfig index 29a9bb0c..db4f9c85 100644 --- a/web/.editorconfig +++ b/web/.editorconfig @@ -1,5 +1,3 @@ -root = true - [*] charset = utf-8 insert_final_newline = true diff --git a/web/.eslintrc.json b/web/.eslintrc.json index 2fd2b19b..2d298ca2 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -1,38 +1,40 @@ { + "parser": "@babel/eslint-parser", "env": { "browser": true, "es6": true, "es2017": true, "es2020": true, + "es2021": true, "jquery": true }, "plugins": [ - "node", + "@babel", "import", - "jsdoc" + "jsdoc", + "jquery" ], "extends": [ "eslint:recommended", - "google", - "plugin:node/recommended-module", "plugin:import/errors", "plugin:import/warnings", - "plugin:jsdoc/recommended" + "plugin:jsdoc/recommended", + "plugin:jquery/deprecated" ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parser": "babel-eslint", "rules": { - "require-jsdoc": "off", - "valid-jsdoc": "off", - "jsdoc/require-jsdoc": "off", "max-len": ["warn", { "code": 120 }], + "linebreak-style": "off", + "jsdoc/require-jsdoc": "off", + "import/unambiguous": "error", "import/no-commonjs": "error", "import/no-amd": "error", - "linebreak-style": "off" + "import/no-nodejs-modules": "error", + "import/no-deprecated": "error", + "import/extensions": ["error", "always"], + "import/no-unresolved": ["error", { + "commonjs": true + }] } } diff --git a/web/.gitattributes b/web/.gitattributes new file mode 100644 index 00000000..f8a7cde1 --- /dev/null +++ b/web/.gitattributes @@ -0,0 +1 @@ +package-lock.json text eol=lf diff --git a/web/.gitignore b/web/.gitignore index 40b878db..1436c174 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -1 +1,2 @@ -node_modules/ \ No newline at end of file +!* +node_modules/ diff --git a/web/babel.config.json b/web/babel.config.json new file mode 100644 index 00000000..db848145 --- /dev/null +++ b/web/babel.config.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "@babel/plugin-proposal-class-properties" + ] +} diff --git a/web/js/app.mjs b/web/js/app.mjs index b1725439..4506030a 100644 --- a/web/js/app.mjs +++ b/web/js/app.mjs @@ -1,13 +1,23 @@ -import {library, dom} from '@fortawesome/fontawesome-svg-core/index.es'; -import {fas} from '@fortawesome/free-solid-svg-icons/index.es'; -import {far} from '@fortawesome/free-regular-svg-icons/index.es'; -library.add(fas, far); +import {library, dom} from '@fortawesome/fontawesome-svg-core/index.es.js'; +import { + faTimesCircle, faPlus, faCheck, faUpload, faTimes, faTrash, faPlay, faPause, faFastForward, faPlayCircle, faLightbulb, + faTrashAlt, faDownload, faSyncAlt, faEdit, faVolumeUp, faVolumeDown, faRobot, faRedo, faRandom, faTasks +} from '@fortawesome/free-solid-svg-icons/index.es.js'; +import {faFileAlt} from '@fortawesome/free-regular-svg-icons/index.es.js'; + +library.add( + // Solid + faTimesCircle, faPlus, faCheck, faUpload, faTimes, faTrash, faPlay, faPause, faFastForward, faPlayCircle, faLightbulb, + faTrashAlt, faDownload, faSyncAlt, faEdit, faVolumeUp, faVolumeDown, faRobot, faRedo, faRandom, faTasks, + // Regular + faFileAlt +); // Old application code import './main.mjs'; // New application code -import Theme from './theme.mjs'; +import Theme from './lib/theme.mjs'; document.addEventListener('DOMContentLoaded', () => { Theme.init(); diff --git a/web/js/lib/text.mjs b/web/js/lib/text.mjs new file mode 100644 index 00000000..6ee3f9be --- /dev/null +++ b/web/js/lib/text.mjs @@ -0,0 +1,42 @@ +import {validateString, validateNumber} from './type.mjs'; + +/** + * Truncate string length by characters. + * + * @param {string} text String to format. + * @param {number} limit Maximum number of characters in resulting string. + * @param {string} ending Ending to use if string is trucated. + * + * @returns {string} Formatted string. + */ +export function limitChars(text, limit = 50, ending = '...') { + validateString(text); + validateNumber(limit); + validateString(ending); + + // Check if string is already below limit + if (text.length <= limit) { + return text; + } + + // Limit string length by characters + return text.substring(0, limit - ending.length) + ending; +} + +/** + * Truncate string length by words. + * + * @param {string} text String to format. + * @param {number} limit Maximum number of words in resulting string. + * @param {string} ending Ending to use if string is trucated. + * + * @returns {string} Formatted string. + */ +export function limitWords(text, limit = 10, ending = '...') { + validateString(text); + validateNumber(limit); + validateString(ending); + + // Limit string length by words + return text.split(' ').splice(0, limit).join(' ') + ending; +} diff --git a/web/js/theme.mjs b/web/js/lib/theme.mjs similarity index 92% rename from web/js/theme.mjs rename to web/js/lib/theme.mjs index f83f921a..70294d6f 100644 --- a/web/js/theme.mjs +++ b/web/js/lib/theme.mjs @@ -1,6 +1,6 @@ export default class { /** - * @property {boolean} #dark Interal state for dark theme activation. + * @property {boolean} dark Interal state for dark theme activation. * @private */ static #dark = false; diff --git a/web/js/lib/type.mjs b/web/js/lib/type.mjs new file mode 100644 index 00000000..6429dd89 --- /dev/null +++ b/web/js/lib/type.mjs @@ -0,0 +1,65 @@ +/** + * Checks if `value` is the type `Object` excluding `Function` and `null` + * + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, otherwise `false`. + */ +export function isObject(value) { + return (Object.prototype.toString.call(value) === '[object Object]'); +} + +/** + * Checks if `value` is the type `string` + * + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a string, otherwise `false`. + */ +export function isString(value) { + return (typeof value === 'string'); +} + +/** + * Checks if `value` is the type `number` + * + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a number, otherwise `false`. + */ +export function isNumber(value) { + return (typeof value === 'number'); +} + +/** + * Validate parameter is of type object. + * + * @param {string} value Variable to validate. + * @throws Error if not an object. + */ +export function validateObject(value) { + if (!isObject(value)) { + throw new TypeError('Parameter "value" must be of type object.'); + } +} + +/** + * Validate parameter is of type string. + * + * @param {string} value Variable to validate. + * @throws Error if not an string. + */ +export function validateString(value) { + if (!isString(value)) { + throw new TypeError('Parameter "value" must be of type string.'); + } +} + +/** + * Validate parameter is of type number. + * + * @param {number} value Variable to validate. + * @throws Error if not an number. + */ +export function validateNumber(value) { + if (!isNumber(value)) { + throw new TypeError('Parameter "value" must be of type number.'); + } +} diff --git a/web/js/util.js b/web/js/lib/util.mjs similarity index 60% rename from web/js/util.js rename to web/js/lib/util.mjs index b5b2b1c4..4bd72211 100644 --- a/web/js/util.js +++ b/web/js/lib/util.mjs @@ -3,13 +3,13 @@ export function isOverflown(element) { } export function hash(string) { - if (typeof string != "string") return 0; + if (typeof string != 'string') return 0; let hash = 0; if (string.length === 0) { return hash; } for (let i = 0; i < string.length; i++) { - let char = string.charCodeAt(i); + const char = string.charCodeAt(i); hash = ((hash<<5)-hash)+char; hash = hash & hash; // Convert to 32bit integer } @@ -17,24 +17,25 @@ export function hash(string) { } export function getColor(string) { - let num = hash(string) % 8; - switch(num) { + const num = hash(string) % 8; + + switch (num) { case 0: - return "primary"; + return 'primary'; case 1: - return "secondary"; + return 'secondary'; case 2: - return "success"; + return 'success'; case 3: - return "danger"; + return 'danger'; case 4: - return "warning"; + return 'warning'; case 5: - return "info"; + return 'info'; case 6: - return "light"; + return 'light'; case 7: - return "dark"; + return 'dark'; } } @@ -52,20 +53,3 @@ export function secondsToStr(seconds) { const secs = seconds % 60; 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; -} diff --git a/web/js/main.mjs b/web/js/main.mjs index b6e51efe..c82acc68 100644 --- a/web/js/main.mjs +++ b/web/js/main.mjs @@ -1,20 +1,20 @@ -import 'jquery/src/jquery'; -import 'jquery-migrate/src/migrate'; -import Popper from 'popper.js/dist/esm/popper'; +import 'jquery/src/jquery.js'; +import 'jquery-migrate/src/migrate.js'; +import Popper from 'popper.js/dist/esm/popper.js'; import { Modal, Toast, Tooltip, -} from 'bootstrap/js/src/index'; +} from 'bootstrap/js/src/index.js'; import { getColor, isOverflown, setProgressBar, secondsToStr, - coverArtString, -} from './util'; +} from './lib/util.mjs'; +import {limitChars} from './lib/text.mjs'; -$('#uploadSelectFile').on('change', function () { +$('#uploadSelectFile').on('change', function() { // get the file name const fileName = $(this).val().replace('C:\\fakepath\\', ' '); // replace the "Choose a file" label @@ -85,7 +85,7 @@ fastForwardBtn.on('click', () => { }); document.getElementById('clear-playlist-btn').addEventListener('click', () => { - request('post', { action: 'clear' }); + request('post', {action: 'clear'}); }); // eslint-disable-next-line guard-for-in @@ -102,14 +102,14 @@ function request(_url, _data, refresh = false) { url: _url, data: _data, statusCode: { - 200: function (data) { + 200: function(data) { if (data.ver !== playlist_ver) { checkForPlaylistUpdate(); } updateControls(data.empty, data.play, data.mode, data.volume); updatePlayerPlayhead(data.playhead); }, - 403: function () { + 403: function() { location.reload(true); }, }, @@ -125,7 +125,7 @@ function addPlaylistItem(item) { pl_title_element.html(item.title); pl_artist_element.html(item.artist); pl_thumb_element.attr('src', item.thumbnail); - pl_thumb_element.attr('alt', coverArtString(item.title)); + pl_thumb_element.attr('alt', limitChars(item.title)); pl_type_element.html(item.type); pl_path_element.html(item.path); @@ -137,13 +137,13 @@ function addPlaylistItem(item) { tags.empty(); 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); }); tag_edit_copy.appendTo(tags); if (item.tags.length > 0) { - item.tags.forEach(function (tag_tuple) { + item.tags.forEach(function(tag_tuple) { const tag_copy = tag_element.clone(); tag_copy.html(tag_tuple[0]); tag_copy.addClass('badge-' + tag_tuple[1]); @@ -160,14 +160,14 @@ function addPlaylistItem(item) { function displayPlaylist(data) { playlist_table.animate({ opacity: 0, - }, 200, function () { + }, 200, function() { playlist_loading.hide(); $('.playlist-item').remove(); const items = data.items; const length = data.length; - if (items.length === 0){ + if (items.length === 0) { playlist_empty.removeClass('d-none'); - playlist_table.animate({ opacity: 1 }, 200); + playlist_table.animate({opacity: 1}, 200); return; } playlist_items = {}; @@ -188,9 +188,9 @@ function displayPlaylist(data) { } items.forEach( - function (item) { - addPlaylistItem(item); - }, + function(item) { + addPlaylistItem(item); + }, ); if (items.length < length && start_from + items.length < length) { @@ -205,7 +205,7 @@ function displayPlaylist(data) { displayActiveItem(data.current_index); updatePlayerInfo(playlist_items[data.current_index]); bindPlaylistEvent(); - playlist_table.animate({ opacity: 1 }, 200); + playlist_table.animate({opacity: 1}, 200); }); } @@ -227,7 +227,7 @@ function insertExpandPrompt(real_from, real_to, display_from, display_to, total_ expand_copy.addClass('playlist-item'); expand_copy.appendTo(playlist_table); - expand_copy.click(function () { + expand_copy.click(function() { playlist_range_from = real_from; playlist_range_to = real_to; updatePlaylist(); @@ -237,7 +237,7 @@ function insertExpandPrompt(real_from, real_to, display_from, display_to, total_ function updatePlaylist() { playlist_table.animate({ opacity: 0, - }, 200, function () { + }, 200, function() { playlist_empty.addClass('d-none'); playlist_loading.show(); playlist_table.find('.playlist-item').css('opacity', 0); @@ -267,7 +267,7 @@ function checkForPlaylistUpdate() { type: 'POST', url: 'post', statusCode: { - 200: function (data) { + 200: function(data) { if (data.ver !== playlist_ver) { playlist_ver = data.ver; playlist_range_from = 0; @@ -298,18 +298,18 @@ function checkForPlaylistUpdate() { function bindPlaylistEvent() { $('.playlist-item-play').unbind().click( - function (e) { - request('post', { - 'play_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1), - }); - }, + function(e) { + request('post', { + 'play_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1), + }); + }, ); $('.playlist-item-trash').unbind().click( - function (e) { - request('post', { - 'delete_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1), - }); - }, + function(e) { + request('post', { + 'delete_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1), + }); + }, ); } @@ -416,10 +416,10 @@ function setFilterType(event, type) { } -filter_dir.change(function () { +filter_dir.change(function() { updateResults(); }); -filter_keywords.change(function () { +filter_keywords.change(function() { updateResults(); }); @@ -427,69 +427,69 @@ const item_template = $('#library-item'); function bindLibraryResultEvent() { $('.library-thumb-col').unbind().hover( - function (e) { - $(e.currentTarget).find('.library-thumb-grp').addClass('library-thumb-grp-hover'); - }, - function (e) { - $(e.currentTarget).find('.library-thumb-grp').removeClass('library-thumb-grp-hover'); - }, + function(e) { + $(e.currentTarget).find('.library-thumb-grp').addClass('library-thumb-grp-hover'); + }, + function(e) { + $(e.currentTarget).find('.library-thumb-grp').removeClass('library-thumb-grp-hover'); + }, ); $('.library-info-title').unbind().hover( - function (e) { - $(e.currentTarget).parent().find('.library-thumb-grp').addClass('library-thumb-grp-hover'); - }, - function (e) { - $(e.currentTarget).parent().find('.library-thumb-grp').removeClass('library-thumb-grp-hover'); - }, + function(e) { + $(e.currentTarget).parent().find('.library-thumb-grp').addClass('library-thumb-grp-hover'); + }, + function(e) { + $(e.currentTarget).parent().find('.library-thumb-grp').removeClass('library-thumb-grp-hover'); + }, ); $('.library-item-play').unbind().click( - function (e) { - request('post', { - 'add_item_at_once': $(e.currentTarget).parent().parent().parent().find('.library-item-id').val(), - }); - }, + function(e) { + request('post', { + 'add_item_at_once': $(e.currentTarget).parent().parent().parent().find('.library-item-id').val(), + }); + }, ); $('.library-item-trash').unbind().click( - function (e) { - request('post', { - 'delete_item_from_library': $(e.currentTarget).parent().parent().find('.library-item-id').val(), - }); - updateResults(active_page); - }, + function(e) { + request('post', { + 'delete_item_from_library': $(e.currentTarget).parent().parent().find('.library-item-id').val(), + }); + updateResults(active_page); + }, ); $('.library-item-download').unbind().click( - function (e) { - const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); - // window.open('/download?id=' + id); - downloadId(id); - }, + function(e) { + const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); + // window.open('/download?id=' + id); + downloadId(id); + }, ); $('.library-item-add-next').unbind().click( - function (e) { - const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); - request('post', { - 'add_item_next': id, - }); - }, + function(e) { + const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); + request('post', { + 'add_item_next': id, + }); + }, ); $('.library-item-add-bottom').unbind().click( - function (e) { - const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); - request('post', { - 'add_item_bottom': id, - }); - }, + function(e) { + const id = $(e.currentTarget).parent().parent().find('.library-item-id').val(); + request('post', { + 'add_item_bottom': id, + }); + }, ); } -const lib_filter_tag_group = $("#filter-tags"); -const lib_filter_tag_element = $(".filter-tag"); +const lib_filter_tag_group = $('#filter-tags'); +const lib_filter_tag_element = $('.filter-tag'); const lib_group = $('#library-group'); const id_element = $('.library-item-id'); @@ -503,62 +503,62 @@ const tag_edit_element = $('.library-item-edit'); // var notag_element = $(".library-item-notag"); // var tag_element = $(".library-item-tag"); -let library_tags = []; +const library_tags = []; -function updateLibraryControls(){ +function updateLibraryControls() { $.ajax({ type: 'GET', url: 'library/info', statusCode: { 200: displayLibraryControls, - 403: function () { + 403: function() { location.reload(true); }, }, }); } -function displayLibraryControls(data){ - $("#maxUploadFileSize").val(data.max_upload_file_size); +function displayLibraryControls(data) { + $('#maxUploadFileSize').val(data.max_upload_file_size); if (data.upload_enabled) { - $("#uploadDisabled").val("false"); - $("#upload").show(); + $('#uploadDisabled').val('false'); + $('#upload').show(); } else { - $("#uploadDisabled").val("true"); - $("#upload").hide(); + $('#uploadDisabled').val('true'); + $('#upload').hide(); } if (data.delete_allowed) { - $("#deleteAllowed").val("true"); - $(".library-delete").show(); + $('#deleteAllowed').val('true'); + $('.library-delete').show(); } else { - $("#uploadDisabled").val("false"); - $(".library-delete").hide(); + $('#uploadDisabled').val('false'); + $('.library-delete').hide(); } - let select = $("#filter-dir"); - let dataList = $("#upload-target-dirs"); - select.find("option").remove(); - dataList.find("option").remove(); + const select = $('#filter-dir'); + const dataList = $('#upload-target-dirs'); + select.find('option').remove(); + dataList.find('option').remove(); if (data.dirs.length > 0) { console.log(data.dirs); - data.dirs.forEach(function (dir) { - $("").appendTo(select); - $("').appendTo(select); + $('