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
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]; }