569 lines
13 KiB
JavaScript
Executable File
569 lines
13 KiB
JavaScript
Executable File
|
|
|
|
/**
|
|
* Returns a function, that, as long as it continues to be invoked, will not
|
|
* be triggered. The function will be called after it stops being called for
|
|
* N milliseconds. If `immediate` is passed, trigger the function on the
|
|
* leading edge, instead of the trailing.
|
|
* */
|
|
const debounce = function (fn, wait, immediate) {
|
|
let timeout;
|
|
return function () {
|
|
const context = this; const
|
|
args = arguments;
|
|
|
|
const later = function () {
|
|
timeout = null;
|
|
if (!immediate) {
|
|
fn.apply(context, args);
|
|
}
|
|
};
|
|
|
|
const callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
|
|
if (callNow) {
|
|
fn.apply(context, args);
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
const throttle = function (fn, delay) {
|
|
let lastCall = 0;
|
|
return function (...args) {
|
|
const now = (new Date()).getTime();
|
|
if (now - lastCall < delay) {
|
|
return;
|
|
}
|
|
lastCall = now;
|
|
return fn(...args);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Set the app's favicon to a specific image.
|
|
*
|
|
* @param filename String
|
|
*/
|
|
const setFavicon = function (filename) {
|
|
const links = document.getElementsByClassName('favicon');
|
|
for (const link of links) {
|
|
// Construct new <links>
|
|
const new_link = document.createElement('link');
|
|
new_link.className = link.className;
|
|
new_link.rel = link.rel;
|
|
new_link.href = `/iris/assets/${filename}`;
|
|
if (link.type) {
|
|
new_link.type = link.type;
|
|
}
|
|
|
|
// Remove the old one and add the new one
|
|
document.head.removeChild(link);
|
|
document.head.appendChild(new_link);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Digest a react-router's location.search string into an array of values
|
|
*
|
|
* @param key String = the key you want from the URL
|
|
* @param string String = the locaion.search string
|
|
*/
|
|
const queryString = (key, string, compact = true) => {
|
|
const elements = string.replace('?','').split('&');
|
|
const results = elements.reduce((accumulator, current) => {
|
|
const subElements = current.split('=');
|
|
let results = [];
|
|
|
|
// We decode the URI, but also treat "+" as a space. This is needed for backend CGI.encode that
|
|
// happens when redirecting from an OAuth failure.
|
|
if (subElements[0] === key) {
|
|
results = subElements[1].split(',').map(
|
|
item => decodeURIComponent(item.replace(/\+/g, '%20')),
|
|
);
|
|
}
|
|
return [...accumulator, ...results];
|
|
}, []);
|
|
|
|
if (compact && results.length === 1) return results[0];
|
|
if (compact && results.length === 0) return null;
|
|
return results;
|
|
};
|
|
|
|
|
|
|
|
const generateGuid = function (type = 'numeric', length = 12) {
|
|
// numeric
|
|
if (type == 'numeric') {
|
|
const date = new Date().valueOf().toString();
|
|
const random_number = Math.floor((Math.random() * 100)).toString();
|
|
return parseInt(date + random_number);
|
|
}
|
|
const format = 'x'.repeat(length);
|
|
return format.replace(/[xy]/g, (c) => {
|
|
const r = Math.random() * 16 | 0; const
|
|
v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(length);
|
|
});
|
|
};
|
|
|
|
const getCurrentPusherConnection = function (connections, connectionid) {
|
|
function isCurrentConnection(connection) {
|
|
return connection.connectionid == newProps.pusher.connectionid;
|
|
}
|
|
|
|
const currentConnection = newProps.pusher.connections.find(isCurrentConnection);
|
|
if (!currentConnection) return false;
|
|
|
|
return currentConnection;
|
|
};
|
|
|
|
/**
|
|
* Figure out a URI's source namespace
|
|
* @param uri = string
|
|
* */
|
|
let uriSource = function (uri) {
|
|
if (!uri) {
|
|
return false;
|
|
}
|
|
const exploded = uri.split(':');
|
|
return exploded[0];
|
|
};
|
|
/**
|
|
* Identify what kind of asset a URI is (playlist, album, etc)
|
|
*
|
|
* @param uri = string
|
|
* @return string
|
|
* */
|
|
const uriType = function (uri) {
|
|
if (!uri) return null;
|
|
|
|
const exploded = uri.split(':');
|
|
|
|
if (exploded[0] === 'm3u') {
|
|
return 'playlist';
|
|
}
|
|
|
|
if (exploded[0] === 'iris') {
|
|
return exploded[1];
|
|
}
|
|
|
|
switch (exploded[1]) {
|
|
case 'track':
|
|
case 'artist':
|
|
case 'album':
|
|
case 'playlist':
|
|
case 'genre':
|
|
return exploded[1];
|
|
case 'user':
|
|
if (exploded.length > 3 && exploded[3] == 'playlist') {
|
|
return 'playlist';
|
|
}
|
|
return exploded[1];
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
|
|
const sourceIcon = function (uri, source = null) {
|
|
if (uri) source = uriSource(uri);
|
|
switch (source) {
|
|
case 'local':
|
|
case 'm3u':
|
|
return 'folder';
|
|
|
|
case 'gmusic':
|
|
return 'google';
|
|
|
|
case 'podcast':
|
|
case 'podcast+file':
|
|
case 'podcast+http':
|
|
case 'podcast+https':
|
|
case 'podcast+itunes':
|
|
return 'podcast';
|
|
|
|
case 'tunein':
|
|
case 'somafm':
|
|
case 'dirble':
|
|
return 'microphone';
|
|
|
|
default:
|
|
return source;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Get an element from a URI
|
|
* @param element = string, the element we wish to extract
|
|
* @param uri = string
|
|
* */
|
|
const getFromUri = function (element, uri = '') {
|
|
const exploded = uri.split(':');
|
|
const namespace = exploded[0];
|
|
|
|
switch (element) {
|
|
case 'mbid':
|
|
var index = exploded.indexOf('mbid');
|
|
if (index > -1) return exploded[index + 1];
|
|
break;
|
|
|
|
case 'artistid':
|
|
if (exploded[1] == 'artist') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'albumid':
|
|
if (exploded[1] == 'album') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'playlistid':
|
|
if (exploded[1] == 'playlist') {
|
|
return exploded[2];
|
|
} if (exploded[1] == 'user' && exploded[3] == 'playlist') {
|
|
return exploded[4];
|
|
}
|
|
break;
|
|
|
|
case 'playlistowner':
|
|
if (exploded[1] == 'user' && exploded[3] == 'playlist') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'trackid':
|
|
if (exploded[1] == 'track') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'userid':
|
|
if (exploded[1] == 'user') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'genreid':
|
|
if (exploded[1] == 'genre') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'seeds':
|
|
if (exploded[1] == 'discover') {
|
|
return exploded[2];
|
|
} else if (exploded[1] === 'radio') {
|
|
const seeds = exploded[2].split(',');
|
|
return seeds.map(seed => seed.replace(/_/gi,':'));
|
|
}
|
|
break;
|
|
|
|
case 'searchtype':
|
|
if (exploded[1] == 'search') {
|
|
return exploded[2];
|
|
}
|
|
break;
|
|
|
|
case 'searchterm':
|
|
if (exploded[1] == 'search') {
|
|
return exploded[3];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Build a link to an asset. Using the URI type we can ascertain where we need
|
|
* to direct the user (eg /track/local:track:1235.mp3)
|
|
*
|
|
* @param $uri = String
|
|
* @return String
|
|
* */
|
|
const buildLink = function (uri) {
|
|
// Start the link with the URI type
|
|
const type = uriType(uri);
|
|
let link = `/${type}/`;
|
|
|
|
// Encode the whole URI as though it's a component. This makes it URL friendly for
|
|
// all Mopidy backends (some use URIs like local:track:http://rss.com/stuff.mp3) which
|
|
// is never going to work nicely.
|
|
uri = encodeURIComponent(uri);
|
|
link += uri;
|
|
|
|
return link;
|
|
};
|
|
|
|
/**
|
|
* Detect touch-ability
|
|
*/
|
|
const isTouchDevice = function () {
|
|
return 'ontouchstart' in document.documentElement;
|
|
};
|
|
|
|
/**
|
|
* Figure out if a value is a number
|
|
* @param value = mixed
|
|
* @return boolean
|
|
* */
|
|
const isNumeric = function (value) {
|
|
return !isNaN(parseFloat(value)) && isFinite(value);
|
|
};
|
|
|
|
|
|
/**
|
|
* Figure out if a value is an object
|
|
* @param value = mixed
|
|
* @return boolean
|
|
* */
|
|
let isObject = function (value) {
|
|
return value instanceof Object && value.constructor === Object;
|
|
};
|
|
|
|
|
|
/**
|
|
* Detect if an item is in the loading queue. We simply loop all load items to
|
|
* see if any items contain our searched key.
|
|
*
|
|
* TODO: Explore performance of this
|
|
* TODO: Allow wildcards
|
|
*
|
|
* @param load_queue = obj (passed from store)
|
|
* @param key = string (the string to lookup)
|
|
* @return boolean
|
|
* */
|
|
const isLoading = function (load_queue = [], keys = []) {
|
|
// Loop all of our load queue items
|
|
for (const load_queue_key in load_queue) {
|
|
// Make sure it's not a root object method
|
|
if (load_queue.hasOwnProperty(load_queue_key)) {
|
|
// Loop all the keys we're looking for
|
|
for (let i = 0; i < keys.length; i++) {
|
|
if (load_queue[load_queue_key].includes(keys[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Is this app running from the hosted instance?
|
|
* For example the GitHub-hosted UI
|
|
*
|
|
* @param Array hosts = valid hosted domain names
|
|
* @return Boolean
|
|
* */
|
|
const isHosted = function (hosts = ['jaedb.github.io']) {
|
|
const { hostname } = window.location;
|
|
return hosts.includes(hostname);
|
|
};
|
|
|
|
|
|
/**
|
|
* Get indexed record(s) by URI from our asset index
|
|
*
|
|
* @param store = obj
|
|
* @param uris = mixed (array or string)
|
|
* @return array
|
|
* */
|
|
const getIndexedRecords = function (index, uris) {
|
|
const records = [];
|
|
|
|
// Wrap in array, if we've only got one URI
|
|
if (!(uris instanceof Array)) {
|
|
uris = [uris];
|
|
}
|
|
|
|
for (let i = 0; i < uris.length; i++) {
|
|
if (index.hasOwnProperty(uris[i])) {
|
|
records.push(index[uris[i]]);
|
|
}
|
|
}
|
|
|
|
return records;
|
|
};
|
|
|
|
|
|
/**
|
|
* Uppercase-ify the first character of a string
|
|
*
|
|
* @param string String
|
|
* @return String
|
|
* */
|
|
const titleCase = function (string) {
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
};
|
|
|
|
/**
|
|
* Use a keyword to return an icon name
|
|
*/
|
|
const iconFromKeyword = (name) => {
|
|
const iconWords = [
|
|
{ icon: 'business', words: ['office', 'work'] },
|
|
{ icon: 'king_bed', words: ['bed'] },
|
|
{ icon: 'weekend', words: ['lounge', 'tv', 'sitting room'] },
|
|
{ icon: 'directions_car', words: ['garage', 'basement'] },
|
|
{ icon: 'local_laundry_service', words: ['laundry'] },
|
|
{ icon: 'fitness_center', words: ['gym'] },
|
|
{ icon: 'kitchen', words: ['kitchen'] },
|
|
{ icon: 'deck', words: ['deck', 'outside'] },
|
|
{ icon: 'restaurant_menu', words: ['dining', 'dinner'] },
|
|
{ icon: 'laptop', words: ['laptop'] },
|
|
{ icon: 'bug_report', words: ['test', 'debug'] },
|
|
{ icon: 'child_care', words: ['kids', 'baby'] },
|
|
{ icon: 'smartphone', words: ['phone', 'mobile'] },
|
|
];
|
|
for (let item of iconWords) {
|
|
for (let word of item.words) {
|
|
if (name.match(new RegExp(`(${word})`, 'gi'))) {
|
|
return item.icon;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Scroll to the top of the page
|
|
* Our 'content' is housed in the <main> DOM element
|
|
* We make sure the target supports scrolling before we attempt it
|
|
* Safari and IE Edge, well, don't.
|
|
*
|
|
* @param target String (element ID, optional)
|
|
* @param smooth_scroll Boolean (optional)
|
|
* */
|
|
const scrollTo = function (target = null, smooth_scroll = false) {
|
|
const main = document.getElementById('main');
|
|
|
|
// Remove our smooth-scroll class
|
|
if (!smooth_scroll) {
|
|
main.classList.remove('smooth-scroll');
|
|
}
|
|
|
|
// Target is a number, so treat as pixel position
|
|
if (target && Number.isInteger(target)) {
|
|
if (typeof main.scrollTo === 'function') {
|
|
main.scrollTop = target;
|
|
}
|
|
|
|
|
|
// Target is a string representing a DOM element by class/id
|
|
} else if (target) {
|
|
let element = null;
|
|
|
|
if (target.charAt(0) == '#') {
|
|
element = document.getElementById(target.substring(1));
|
|
} else if (target.charAt(0) == '.') {
|
|
element = document.getElementsByClassName(target.substring(1));
|
|
if (element.length > 0) {
|
|
element = element[0];
|
|
}
|
|
} else {
|
|
console.error(`Invalid target type '${target}'. Must start with '#' or '.'.`);
|
|
}
|
|
|
|
if (element && typeof element.scrollIntoView === 'function') {
|
|
element.scrollIntoView();
|
|
}
|
|
} else {
|
|
main.scrollTop = 0;
|
|
}
|
|
|
|
// Now reinstate smooth scroll
|
|
if (!smooth_scroll) {
|
|
main.classList.add('smooth-scroll');
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Upgrade one or many Spotify Playlist URIs
|
|
* This is their new, simplified syntax (September 2018) but they haven't updated it everywhere
|
|
* So we need to manually strip user:abc to keep things consistent
|
|
*
|
|
* @param uris Array|String
|
|
* @return Array|String
|
|
* */
|
|
const upgradeSpotifyPlaylistUris = function (uris) {
|
|
const upgraded = [];
|
|
|
|
for (let uri of uris) {
|
|
if (uri.includes('spotify:user:')) {
|
|
uri = uri.replace(/spotify:user:([^:]*?):/i, 'spotify:');
|
|
}
|
|
upgraded.push(uri);
|
|
}
|
|
|
|
return upgraded;
|
|
};
|
|
|
|
// As above, but for a single URI
|
|
let upgradeSpotifyPlaylistUri = function (uri) {
|
|
return upgradeSpotifyPlaylistUris([uri])[0];
|
|
};
|
|
|
|
export {
|
|
debounce,
|
|
throttle,
|
|
setFavicon,
|
|
queryString,
|
|
generateGuid,
|
|
getCurrentPusherConnection,
|
|
uriSource,
|
|
uriType,
|
|
sourceIcon,
|
|
getFromUri,
|
|
buildLink,
|
|
isTouchDevice,
|
|
isNumeric,
|
|
isObject,
|
|
isLoading,
|
|
isHosted,
|
|
getIndexedRecords,
|
|
titleCase,
|
|
scrollTo,
|
|
upgradeSpotifyPlaylistUris,
|
|
upgradeSpotifyPlaylistUri,
|
|
iconFromKeyword,
|
|
};
|
|
|
|
export default {
|
|
debounce,
|
|
throttle,
|
|
setFavicon,
|
|
queryString,
|
|
generateGuid,
|
|
getCurrentPusherConnection,
|
|
uriSource,
|
|
uriType,
|
|
sourceIcon,
|
|
getFromUri,
|
|
buildLink,
|
|
isTouchDevice,
|
|
isNumeric,
|
|
isObject,
|
|
isLoading,
|
|
isHosted,
|
|
getIndexedRecords,
|
|
titleCase,
|
|
scrollTo,
|
|
upgradeSpotifyPlaylistUris,
|
|
upgradeSpotifyPlaylistUri,
|
|
iconFromKeyword,
|
|
};
|