"use strict";
// SPDX-License-Identifier: GPL-3.0-or-later
// myMPD (c) 2018-2024 Juergen Mang <mail@jcgames.de>
// https://github.com/jcorporation/mympd
/** @module utility_js */
/**
* Ignore keys for inputs
* @param {KeyboardEvent} event triggering key event
* @returns {boolean} true if key event should be ignored, else false
*/
function ignoreKeys(event) {
if (event === undefined) {
return true;
}
switch (event.key) {
case undefined:
case 'Unidentified':
return true;
case 'Escape':
// @ts-ignore
event.target.blur();
return true;
case 'Backspace':
case 'Delete':
// do not ignore some special keys
return false;
// No Default
}
if (event.key.length > 1) {
// ignore all special keys
return true;
}
return false;
}
/**
* Checks if event should be executed
* @param {EventTarget} target triggering event target
* @returns {boolean} true if target is clickable else false
*/
function checkTargetClick(target) {
if (target === null ||
target.classList.contains('not-clickable') ||
target.parentNode.nodeName === 'TH')
{
return false;
}
return true;
}
/**
* Sets the updating indicator(s) for a view with the given id
* @param {string} id element id
* @returns {void}
*/
function setUpdateViewId(id) {
setUpdateView(elGetById(id));
}
/**
* Sets the updating indicator(s) for the element
* @param {Element} el element
* @returns {void}
*/
function setUpdateView(el) {
el.classList.add('opacity05');
domCache.main.classList.add('border-progress');
}
/**
* Removes the updating indicator(s) for a view with the given id
* @param {string} id element id
* @returns {void}
*/
function unsetUpdateViewId(id) {
unsetUpdateView(elGetById(id));
}
/**
* Removes the updating indicator(s) for the element
* @param {Element | ParentNode} el element
* @returns {void}
*/
function unsetUpdateView(el) {
el.classList.remove('opacity05');
domCache.main.classList.remove('border-progress');
}
/**
* Custom encoding function, works like encodeURIComponent but
* - does not escape /
* - escapes further reserved characters
* @param {string} str string to encode
* @returns {string} the encoded string
*/
function myEncodeURI(str) {
return encodeURI(str).replace(/[!'()*#?;:,@&=+$~]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
/**
* Custom encoding function, works like encodeURIComponent but
* escapes further reserved characters
* @param {string} str string to encode
* @returns {string} the encoded string
*/
function myEncodeURIComponent(str) {
return encodeURIComponent(str).replace(/[!'()*~]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
/**
* Concatenates two arrays and checks if second array is defined
* @param {Array} c1 first array
* @param {Array} c2 second array, can be undefined
* @returns {Array} concatenated array
*/
function concatArrays(c1, c2) {
return c2 === undefined
? c1
: c1.concat(c2);
}
/**
* Joins an array to a comma separated text
* @param {Array} a array to join
* @returns {string} joined array
*/
function joinArray(a) {
return a === undefined
? ''
: a.join(', ');
}
/**
* Joins an array to a multi-line string
* @param {Array} a array to join
* @returns {string} joined array
*/
function arrayToLines(a) {
return a === undefined
? ''
: a.join('\n');
}
/**
* Parses a comma separated string to an array
* @param {string} str string to parse
* @returns {Array} Parsed string as array
*/
function stringToArray(str) {
const a = str.split(/,/);
for (let i = 0, j=a.length; i < j; i++) {
a[i] = a[i].trim();
}
return a;
}
/**
* Escape a MPD filter value
* @param {string} str value to escape
* @returns {string} escaped value
*/
function escapeMPD(str) {
if (typeof str === 'number') {
return str;
}
return str.replace(/(["'])/g, function(m0, m1) {
switch(m1) {
case '"': return '\\"';
case '\'': return '\\\'';
case '\\': return '\\\\';
// No Default
}
});
}
/**
* Unescape a MPD filter value
* @param {string} str value to unescape
* @returns {string} unescaped value
*/
function unescapeMPD(str) {
if (typeof str === 'number') {
return str;
}
return str.replace(/(\\'|\\"|\\\\)/g, function(m0, m1) {
switch(m1) {
case '\\"': return '"';
case '\\\'': return '\'';
case '\\\\': return '\\';
// No Default
}
});
}
/**
* Pads a number with zeros
* @param {number} num number to pad
* @param {number} places complete width
* @returns {string} padded number
*/
function zeroPad(num, places) {
const zero = places - num.toString().length + 1;
return Array(+(zero > 0 && zero)).join("0") + num;
}
/**
* Gets the directory from the given uri
* @param {string} uri the uri
* @returns {string} directory part of the uri
*/
function dirname(uri) {
return uri.replace(/\/[^/]*$/, '');
}
/**
* Gets the filename from the given uri
* @param {string} uri the uri
* @param {boolean} removeQuery true = remove query string or hash
* @returns {string} filename part of the uri
*/
function basename(uri, removeQuery) {
if (removeQuery === true) {
return uri.split('/').reverse()[0].split(/[?#]/)[0];
}
return uri.split('/').reverse()[0];
}
/**
* Splits a string in path + filename and extension
* @param {string} filename filename to split
* @returns {object} object with file and ext keys
*/
function splitFilename(filename) {
const parts = filename.match(/^(.*)\.([^.]+)$/);
return {
"file": parts[1],
"ext": parts[2]
};
}
/**
* Returns a description of the filetype from uri
* @param {string} uri the uri
* @param {boolean} long return long description?
* @returns {string} description of filetype
*/
function filetype(uri, long) {
if (uri === undefined) {
return '';
}
if (isStreamUri(uri) === true) {
return 'Stream';
}
const ext = uri.split('.').pop().toUpperCase();
if (long === false) {
return ext;
}
switch(ext) {
case 'MP3': return ext + smallSpace + nDash + smallSpace + tn('MPEG-1 Audio Layer III');
case 'FLAC': return ext + smallSpace + nDash + smallSpace + tn('Free Lossless Audio Codec');
case 'OGG': return ext + smallSpace + nDash + smallSpace + tn('Ogg Vorbis');
case 'OPUS': return ext + smallSpace + nDash + smallSpace + tn('Opus Audio');
case 'WAV': return ext + smallSpace + nDash + smallSpace + tn('WAVE Audio File');
case 'WV': return ext + smallSpace + nDash + smallSpace + tn('WavPack');
case 'AAC': return ext + smallSpace + nDash + smallSpace + tn('Advanced Audio Coding');
case 'MPC': return ext + smallSpace + nDash + smallSpace + tn('Musepack');
case 'MP4': return ext + smallSpace + nDash + smallSpace + tn('MPEG-4');
case 'APE': return ext + smallSpace + nDash + smallSpace + tn('Monkey Audio');
case 'WMA': return ext + smallSpace + nDash + smallSpace + tn('Windows Media Audio');
case 'CUE': return ext + smallSpace + nDash + smallSpace + tn('Cuesheet');
default: return ext;
}
}
/**
* View specific focus of the search input
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function focusSearch() {
const searchInput = elGetById(app.id + 'SearchStr');
if (searchInput !== null) {
searchInput.focus();
}
else {
appGoto('Search');
}
}
/**
* Parses a string to a javascript command object
* @param {Event} event triggering event
* @param {string} str string to parse
* @returns {void}
*/
function parseCmdFromJSON(event, str) {
const cmd = JSON.parse(str);
parseCmd(event, cmd);
}
/**
* Executes a javascript command object
* @param {Event} event triggering event
* @param {object} cmd cmd object
* @returns {void}
*/
function parseCmd(event, cmd) {
if (event !== null &&
event !== undefined)
{
event.preventDefault();
}
const func = getFunctionByName(cmd.cmd);
if (typeof func === 'function') {
if (cmd.cmd === 'sendAPI') {
sendAPI(cmd.options[0].cmd, {}, null, false);
}
else {
// copy - we do not want to modify the original object
const options = cmd.options.slice();
for (let i = 0, j = options.length; i < j; i++) {
if (options[i] === 'event') {
options[i] = event;
}
else if (options[i] === 'target') {
options[i] = event.target;
}
}
// @ts-ignore
func(... options);
}
}
else {
logError('Can not execute cmd: ' + cmd.cmd);
}
}
/**
* Returns the function by name
* @param {string} functionName name of the function
* @returns {Function} the function
*/
function getFunctionByName(functionName) {
const namespace = functionName.split('.');
if (namespace.length === 2) {
const context = namespace.shift();
const functionToExecute = namespace.shift();
return window[context][functionToExecute];
}
return window[functionName];
}
/**
* Checks for support of the media session api
* @returns {boolean} true if media session api is supported, else false
*/
function checkMediaSessionSupport() {
if (settings.mediaSession === false ||
navigator.mediaSession === undefined)
{
return false;
}
return true;
}
/**
* Uppercases the first letter of a word
* @param {string} word word to uppercase first letter
* @returns {string} changed word
*/
function ucFirst(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
/**
* Returns the cuesheet name
* @param {string} uri uri to check
* @returns {string} the uri part
*/
function cuesheetUri(uri) {
const cuesheet = uri.match(/^(.*\.cue)\/(track\d+)$/);
if (cuesheet !== null) {
return cuesheet[1];
}
return uri;
}
/**
* Returns the cuesheet track name
* @param {string} uri uri to check
* @returns {string} the track part
*/
function cuesheetTrack(uri) {
const cuesheet = uri.match(/^(.*\.cue)\/(track\d+)$/);
if (cuesheet !== null) {
return cuesheet[2];
}
return '';
}
/**
* Sets the viewport tag scaling option
* @returns {void}
*/
function setViewport() {
document.querySelector("meta[name=viewport]").setAttribute('content', 'width=device-width, initial-scale=' +
localSettings.scaleRatio + ', maximum-scale=' + localSettings.scaleRatio);
}
/**
* Sets the height of the container for scrolling
* @param {HTMLElement} container scrolling container element
* @returns {void}
*/
function setScrollViewHeight(container) {
if (userAgentData.isMobile === true) {
//no scrolling container in the mobile view
container.parentNode.style.maxHeight = '';
return;
}
const footerHeight = domCache.footer.offsetHeight;
const tpos = getYpos(container.parentNode);
const maxHeight = window.innerHeight - tpos - footerHeight;
container.parentNode.style.maxHeight = maxHeight + 'px';
}
/**
* Enables the mobile view for specific user agents
* @returns {void}
*/
function setMobileView() {
if (userAgentData.isMobile === true) {
setViewport();
domCache.body.classList.remove('not-mobile');
domCache.body.classList.add('mobile');
}
else {
domCache.body.classList.remove('mobile');
domCache.body.classList.add('not-mobile');
}
}
/**
* Generic http get request (async function)
* @param {string} uri uri for the request
* @param {Function} callback callback function
* @param {boolean} json true = parses the response as json, else pass the plain text response
* @returns {Promise<void>}
*/
async function httpGet(uri, callback, json) {
let response = null;
try {
response = await fetch(uri, {
method: 'GET',
mode: 'same-origin',
credentials: 'same-origin',
cache: 'no-store',
redirect: 'follow'
});
}
catch(error) {
showNotification(tn('API error') + ':\n' + tn('Error accessing %{uri}', {"uri": uri}), 'general', 'error');
logError('Error posting to ' + uri);
logError(error);
callback(null);
return;
}
if (response.redirected === true) {
logError('Request was redirect, reloading application');
window.location.reload();
return;
}
if (response.ok === false) {
showNotification(tn('API error') + '\n' +
tn('Error accessing %{uri}', {"uri": uri}) + ',\n' +
tn('Response code: %{code}', {"code": response.status + ' - ' + response.statusText}),
'general', 'error');
logError('Error accessing ' + uri + ', code ' + response.status + ' - ' + response.statusText);
callback(null);
return;
}
let data;
try {
data = json === true
? await response.json()
: await response.text();
}
catch(error) {
showNotification(tn('API error') + '\n' + tn('Can not parse response from %{uri}', {"uri": uri}), 'general', 'error');
logError('Can not parse response from ' + uri);
logError(error);
callback(null);
return;
}
callback(data);
}
/**
* Returns the myMPD uri calculated from the window location
* @param {string} [proto] protocol to return, allowed: http or ws
* @returns {string} myMPD uri
*/
function getMyMPDuri(proto) {
const protocol = proto === 'ws'
? window.location.protocol === 'https:'
? 'wss:'
: 'ws:'
: window.location.protocol;
return protocol + '//' + window.location.hostname +
(window.location.port !== '' ? ':' + window.location.port : '') +
subdir;
}
/**
* Parses a string to seconds
* @param {string} value [hh:][mm:]ss value to parse
* @returns {number} value in seconds
*/
function parseToSeconds(value) {
let match = value.match(/(\d+):(\d+):(\d+)/);
if (match) {
return Number(match[1]) * 60 * 60 +
Number(match[2]) * 60 +
Number(match[3]);
}
match = value.match(/(\d+):(\d+)/);
if (match) {
return Number(match[1]) * 60 +
Number(match[2]);
}
return Number(value);
}
/**
* Initializes elements with data-href attribute
* @param {Node} root root of the elements to initialize
* @returns {void}
*/
function initLinks(root) {
const hrefs = root.querySelectorAll('[data-href]');
for (const href of hrefs) {
if (href.nodeName !== 'A' &&
href.nodeName !== 'BUTTON' &&
href.classList.contains('not-clickable') === false)
{
href.classList.add('clickable');
}
if (href.parentNode.classList.contains('noInitChilds') ||
href.parentNode.parentNode.classList.contains('noInitChilds'))
{
//handler on parentnode
continue;
}
href.addEventListener('click', function(event) {
parseCmdFromJSON(event, getData(this, 'href'));
}, false);
}
}
/**
* Tries to convert a string to number or bool
* @param {string} str string to convert
* @returns {string|number|boolean} parsed string
*/
function convertType(str) {
if (str === 'true') {
return true;
}
if (str === 'false') {
return false;
}
if (str.match(/^(-)?[\d.]+$/)) {
return Number(str);
}
return str;
}
/**
* Gets a unix timestamp
* @returns {number} the unix timestamp
*/
function getTimestamp() {
return Math.floor(Date.now() / 1000);
}
/**
* Parses a YYYY-MM-DD string to unix timestamp
* @param {string} value string to parses
* @returns {number} unix timestamp
*/
function parseDateFromText(value) {
const m = value.match(/(\d{4})-(\d{2})-(\d{2})/);
if (m !== null) {
return Date.parse(value) / 1000;
}
return NaN;
}