Files
Iris/src/js/helpers.js
2019-04-22 21:39:40 +12:00

1600 lines
36 KiB
JavaScript
Executable File

export let isTouchDevice = function(){
return 'ontouchstart' in document.documentElement
}
/**
* 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.
**/
export let debounce = function(fn, wait, immediate){
var timeout;
return function(){
var context = this, args = arguments;
var later = function(){
timeout = null;
if (!immediate){
fn.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow){
fn.apply(context, args);
}
};
};
export let 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);
}
}
/**
* Storage handler
* All localStorage tasks are handled below. This means we can detect for localStorage issues in one place
**/
var storage = (function() {
var uid = new Date;
var storage;
var result;
try {
(storage = window.localStorage).setItem(uid, uid);
result = storage.getItem(uid) == uid;
storage.removeItem(uid);
return result && storage;
} catch (exception) {}
}());
/**
* Get a storage value
*
* @param key = string
* @param default_value = mixed (optional, if localStorage key doesn't exist, return this)
**/
export let getStorage = function(key, default_value = {}){
if (storage){
var value = storage.getItem(key);
if (value){
return JSON.parse(value);
} else {
return default_value;
}
} else {
console.warn("localStorage not available. Using default value for '"+key+"'.");
return default_value;
}
}
/**
* Set a storage value
*
* @param key = string
* @param value = object
* @param replace = boolean (optional, completely replace our local value rather than merging it)
**/
export let setStorage = function(key, value, replace = false){
if (storage){
var stored_value = storage.getItem(key);
// We have nothing to merge with, or we want to completely replace previous value
if (!stored_value || replace){
var new_value = value;
// Merge new value with existing
} else {
var new_value = Object.assign(
{},
JSON.parse(stored_value),
value
);
}
storage.setItem(key, JSON.stringify(new_value));
return;
} else {
console.warn("localStorage not available. '"+key+"'' will not perist when you close your browser.");
return;
}
}
/**
* Convert a string to JSON, after we've checked whether it needs
* conversion or not.
*
* @param data String or Object
* @return Object
**/
export let toJSON = function(data){
// Parse it
try {
let json = JSON.parse(data);
return json;
// Could not parse string
} catch (e){
// Check if it's JSON already
if (data.constructor === {}.constructor){
return data;
} else {
console.error("Could not convert non-JSON", string);
}
}
return {};
}
/**
* Set the app's favicon to a specific image.
*
* @param file String
* @return Boolean
*/
export let setFavicon = function(favicon){
console.log(favicon);
let link = document.createElement('link'),
oldLink = document.getElementById('favicon');
link.id = 'favicon';
link.rel = 'shortcut icon';
link.href = "/iris/assets/"+favicon;
if (oldLink) {
document.head.removeChild(oldLink);
}
document.head.appendChild(link);
}
/**
* Check if an image URL is cached or not
* Useful for bypassing load animations for cached assets (eg parallax)
*
* @param url String
* @return Boolean
**/
export let isCached = function(url){
var image = new Image();
image.src = url;
return image.complete;
}
/**
* Digest an array of Mopidy image objects into a universal format. We also re-write
* image URLs to be absolute to the mopidy server (required for proxy setups).
*
* @param mopidy = obj (mopidy store object)
* @param images = array
* @return array
**/
export let digestMopidyImages = function(mopidy, images){
var digested = [];
for (var i = 0; i < images.length; i++){
// Image object (ie from images.get)
if (typeof images[i] === 'object'){
// Accommodate backends that provide URIs vs URLs
var url = images[i].url
if (!url && images[i].uri){
url = images[i].uri
}
// Amend our URL
images[i].url = url
// Replace local images to point directly to our Mopidy server
if (url && url.startsWith('/images/')){
url = '//'+mopidy.host+':'+mopidy.port+url
}
// String-based image
} else if (typeof images[i] === 'string'){
// Replace local images to point directly to our Mopidy server
if (images[i].startsWith('/images/')){
images[i] = '//'+mopidy.host+':'+mopidy.port+images[i]
}
}
digested.push(images[i])
}
return digested
}
export let generateGuid = function(type = 'numeric'){
// numeric
if (type == 'numeric'){
var date = new Date().valueOf().toString();
var random_number = Math.floor((Math.random() * 100)).toString();
return parseInt(date+random_number);
} else {
var format = 'xxxxxxxxxx';
return format.replace(/[xy]/g, function(c){
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
}
export let getCurrentPusherConnection = function(connections, connectionid){
function isCurrentConnection(connection){
return connection.connectionid == newProps.pusher.connectionid;
}
var currentConnection = newProps.pusher.connections.find(isCurrentConnection);
if (!currentConnection ) return false;
return currentConnection;
}
/**
* Get a track's icon
* @param track object
* @return string
**/
export let getTrackIcon = function(current_track = false, core = false){
if (!core) return false
if (!current_track) return false
if (typeof(current_track.uri) == 'undefined') return false
if (typeof(core.tracks[current_track.uri]) === 'undefined') return false
var track = core.tracks[current_track.uri]
if (!track.images) return false
return formatImages(track.images).small
}
/**
* Format image URLs into a consistent size-based object
* We digest all our known image source formats into a universal small,medium,large,huge object
*
* @param $data mixed
* @return Object
**/
export let formatImages = function(data){
var sizes = {
formatted: true,
small: null,
medium: null,
large: null,
huge: null
}
if (!data){
return sizes;
}
// An array of images has been provided
if (Array.isArray(data)){
if (data.length <= 0){
return sizes;
}
for (var i = 0; i < data.length; i++){
let image = data[i]
// Already-formatted
if (image.formatted){
return image;
// Mopidy image object
} else if (image.__model__ && image.__model__ == 'Image'){
if (image.width < 400){
sizes.small = image.url;
}else if (image.width < 800){
sizes.medium = image.url;
}else if (image.width < 1000){
sizes.large = image.url;
} else {
sizes.huge = image.url;
}
// Mopidy image string
} else if (typeof(image) == 'string'){
sizes.small = image
// spotify-styled images
} else if (image.width !== undefined){
if (image.width < 400){
sizes.small = image.url;
}else if (image.width < 800){
sizes.medium = image.url;
}else if (image.width < 1000){
sizes.large = image.url;
} else {
sizes.huge = image.url;
}
// lastfm-styled images
} else if (image.size !== undefined){
switch(image.size){
case 'mega':
case 'extralarge':
case 'large':
sizes.medium = image['#text']
break;
case 'medium':
case 'small':
sizes.small = image['#text']
break;
}
}
}
// An object of images has been provided
// The Genius avatar object is an example of this
} else {
if (data.small) sizes.small = data.small.url;
if (data.medium) sizes.medium = data.medium.url;
if (data.large) sizes.large = data.large.url;
if (data.huge) sizes.huge = data.huge.url;
}
// Inherit images where we haven't been given the appropriate size
// Ie small duplicated to tiny, large duplicated to medium, etc
if (!sizes.small){
if (sizes.medium) sizes.small = sizes.medium
else if (sizes.large) sizes.small = sizes.large
else if (sizes.huge) sizes.small = sizes.huge
else sizes.small = null
}
if (!sizes.medium){
if (sizes.large) sizes.medium = sizes.large
else if (sizes.huge) sizes.medium = sizes.huge
else sizes.medium = sizes.small
}
if (!sizes.large) sizes.large = sizes.medium;
if (!sizes.huge) sizes.huge = sizes.large;
return sizes;
}
/**
* Format a simple object
* This is a shell record containing only the bare essentials. Typically
* a tracks' artists/album
*
* @param data obj
* @return obj
**/
export let formatSimpleObject = function(data){
var simple_object = {}
var fields = [
'uri',
'name'
];
for (var field of fields){
if (data.hasOwnProperty(field)){
simple_object[field] = data[field];
}
}
return simple_object;
}
/**
* Format multiple items
*
* @param tracks Array
* @return Array
**/
export let formatTracks = function(records = []){
var formatted = [];
for (var record of records){
formatted.push(formatTrack(record));
}
return formatted;
}
export let formatAlbums = function(records = []){
var formatted = [];
for (var record of records){
formatted.push(formatAlbum(record));
}
return formatted;
}
export let formatArtists = function(records = []){
var formatted = [];
for (var record of records){
formatted.push(formatArtist(record));
}
return formatted;
}
export let formatPlaylists = function(records = []){
var formatted = [];
for (var record of records){
formatted.push(formatTrack(record));
}
return formatted;
}
export let formatUsers = function(records = []){
var formatted = [];
for (var record of records){
formatted.push(formatUser(record));
}
return formatted;
}
/**
* Format our album objects into a universal format
*
* @param data obj
* @return obj
**/
export let formatAlbum = function(data){
var album = {};
var fields = [
'uri',
'provider',
'name',
'type',
'added_at',
'release_date',
'listeners',
'play_count',
'wiki',
'wiki_publish_date',
'popularity',
'images',
'artists_uris',
'tracks_uris',
'artists' // Array of simple records
];
// Loop fields and import from data
for (var field of fields){
if (data.hasOwnProperty(field)){
album[field] = data[field];
}
}
if (album.images && !album.images.formatted){
album.images = formatImages(album.images);
}
if (data.date && !album.date){
album.release_date = data.date;
}
if (data.album_type){
album.type = data.album_type;
}
if (album.provider === undefined && album.uri !== undefined){
album.provider = uriSource(album.uri);
}
return album;
}
/**
* Format our artist objects into a universal format
*
* @param data obj
* @return obj
**/
export let formatArtist = function(data){
var artist = {}
var fields = [
'uri',
'provider',
'mbid',
'name',
'type',
'popularity',
'followers',
'listeners',
'added_at',
'biography',
'biography_link',
'biography_publish_date',
'related_artists_uris',
'albums_uris',
'albums_total',
'albums_more',
'tracks_uris',
'tracks_total',
'tracks_more'
];
// Loop fields and import from data
for (var field of fields){
if (data.hasOwnProperty(field)){
artist[field] = data[field];
}
}
if (data.images && data.images.length > 0){
artist.images = [formatImages(data.images)];
}
if (data.followers && data.followers.total !== undefined){
artist.followers = data.followers.total;
}
if (data.bio){
if (data.bio.content && !artist.biography){
artist.biography = data.bio.content;
}
if (data.bio.links && data.bio.links.link && data.bio.links.link.href && !artist.biography_link){
artist.biography_link = data.bio.links.link.href;
}
if (data.bio.published && !artist.biography_publish_date){
artist.biography_publish_date = data.bio.published;
}
}
if (artist.provider === undefined && artist.uri !== undefined){
artist.provider = uriSource(artist.uri);
}
return artist;
}
/**
* Format our playlist objects into a universal format
*
* @param data obj
* @return obj
**/
export let formatPlaylist = function(data){
var playlist = {}
var fields = [
'uri',
'snapshot_id',
'provider',
'type',
'collaborative',
'public',
'name',
'description',
'images',
'popularity',
'followers',
'added_at',
'last_modified_date',
'can_edit',
'owner',
'user_uri',
'tracks_uris',
'tracks_total',
'tracks_more'
];
// Loop fields and import from data
for (var field of fields){
if (data.hasOwnProperty(field)){
playlist[field] = data[field];
}
}
if (playlist.images && !playlist.images.formatted){
playlist.images = formatImages(playlist.images);
}
if (data.followers && data.followers.total !== undefined){
playlist.followers = data.followers.total;
}
if (data.tracks && data.tracks.total !== undefined){
playlist.tracks_total = data.tracks.total;
}
if (data.owner){
playlist.owner = {
id: data.owner.id,
uri: data.owner.uri,
name: (data.owner.display_name ? data.owner.display_name : null)
}
playlist.user_uri = data.owner.uri;
}
// Spotify upgraded their playlists URI to remove user component (Sept 2018)
playlist.uri = upgradeSpotifyPlaylistUri(playlist.uri);
if (playlist.provider === undefined && playlist.uri !== undefined){
playlist.provider = uriSource(playlist.uri);
}
return playlist;
}
/**
* Format a user objects into a universal format
*
* @param data obj
* @return obj
**/
export let formatUser = function(data){
var user = {}
var fields = [
'id',
'uri',
'provider',
'name',
'images',
'followers',
'playlists_uris',
'playlists_total',
'playlists_more'
];
// Loop fields and import from data
for (var field of fields){
if (data.hasOwnProperty(field)){
user[field] = data[field];
}
}
if (!user.images && data.image){
user.images = formatImages(data.image);
} else if (!user.images && data.avatar){
user.images = formatImages(data.avatar);
} else if (user.images && !user.images.formatted){
user.images = formatImages(user.images);
}
if (data.followers && data.followers.total !== undefined){
user.followers = data.followers.total;
}
if (data.realname){
user.name = data.realname;
}
if (data.display_name && !user.name){
user.name = data.display_name;
}
if (data.id && !user.name){
user.name = data.id;
}
if (user.provider === undefined && user.uri !== undefined){
user.provider = uriSource(user.uri);
}
return user;
}
/**
* Format tracks into our universal format
*
* @param data obj
* @return obj
**/
export let formatTrack = function(data){
var track = {}
var fields = [
'uri',
'tlid',
'provider',
'name',
'images',
'release_date',
'disc_number',
'track_number',
'duration',
'followers',
'popularity',
'userloved',
'is_explicit',
'is_local',
'lyrics',
'lyrics_path',
'lyrics_results',
'artists', // Array of simple records
'album' // Array of simple records
];
// Nested track object (eg in spotify playlist)
if (data && data.track && isObject(data.track)){
// Copy wrapper's details (if applicable)
if (data.added_by){
data.track.added_by = data.added_by;
}
if (data.added_at){
data.track.added_at = data.added_at;
}
if (data.tlid){
data.track.tlid = data.tlid;
}
// And now flatten
data = data.track;
}
// Loop fields and import from data
for (var field of fields){
if (data.hasOwnProperty(field)){
track[field] = data[field];
}
}
if (data.followers && data.followers.total){
track.followers = data.followers.total;
}
if (track.duration === undefined && data.duration_ms !== undefined){
track.duration = data.duration_ms;
} else if (track.duration === undefined && data.length !== undefined){
track.duration = data.length;
}
if (track.track_number === undefined && data.track_no !== undefined){
track.track_number = data.track_no;
}
if (track.disc_number === undefined && data.disc_no !== undefined){
track.disc_number = data.disc_no;
}
if (track.release_date === undefined && data.date !== undefined){
track.release_date = data.date;
}
if (track.explicit === undefined && data.explicit !== undefined){
track.is_explicit = data.explicit;
}
// Copy images from albums (if applicable)
// TOOD: Identify if we stil need this...
if (data.album && data.album.images){
if (track.images === undefined || !track.images.formatted){
track.images = formatImages(data.album.images);
}
}
if (track.provider === undefined && track.uri !== undefined){
track.provider = uriSource(track.uri);
}
return track;
}
/**
* Format a snapcast client object into a universal format
*
* @param data obj
* @return obj
**/
export let formatClient = function(data){
var client = {}
var fields = [
'id',
'connected',
'name',
'host_name',
'volume',
'mute',
'latency',
'power_on_command',
'power_off_command'
];
for (var field of fields){
if (data.hasOwnProperty(field)){
client[field] = data[field];
}
}
if (data.config){
if (client.latency === undefined && data.config.latency !== undefined){
client.latency = data.config.latency;
}
if (!client.name && data.config.name){
client.name = data.config.name;
}
if (data.config.volume){
if (data.config.volume.percent){
client.volume = data.config.volume.percent;
}
if (data.config.volume.muted){
client.mute = data.config.volume.muted;
}
}
}
if (client.name === undefined && data.host && data.host.name){
client.name = data.host.name;
}
return client;
}
/**
* Format a snapcast client object into a universal format
*
* @param data obj
* @return obj
**/
export let formatGroup = function(data){
var group = {}
var fields = [
'id',
'name',
'mute',
'stream_id',
'clients_ids'
];
for (var field of fields){
if (data.hasOwnProperty(field)){
group[field] = data[field];
}
}
if (group.name === undefined || group.name === ""){
group.name = 'Group '+data.id.substring(0,3);
}
if (group.mute === undefined && data.mute !== undefined){
group.mute = data.mute;
}
return group;
}
/**
* Collate an object with external references into a fully self-contained object
* We merge *_uris references (ie tracks_uris) into the main object
*
* @param object Obj
* @param indexes Obj (the relevant core indexes)
* @return object Obj
**/
export let collate = function(obj, indexes = {}){
// First, let's reset this object
// This is important because by changing this object, we inadvertently
// change the source object (ie the indexed record), which undoes the
// efficiencies of a lean index object
obj = Object.assign({}, obj);
// Setup empty arrays for the appropriate reference objects
// This helps create a consistent object structure
if (obj.artists_uris !== undefined) obj.artists = [];
if (obj.albums_uris !== undefined) obj.albums = [];
if (obj.tracks_uris !== undefined) obj.tracks = [];
if (obj.users_uris !== undefined) obj.users = [];
if (obj.playlists_uris !== undefined) obj.playlists = [];
if (obj.related_artists_uris !== undefined) obj.related_artists = [];
if (obj.clients_ids !== undefined) obj.clients = [];
if (indexes.artists){
if (obj.artists_uris){
for (var uri of obj.artists_uris){
if (indexes.artists[uri]){
obj.artists.push(indexes.artists[uri]);
}
}
}
if (obj.related_artists_uris){
for (var uri of obj.related_artists_uris){
if (indexes.artists[uri]){
obj.related_artists.push(indexes.artists[uri]);
}
}
}
if (obj.artist_uri){
if (indexes.artists[obj.artist_uri]){
obj.artist = indexes.artists[obj.artist_uri];
}
}
}
if (indexes.albums){
if (obj.albums_uris){
for (var uri of obj.albums_uris){
if (indexes.albums[uri]){
obj.albums.push(indexes.albums[uri]);
}
}
}
if (obj.album_uri){
if (indexes.albums[obj.album_uri]){
obj.album = indexes.albums[obj.album_uri];
}
}
}
if (indexes.tracks){
if (obj.tracks_uris){
for (var uri of obj.tracks_uris){
if (indexes.tracks[uri]){
obj.tracks.push(indexes.tracks[uri]);
}
}
}
if (obj.track_uri){
if (indexes.tracks[obj.track_uri]){
obj.track = indexes.tracks[obj.track_uri];
}
}
}
if (indexes.users){
if (obj.users_uris){
for (var uri of obj.users_uris){
if (indexes.users[uri]){
obj.users.push(indexes.users[uri]);
}
}
}
if (obj.user_uri){
if (indexes.users[obj.user_uri]){
obj.user = indexes.users[obj.user_uri];
}
}
}
if (indexes.playlists){
if (obj.playlists_uris){
for (var uri of obj.playlists_uris){
if (indexes.playlists[uri]){
obj.playlists.push(indexes.playlists[uri]);
}
}
}
if (obj.playlist_uri){
if (indexes.playlists[obj.playlist_uri]){
obj.playlist = indexes.playlists[obj.playlist_uri];
}
}
}
if (indexes.clients){
if (obj.clients_ids){
for (var id of obj.clients_ids){
if (indexes.clients[id]){
obj.clients.push(indexes.clients[id]);
}
}
}
}
return obj;
}
/**
* Figure out a URI's source namespace
* @param uri = string
**/
export let uriSource = function(uri){
if (!uri){
return false;
}
var exploded = uri.split(':');
return exploded[0]
}
/**
* Identify what kind of asset a URI is (playlist, album, etc)
*
* @param uri = string
* @return string
**/
export let uriType = function(uri){
if (!uri) return null;
var exploded = uri.split(':')
if (exploded[0] == 'm3u'){
return 'playlist'
}
if (exploded[0] == 'iris'){
switch (exploded[1]){
case 'search':
case 'discover':
case 'browse':
return exploded[1];
break;
}
}
switch (exploded[1]){
case 'track':
case 'artist':
case 'album':
case 'playlist':
case 'genre':
return exploded[1]
break
case 'user':
if (exploded.length > 3 && exploded[3] == 'playlist'){
return 'playlist'
}
return exploded[1]
break
}
}
export let 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
**/
export let getFromUri = function(element, uri = ""){
var exploded = uri.split(':');
var 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]
} else 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]
}
break
case 'searchtype':
if (exploded[0] == "search"){
return exploded[1];
}
break
case 'searchterm':
if (exploded[0] == "search"){
return exploded[2];
}
break
}
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
**/
export let buildLink = function (uri){
// Start the link with the URI type
var type = uriType(uri);
var 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;
}
/**
* Digest an array of objects and pull into simple array of one property
*
* @param property = string
* @param items = Array
* @return Array
**/
export let arrayOf = function(property, items){
let array = [];
for (var item of items){
// Make sure the property is defined
if (item[property] !== undefined && item[property] != null){
array.push(item[property]);
}
}
return array;
}
/**
* Merge duplicated items in an array
*
* @param list Array the unclean array
* @param key string = the unique key (id, uri, tlid, etc)
**/
export let mergeDuplicates = function(list, key){
var clean_list = [];
var keyed_list = {};
for(var i in list){
var item = list[i]
if (item[key] in keyed_list){
item = Object.assign({}, keyed_list[item[key]], item)
}
keyed_list[item[key]] = item;
}
for(i in keyed_list){
clean_list.push(keyed_list[i]);
}
return clean_list;
}
/**
* Remove duplicate items in a simple array
*
* @param list Array the unclean array
**/
export let removeDuplicates = function(array){
var unique = [];
for (var i in array){
if (unique.indexOf(array[i]) <= -1){
unique.push(array[i])
}
}
return unique;
}
/**
* Apply a partial text search on an array of objects
*
* @param field = string (the field we're to search)
* @param value = string (the value to find)
* @param array = array of objects to search
* @param singular = boolean (just return the first result)
* @return array
**/
export let applyFilter = function(field, value, array, singular = false){
var results = [];
for (var i = 0; i < array.length; i++){
if (array[i][field] && String(array[i][field]).toLowerCase().includes(String(value).toLowerCase())){
if (singular){
return array[i];
} else {
results.push(array[i]);
}
}
}
return results;
}
/**
* Convert a list of indexes to a useable range
* We ignore stragglers, and only attend to the first 'bunch' of consecutive indexes
*
* @param indexes array of int
**/
export let createRange = function (indexes){
// sort our indexes smallest to largest
function sortAsc(a,b){
return a - b
}
indexes.sort(sortAsc);
// iterate indexes to build the first 'bunch'
var first_bunch = []
var previous_index = false
for(var i = 0; i < indexes.length; i++){
if (!previous_index || previous_index == indexes[i]-1){
first_bunch.push(indexes[i])
previous_index = indexes[i]
}
// TODO: break when we find an integer step for better performance
}
return {
start: first_bunch[0],
length: first_bunch.length
}
}
/**
* Sort an array of objects
* @param array = array to sort
* @param property = string to sort by
* @param reverse = boolean
* @param sort_map = array of value ordering (rather than alphabetical, numerical, etc)
* @return array
**/
export let sortItems = function (array, property, reverse = false, sort_map = null){
if (!array || array.length <= 0){
return [];
}
function get_value(value){
var split = property.split('.');
for (var property_element of split){
// Apply sort on a property of the first item of an array
if (property_element == 'first'){
if (Array.isArray(value) && value.length > 0){
value = value[0];
continue;
} else {
return null;
}
// Just need the length of an array
} else if (property_element == 'length'){
if (Array.isArray(value)){
return value.length;
} else {
return 0;
}
// No value here
} else if (typeof(value[property_element]) === 'undefined'){
return null;
}
// Otherwise continue looping to the end of the split property
value = value[property_element];
}
return value;
}
function compare(a,b){
var a_value = get_value(a);
var b_value = get_value(b);
// Sorting by URI as a reference for sorting by uri source (first component of URI)
if (property == 'uri'){
a_value = uriSource(a_value);
b_value = uriSource(b_value);
}
// Map sorting
// Use the index of the string as a sorting mechanism
if (sort_map){
var a_index = sort_map.indexOf(a_value+':');
var b_index = sort_map.indexOf(b_value+':');
if (a_index < b_index) return 1;
if (a_index > b_index) return -1;
// Boolean sorting
} else if (typeof a_value == 'boolean' && typeof b_value == 'boolean'){
if (a_value && !b_value) return -1;
if (!a_value && b_value) return 1;
return 0
// Numeric sorting
} else if (typeof a_value == 'number' && typeof b_value == 'number'){
if (a_value == null && b_value == null) return 0;
if (a_value == null) return -1;
if (b_value == null) return 1;
if (parseInt(a_value) > parseInt(b_value)) return 1;
if (parseInt(a_value) < parseInt(b_value)) return -1;
return 0
// Alphabetic sorting
} else {
if (a_value && !b_value ) return -1;
if (!a_value && b_value ) return 1;
if (!a_value && !b_value ) return 0;
if (a_value.toLowerCase() > b_value.toLowerCase()) return 1;
if (a_value.toLowerCase() < b_value.toLowerCase()) return -1;
return 0
}
}
var sorted = Object.assign([], array.sort(compare));
if (reverse){
sorted.reverse();
}
return sorted;
}
/**
* Shuffle items in place
*
* @param Array items
* @return Array
**/
export let shuffle = function(array){
var j, x, i;
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = array[i];
array[i] = array[j];
array[j] = x;
}
return array;
}
/**
* Figure out if a value is a number
* @param value = mixed
* @return boolean
**/
export let isNumeric = function (value){
return !isNaN(parseFloat(value)) && isFinite(value)
}
/**
* Figure out if a value is an object
* @param value = mixed
* @return boolean
**/
export 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
**/
export let isLoading = function(load_queue = [], keys = []){
// Loop all of our load queue items
for (var 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 (var 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
**/
export let isHosted = function(hosts = ['jaedb.github.io']){
var hostname = window.location.hostname;
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
**/
export let getIndexedRecords = function(index, uris){
var records = []
// Wrap in array, if we've only got one URI
if (!uris instanceof Array){
uris = [uris]
}
for (var 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
**/
export let titleCase = function(string){
return string.charAt(0).toUpperCase() + string.slice(1)
}
/**
* 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)
**/
export let scrollTo = function(target = null, smooth_scroll = false){
var 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){
var 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
**/
export let upgradeSpotifyPlaylistUris = function(uris){
var upgraded = [];
for (var 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
export let upgradeSpotifyPlaylistUri = function(uri){
return upgradeSpotifyPlaylistUris([uri])[0];
}