Service worker caching, Loader component

This commit is contained in:
James Barnsley
2019-09-10 06:59:44 +12:00
parent a86403fa59
commit c08cc71c47
49 changed files with 16390 additions and 11259 deletions

View File

@ -179,6 +179,7 @@ export class App extends React.Component {
render() {
let className = `${this.props.theme}-theme app-inner`;
className += ` ${navigator.onLine ? 'online' : 'offline'}`
if (this.props.wide_scrollbar_enabled) {
className += ' wide-scrollbar';
}
@ -190,7 +191,6 @@ export class App extends React.Component {
}
if (this.props.touch_dragging) {
className += ' touch-dragging';
console.log('TOUCH DRAGGING');
}
if (this.props.context_menu) {
className += ' context-menu-open';

View File

@ -7,7 +7,7 @@ import { withRouter } from 'react-router';
import Link from './Link';
import TrackList from './TrackList';
import Icon from './Icon';
import Thumbnail from './Thumbnail';
import Loader from './Loader';
import URILink from './URILink';
import * as helpers from '../helpers';
@ -402,7 +402,7 @@ added from
renderSubmenu() {
let list = null;
let loader = null;
let isLoading = false;
switch (this.state.submenu) {
case 'add-to-playlist':
@ -415,13 +415,7 @@ added from
playlists = helpers.sortItems(playlists, 'name');
if (this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR && this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR.status == 'running') {
loader = (
<div className="context-menu__item">
<div className="context-menu__item mini-loader loading">
<div className="loader" />
</div>
</div>
);
isLoading = true;
}
list = <span className="context-menu__item"><span className="context-menu__item mid_grey-text">No writable playlists</span></span>;
@ -448,7 +442,11 @@ Back
</a>
</div>
{list}
{loader}
{isLoading && (
<div className="context-menu__item">
<Loader className="context-menu__item" mini loading />
</div>
)}
</div>
);
}

View File

@ -6,7 +6,7 @@ import { bindActionCreators } from 'redux';
import * as helpers from '../helpers';
import * as uiActions from '../services/ui/actions';
class DebugInfo extends React.Component {
class DebugInfo extends React.Component {
constructor(props) {
super(props);
}
@ -67,7 +67,7 @@ class DebugInfo extends React.Component {
</div>
);
}
return <div className="debug-info-item mid_grey-text">Nothing loading</div>;
return <div className="debug-info-item mid_grey-text">Nothing loading</div>;
}
render() {
@ -78,13 +78,13 @@ class DebugInfo extends React.Component {
<div className="debug-info-section">
<div className="debug-info-item">
Version:
{' '}
Version:
{' '}
{version}
</div>
<div className="debug-info-item">
Build:
{' '}
Build:
{' '}
{build}
</div>
</div>
@ -92,76 +92,81 @@ class DebugInfo extends React.Component {
<div className="debug-info-section">
<h5>Indexes</h5>
<div className="debug-info-item">
Albums:
{' '}
Albums:
{' '}
{this.props.core.albums ? Object.keys(this.props.core.albums).length : '0'}
</div>
<div className="debug-info-item">
Artists:
{' '}
Artists:
{' '}
{this.props.core.artists ? Object.keys(this.props.core.artists).length : '0'}
</div>
<div className="debug-info-item">
Playlists:
{' '}
Playlists:
{' '}
{this.props.core.playlists ? Object.keys(this.props.core.playlists).length : '0'}
</div>
<div className="debug-info-item">
Tracks:
{' '}
Tracks:
{' '}
{this.props.core.tracks ? Object.keys(this.props.core.tracks).length : '0'}
</div>
<div className="debug-info-item">
Users:
{' '}
Users:
{' '}
{this.props.core.users ? Object.keys(this.props.core.users).length : '0'}
</div>
<div className="debug-info-item">
Notifications:
{' '}
Notifications:
{' '}
{this.props.ui.notifications ? Object.keys(this.props.ui.notifications).length : '0'}
</div>
<div className="debug-info-item">
Processes:
{' '}
Processes:
{' '}
{this.props.ui.processes ? Object.keys(this.props.ui.processes).length : '0'}
</div>
<div className="debug-info-item">
Enqueue batches:
{' '}
Enqueue batches:
{' '}
{this.props.mopidy.enqueue_uris_batches ? this.props.mopidy.enqueue_uris_batches.length : '0'}
</div>
<div className="debug-info-item">
Cached URLs:
{' '}
{Object.keys(helpers.getStorage('cache')).length}
</div>
</div>
<div className="debug-info-section">
<h5>Config</h5>
<div className="debug-info-item">
Slim mode:
{' '}
Slim mode:
{' '}
{this.props.ui.slim_mode ? 'on' : 'off'}
</div>
<div className="debug-info-item">
Test mode:
{' '}
Test mode:
{' '}
{this.props.ui.test_mode ? 'on' : 'off'}
</div>
<div className="debug-info-item">
Touch:
{' '}
Touch:
{' '}
{helpers.isTouchDevice() ? 'on' : 'off'}
</div>
<div className="debug-info-item">
LocalStorage usage:
{' '}
{localStorageUsage.used}
kb (~
{localStorageUsage.percent}
LocalStorage usage:
{' '}
{localStorageUsage.used}
kb (~
{localStorageUsage.percent}
%)
</div>
<div className="debug-info-item">
Selected tracks:
{' '}
{this.props.ui.selected_tracks.length}
Selected tracks:
{' '}
{this.props.ui.selected_tracks.length}
<br />
{
this.props.ui.selected_tracks.map((track_key, index) => (
@ -191,4 +196,4 @@ const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(DebugInfo);
export default connect(mapStateToProps, mapDispatchToProps)(DebugInfo);

View File

@ -14,8 +14,8 @@ export default class GridItem extends React.Component {
// If the item that has just been mounted doesn't have images,
// try fetching them from LastFM or Discogs
if (!item.images && discogsActions) {
switch (helpers.uriType(item.uri)) {
if (!item.images) {
switch (helpers.uriType(item.uri)) {
case 'artist':
if (discogsActions) {
discogsActions.getArtistImages(item.uri, item);
@ -50,8 +50,8 @@ export default class GridItem extends React.Component {
if (item.tracks_total) {
return (
<span className="grid__item__secondary__content">
{item.tracks_total}
{' '}
{item.tracks_total}
{' '}
tracks
</span>
);
@ -119,7 +119,7 @@ tracks
className={`grid__item grid__item--${this.props.type}`}
to={link}
onClick={(e) => helpers.scrollTo()}
onContextMenu={(e) => this.onContextMenu(e)}
onContextMenu={(e) => this.onContextMenu(e)}
>
<Thumbnail glow size="medium" className="grid__item__thumbnail" images={images} />
<div className="grid__item__name">

View File

@ -1,8 +1,9 @@
import React from 'react';
import * as helpers from '../helpers';
import Loader from './Loader';
export default class LazyLoadListener extends React.Component {
export default class LazyLoadListener extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -32,11 +33,11 @@ export default class LazyLoadListener extends React.Component {
}
handleScroll(e) {
if (this.state.listening) {
if (this.state.listening) {
const window_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
// At, or half a screen from bottom of the page
if (this.element.scrollTop > (this.element.scrollHeight - this.element.offsetHeight - (window_height / 2))) {
if (this.element.scrollTop > (this.element.scrollHeight - this.element.offsetHeight - (window_height / 2))) {
// Immediately stop listening to avoid duplicating pagination requests
this.setState(
{ listening: false },
@ -44,16 +45,14 @@ export default class LazyLoadListener extends React.Component {
console.info(`Loading more: ${this.props.loadKey}`);
this.props.loadMore();
},
);
);
}
}
}
render() {
return (
<div className={`lazy-loader body-loader${this.props.showLoader ? ' loading' : ''}`}>
{this.props.showLoader ? <div className="loader" /> : null}
</div>
<Loader body lazy loading={this.props.showLoader} />
);
}
}
}

44
src/js/components/Loader.js Executable file
View File

@ -0,0 +1,44 @@
import React, { memo } from 'react';
import Icon from './Icon';
export default memo((props) => {
const { body, loading, mini, lazy, white, className = '', children = null } = props;
if (!loading) {
return null;
}
let classNameString = 'loader';
if (className) {
classNameString += ` ${className}`;
}
if (mini) {
classNameString += ` loader--mini`;
}
if (body) {
classNameString += ` loader--body`;
}
if (lazy) {
classNameString += ` loader--lazy`;
}
if (white) {
classNameString += ` loader--white`;
}
return (
<div className={classNameString}>
{
navigator.onLine ? (
<div className="loader__spinner"></div>
) : (
<div className="loader__offline">
<Icon name="wifi_off" />
<p>You need to be online load this resource</p>
</div>
)
}
{children}
</div>
);
});

View File

@ -10,8 +10,9 @@ import * as geniusActions from '../services/genius/actions';
import * as snapcastActions from '../services/snapcast/actions';
import Icon from './Icon';
import Loader from './Loader';
class Notifications extends React.Component {
class Notifications extends React.Component {
constructor(props) {
super(props);
}
@ -91,10 +92,10 @@ class Notifications extends React.Component {
{notification.title ? <h4 className="notification__title">{notification.title}</h4> : null}
{notification.content ? <div className="notification__content">{notification.content}</div> : null}
{notification.description ? <div className="notification__description">{notification.description}</div> : null }
{notification.links ? (
<div className="notification__actions">
{notification.links.map((link, i) => <a className="notification__actions__item button button--secondary" href={link.url} target={link.new_window ? '_blank' : 'self'} key={i}>{link.text}</a>)}
</div>
{notification.links ? (
<div className="notification__actions">
{notification.links.map((link, i) => <a className="notification__actions__item button button--secondary" href={link.url} target={link.new_window ? '_blank' : 'self'} key={i}>{link.text}</a>)}
</div>
) : null }
</div>
);
@ -105,7 +106,7 @@ class Notifications extends React.Component {
);
}
renderProcess(process) {
renderProcess(process) {
let progress = 0;
if (process.data.total && process.data.remaining) {
progress = ((process.data.total - process.data.remaining) / process.data.total * 100).toFixed();
@ -115,11 +116,11 @@ class Notifications extends React.Component {
case 'running':
return (
<div className={`notification notification--process${process.closing ? ' closing' : ''}`} key={process.key}>
<div className="loader">
<Loader loading mini white>
<div className="progress">
<div className="fill" style={{ width: `${progress}%` }} />
</div>
</div>
</Loader>
{process.message}
<Icon name="close" className="notification__close-button" onClick={(e) => { this.props.uiActions.cancelProcess(process.key); }} />
</div>
@ -184,7 +185,7 @@ class Notifications extends React.Component {
<div className={className} />
);
}
return null;
return null;
}
render() {

View File

@ -12,11 +12,47 @@ import Thumbnail from './Thumbnail';
import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
class Sidebar extends React.Component {
class Sidebar extends React.Component {
constructor(props) {
super(props);
}
renderStatusIcon() {
if (this.props.update_available) {
return (
<span className="status tooltip tooltip--right">
<Icon name="cloud_download" className="green-text" />
<span className="tooltip__content">Update available</span>
</span>
);
}
if (!navigator.onLine) {
return (
<span className="status tooltip tooltip--right">
<Icon name="wifi_off" className="red-text" />
<span className="tooltip__content">
Browser offline
</span>
</span>
);
}
if (!this.props.mopidy_connected || !this.props.pusher_connected) {
return (
<span className="status tooltip tooltip--right">
<Icon name="warning" className="red-text" />
<span className="tooltip__content">
{!this.props.mopidy_connected && (<span>Mopidy not connected<br /></span>)}
{!this.props.pusher_connected && (<span>Pusher not connected<br /></span>)}
</span>
</span>
);
}
return null;
}
render() {
return (
<aside className="sidebar">
@ -34,7 +70,7 @@ class Sidebar extends React.Component {
</Link>
</section>
{this.props.spotify_enabled ? (
{this.props.spotify_enabled ? (
<section className="sidebar__menu__section">
<title className="sidebar__menu__section__title">Discover</title>
<Link to="/discover/recommendations" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
@ -53,7 +89,7 @@ class Sidebar extends React.Component {
<Icon name="new_releases" type="material" />
New releases
</Link>
</section>
</section>
) : null}
<section className="sidebar__menu__section">
@ -84,31 +120,7 @@ class Sidebar extends React.Component {
<Link to="/settings" history={this.props.history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="settings" type="material" />
Settings
{this.props.update_available ? (
<span className="status tooltip tooltip--right">
<Icon name="cloud_download" className="green-text" />
<span className="tooltip__content">Update available</span>
</span>
) : null}
{!this.props.mopidy_connected || !this.props.pusher_connected ? (
<span className="status tooltip tooltip--right">
<Icon name="warning" className="red-text" />
<span className="tooltip__content">
{!this.props.mopidy_connected ? (
<span>
Mopidy not connected
<br />
</span>
) : null}
{!this.props.pusher_connected ? (
<span>
Pusher not connected
<br />
</span>
) : null}
</span>
</span>
) : null}
{this.renderStatusIcon()}
</Link>
</section>
@ -141,4 +153,4 @@ const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Sidebar));
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Sidebar));

View File

@ -12,7 +12,7 @@ import ErrorBoundary from './ErrorBoundary';
import * as helpers from '../helpers';
export default class Track extends React.Component {
export default class Track extends React.Component {
constructor(props) {
super(props);
@ -68,7 +68,7 @@ export default class Track extends React.Component {
const threshold = 5;
// Have we dragged outside of our threshold zone?
if (e.pageX > start_x + threshold || e.pageX < start_x - threshold || e.pageY > start_y + threshold || e.pageY < start_y - threshold) {
if (e.pageX > start_x + threshold || e.pageX < start_x - threshold || e.pageY > start_y + threshold || e.pageY < start_y - threshold) {
// Handover to parent for dragging. We can unset all our behaviour now.
this.props.handleDrag(e);
this.start_position = false;
@ -143,14 +143,14 @@ export default class Track extends React.Component {
if (this.start_position.x + tap_distance_threshold > end_position.x
&& this.start_position.x - tap_distance_threshold < end_position.x
&& this.start_position.y + tap_distance_threshold > end_position.y
&& this.start_position.y - tap_distance_threshold < end_position.y) {
&& this.start_position.y - tap_distance_threshold < end_position.y) {
// Clicked a nested link (ie Artist name), so no dragging required
if (!target.is('a')) {
e.preventDefault();
}
// Context trigger
if (target.hasClass('touch-contextable')) {
if (target.hasClass('touch-contextable')) {
// Update our selection. By not passing touch = true selection will work like a regular click
// this.props.handleSelection(e);
this.props.handleContextMenu(e);
@ -188,7 +188,7 @@ export default class Track extends React.Component {
);
}
if (track.album) {
if (track.album) {
if (track.album.uri) {
var album = <URILink type="album" uri={track.album.uri}>{track.album.name}</URILink>;
} else {
@ -205,15 +205,15 @@ export default class Track extends React.Component {
if (this.props.track_context == 'history') {
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--played_at">
{track.played_at ? (
<span>
<Dater type="ago" data={track.played_at} />
{' '}
ago
</span>
{track.played_at ? (
<span>
<Dater type="ago" data={track.played_at} />
{' '}
ago
</span>
) : '-'}
</div>
);
);
} else if (this.props.track_context == 'queue') {
if (track.added_from && track.added_by) {
const type = (track.added_from ? helpers.uriType(track.added_from) : null);
@ -237,16 +237,15 @@ ago
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--added">
<span className="by">
{track.added_by}
{' '}
<span className="by">
{`${track.added_by} `}
</span>
<span className="from">
from
{link}
<span className="from">
{`from `}
{link}
</span>
</div>
);
);
} else if (track.added_by) {
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--added">
@ -264,7 +263,7 @@ from
drag_zone = (
<span
className="list__item__column__item list__item__column__item--drag-zone drag-zone touch-draggable mouse-draggable"
key="drag-zone"
key="drag-zone"
>
<Icon name="drag_indicator" />
</span>
@ -291,7 +290,7 @@ from
onDoubleClick={(e) => this.handleDoubleClick(e)}
onContextMenu={(e) => this.props.handleContextMenu(e)}
onTouchStart={(e) => this.handleTouchStart(e)}
onTouchEnd={(e) => this.handleTouchEnd(e)}
onTouchEnd={(e) => this.handleTouchEnd(e)}
>
<div className="list__item__column list__item__column--name">
<div className="list__item__column__item--name">
@ -299,10 +298,10 @@ from
{track.explicit ? <span className="flag dark">EXPLICIT</span> : null}
{track.playing ? <Icon className={`js--${this.props.play_state}`} name="playing" type="css" /> : null}
</div>
{track_details ? (
{track_details ? (
<ul className="list__item__column__item--details">
{track_details}
</ul>
</ul>
) : null}
</div>
{track_middle_column ? <div className="list__item__column list__item__column--middle">{track_middle_column}</div> : null}
@ -311,10 +310,10 @@ from
<span className="list__item__column__item list__item__column__item--duration">
{track.duration ? <Dater type="length" data={track.duration} /> : '-'}
</span>
{this.props.show_source_icon ? (
{this.props.show_source_icon ? (
<span className="list__item__column__item list__item__column__item--source">
<Icon type="fontawesome" name={helpers.sourceIcon(track.uri)} fixedWidth />
</span>
</span>
) : null}
<ContextMenuTrigger className="list__item__column__item--context-menu-trigger subtle" onTrigger={(e) => this.props.handleContextMenu(e)} />
</div>
@ -322,4 +321,4 @@ from
</ErrorBoundary>
);
}
}
}

View File

@ -12,7 +12,7 @@ export const isTouchDevice = function () {
export const debounce = function (fn, wait, immediate) {
let timeout;
return function () {
const context = this; const
const context = this; const
args = arguments;
const later = function () {
@ -75,10 +75,10 @@ export const getStorage = function (key, default_value = {}) {
if (value) {
return JSON.parse(value);
}
return default_value;
return default_value;
}
console.warn(`localStorage not available. Using default value for '${key}'.`);
return default_value;
return default_value;
};
/**
@ -104,12 +104,38 @@ export const setStorage = function (key, value, replace = false) {
...value,
};
}
storage.setItem(key, JSON.stringify(new_value));
storage.setItem(key, JSON.stringify(new_value));
} else {
console.warn(`localStorage not available. '${key}'' will not perist when you close your browser.`);
console.warn(`localStorage not available. '${key}'' will not perist when you close your browser.`);
}
};
/**
* Cache handlers
* This allows arbritrary requests to be cached by key into local storage. Typically key would be
* a URL, but it could also be a asset id.
*
* Use sparingly as localStorage is limited in size! Ideally store only the request data needed,
* rather than the entire response data.
*/
export const cache = {
get: key => {
const cache = getStorage('cache', {});
if (cache[`"${key}"`] !== undefined) {
return cache[`"${key}"`];
}
},
set: (key, data) => {
let cache = getStorage('cache', {});
cache[`"${key}"`] = data;
setStorage('cache', cache);
return true;
},
clear: () => {
setStorage('cache', {}, true);
}
}
/**
* Convert a string to JSON, after we've checked whether it needs
@ -118,19 +144,19 @@ export const setStorage = function (key, value, replace = false) {
* @param data String or Object
* @return Object
* */
export const toJSON = function (data) {
export const toJSON = function (data) {
// Parse it
try {
const json = JSON.parse(data);
return json;
// Could not parse string
} catch (e) {
} catch (e) {
// Check if it's JSON already
if (data.constructor === {}.constructor) {
return data;
}
console.error('Could not convert non-JSON', string);
console.error('Could not convert non-JSON', string);
}
return {};
};
@ -143,7 +169,7 @@ export const toJSON = function (data) {
*/
export const setFavicon = function (filename) {
const links = document.getElementsByClassName('favicon');
for (const link of links) {
for (const link of links) {
// Construct new <links>
const new_link = document.createElement('link');
new_link.className = link.className;
@ -185,7 +211,7 @@ export const isCached = function (url) {
export const digestMopidyImages = function (mopidy, images) {
const digested = [];
for (let i = 0; i < images.length; i++) {
for (let i = 0; i < images.length; i++) {
// Image object (ie from images.get)
if (typeof images[i] === 'object') {
// Accommodate backends that provide URIs vs URLs
@ -226,10 +252,10 @@ export const generateGuid = function (type = 'numeric') {
}
const format = 'xxxxxxxxxx';
return format.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0; const
const r = Math.random() * 16 | 0; const
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
});
};
export const getCurrentPusherConnection = function (connections, connectionid) {
@ -266,7 +292,7 @@ export const getTrackIcon = function (current_track = false, core = false) {
* @param $data mixed
* @return Object
* */
export let formatImages = function (data) {
export let formatImages = function (data) {
const sizes = {
formatted: true,
small: null,
@ -280,7 +306,7 @@ export let formatImages = function (data) {
}
// An array of images has been provided
if (Array.isArray(data)) {
if (Array.isArray(data)) {
if (data.length <= 0) {
return sizes;
}
@ -293,7 +319,7 @@ export let formatImages = function (data) {
return image;
// Mopidy image object
} if (image.__model__ && image.__model__ == 'Image') {
} if (image.__model__ && image.__model__ == 'Image') {
if (image.width < 400) {
sizes.small = image.url;
} else if (image.width < 800) {
@ -309,7 +335,7 @@ export let formatImages = function (data) {
sizes.small = image;
// spotify-styled images
} else if (image.width !== undefined) {
} else if (image.width !== undefined) {
if (image.width < 400) {
sizes.small = image.url;
} else if (image.width < 800) {
@ -703,7 +729,7 @@ export let formatTrack = function (data) {
];
// Nested track object (eg in spotify playlist)
if (data && data.track && isObject(data.track)) {
if (data && data.track && isObject(data.track)) {
// Copy wrapper's details (if applicable)
if (data.added_by) {
data.track.added_by = data.added_by;
@ -861,7 +887,7 @@ export const formatGroup = function (data) {
* @param indexes Obj (the relevant core indexes)
* @return object Obj
* */
export const collate = function (obj, indexes = {}) {
export const 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
@ -1031,7 +1057,7 @@ export const uriType = function (uri) {
export const sourceIcon = function (uri, source = null) {
if (uri) source = uriSource(uri);
switch (source) {
switch (source) {
case 'local':
case 'm3u':
return 'folder';
@ -1145,7 +1171,7 @@ export const getFromUri = function (element, uri = '') {
* @param $uri = String
* @return String
* */
export const buildLink = function (uri) {
export const buildLink = function (uri) {
// Start the link with the URI type
const type = uriType(uri);
let link = `/${type}/`;
@ -1169,7 +1195,7 @@ export const buildLink = function (uri) {
* */
export const arrayOf = function (property, items) {
const array = [];
for (const item of items) {
for (const item of items) {
// Make sure the property is defined
if (item[property] !== undefined && item[property] != null) {
array.push(item[property]);
@ -1240,7 +1266,7 @@ export const applyFilter = function (field, value, array, singular = false) {
if (singular) {
return array[i];
}
results.push(array[i]);
results.push(array[i]);
}
}
@ -1254,7 +1280,7 @@ export const applyFilter = function (field, value, array, singular = false) {
*
* @param indexes array of int
* */
export const createRange = function (indexes) {
export const createRange = function (indexes) {
// sort our indexes smallest to largest
function sortAsc(a, b) {
return a - b;
@ -1287,14 +1313,14 @@ export const createRange = function (indexes) {
* @param sort_map = array of value ordering (rather than alphabetical, numerical, etc)
* @return array
* */
export const sortItems = function (array, property, reverse = false, sort_map = null) {
export const sortItems = function (array, property, reverse = false, sort_map = null) {
if (!array || array.length <= 0) {
return [];
}
function get_value(value) {
const split = property.split('.');
for (const property_element of split) {
for (const 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) {
@ -1324,7 +1350,7 @@ export const sortItems = function (array, property, reverse = false, sort_map =
return value;
}
function compare(a, b) {
function compare(a, b) {
let a_value = get_value(a);
let b_value = get_value(b);
@ -1382,7 +1408,7 @@ export const sortItems = function (array, property, reverse = false, sort_map =
* @return Array
* */
export const shuffle = function (array) {
let j; let x; let
let j; let x; let
i;
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
@ -1424,11 +1450,11 @@ export let isObject = function (value) {
* @param key = string (the string to lookup)
* @return boolean
* */
export const isLoading = function (load_queue = [], keys = []) {
export const isLoading = function (load_queue = [], keys = []) {
// Loop all of our load queue items
for (const load_queue_key in load_queue) {
for (const load_queue_key in load_queue) {
// Make sure it's not a root object method
if (load_queue.hasOwnProperty(load_queue_key)) {
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])) {
@ -1515,7 +1541,7 @@ export const scrollTo = function (target = null, smooth_scroll = false) {
// Target is a string representing a DOM element by class/id
} else if (target) {
} else if (target) {
let element = null;
if (target.charAt(0) == '#') {
@ -1567,4 +1593,4 @@ export const upgradeSpotifyPlaylistUris = function (uris) {
// As above, but for a single URI
export let upgradeSpotifyPlaylistUri = function (uri) {
return upgradeSpotifyPlaylistUris([uri])[0];
};
};

View File

@ -10,49 +10,56 @@ const helpers = require('../../helpers');
* @param endpoint = String
* @param params = String
* */
const sendRequest = (dispatch, endpoint, params) => new Promise((resolve, reject) => {
const loader_key = helpers.generateGuid();
dispatch(uiActions.startLoading(loader_key, `,discogs_${endpoint}`));
const sendRequest = (dispatch, getState, endpoint, params) => new Promise((resolve, reject) => {
const key = 'CXIwsVMAjrXIVitBWgqd';
const secret = 'KiEUfwKpebxRnEHlKoXnYIftJxeuqjTK';
const url = `https://api.discogs.com/${endpoint}?${params}`;
const loader_key = helpers.generateGuid();
dispatch(uiActions.startLoading(loader_key, `discogs_${endpoint}`));
const checkRateLimit = (request) => {
const rate = {
limit: request.getResponseHeader('X-Discogs-Ratelimit'),
remaining: request.getResponseHeader('X-Discogs-Ratelimit-Remaining'),
used: request.getResponseHeader('X-Discogs-Ratelimit-Used'),
}
if (rate.remaining !== undefined && rate.remaining === 0) {
dispatch(uiActions.createNotification({
key: "discogs_rate_limit",
type: "bad",
content: "Rate limit exceeded",
description: `Discogs rate limit exceeded, try again in a few minutes.`
}));
}
}
const config = {
method: 'GET',
cache: true,
timeout: 30000,
url: `https://api.discogs.com/${endpoint}?key=${key}&secret=${secret}&${params}`,
url: url,
crossDomain: true,
headers: {
'user-agent': 'Iris/1.0',
'User-Agent': 'Iris/1.0',
'Authorization': `Discogs key=${key}, secret=${secret}`
},
};
$.ajax(config).then(
(response) => {
(data, textStatus, request) => {
dispatch(uiActions.stopLoading(loader_key));
if (response.error) {
reject({
config,
error: response,
});
} else {
resolve(response);
}
checkRateLimit(request);
resolve(data);
},
(xhr, status, error) => {
dispatch(uiActions.stopLoading(loader_key));
// Snatch a more meaningful error
let description = null;
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
description = xhr.responseJSON.message;
}
checkRateLimit(xhr);
reject({
config,
error,
description,
status,
xhr,
});
@ -60,45 +67,62 @@ const sendRequest = (dispatch, endpoint, params) => new Promise((resolve, reject
);
});
export function findArtist(name) {
return sendRequest(dispatch, `artists/${artist.mbid}`, 'inc=url-rels')
.then(
(response) => {
if (response) {
const image_relations = response.relations.filter((rel) => rel.type == 'image');
if (image_relations) {
const updated_artist = {
uri,
images: image_relations.map((relation) => relation.url.resource.replace('File:', 'Special:FilePath/')),
};
dispatch(coreActions.artistLoaded(updated_artist));
}
}
},
(error) => {
console.log(`Musicbrainz: No results for ${artist.mbid}`);
},
);
}
export function getArtistImages(uri, artist) {
return (dispatch, getState) => {
sendRequest(dispatch, 'database/search', `query=${artist.name}`)
sendRequest(dispatch, getState, 'database/search', `type=artist&query=${artist.name}`)
.then(
(response) => {
if (response) {
if (response.results && response.results[0].cover_image) {
if (response.results.length > 0 && response.results[0].cover_image !== undefined) {
const updated_artist = {
uri,
images: [response.results[0].cover_image],
};
dispatch(coreActions.artistLoaded(updated_artist));
} else {
console.log(`Discogs: No results for artist ${artist.name}`);
}
}
},
(error) => {
console.log(`Discogs: No results for ${artist.name}`);
console.error(error);
},
);
/*
const finalise = (cover_image, cache) => {
const updated_artist = {
uri,
images: [cover_image],
};
dispatch(coreActions.artistLoaded(updated_artist));
if (cache) {
helpers.cache.set(`discogs_${artist.name}`, cover_image);
}
}
const cached = helpers.cache.get(`discogs_${artist.name}`);
if (cached) {
finalise(cached);
return;
}
sendRequest(dispatch, getState, 'database/search', `type=artist&query=${artist.name}`)
.then(
(response) => {
if (response) {
if (response.results.length > 0 && response.results[0].cover_image !== undefined) {
finalise(response.results[0].cover_image, true);
} else {
console.log(`Discogs: No results for artist ${artist.name}`);
}
}
},
(error) => {
console.error(error);
},
);
*/
};
}

View File

@ -20,7 +20,7 @@ export function set(data) {
* @param data mixed = request payload
* @return Promise
* */
const sendRequest = (dispatch, getState, endpoint, method = 'GET', data = false) => new Promise((resolve, reject) => {
const sendRequest = (dispatch, getState, endpoint, method = 'GET', data = false) => new Promise((resolve, reject) => {
if (endpoint.startsWith('https://') || endpoint.startsWith('http://')) {
var url = endpoint;
} else {
@ -135,7 +135,7 @@ export function getMe() {
* @param path = String, the relative API path for the HTML lyrics
* */
export function getTrackLyrics(uri, path) {
return (dispatch, getState) => {
return (dispatch, getState) => {
dispatch(coreActions.trackLoaded({
uri,
lyrics: null,
@ -160,7 +160,7 @@ export function getTrackLyrics(uri, path) {
if (response && response.result) {
const html = $(response.result);
let lyrics = html.find('.lyrics');
if (lyrics.length > 0) {
if (lyrics.length > 0) {
lyrics = lyrics.first();
lyrics.find('a').replaceWith(function () {
return this.innerHTML;
@ -181,7 +181,7 @@ export function getTrackLyrics(uri, path) {
'Could not get track lyrics',
response.error,
));
}
}
},
(xhr, status, error) => {
dispatch(uiActions.stopLoading(loader_key));
@ -195,7 +195,7 @@ export function getTrackLyrics(uri, path) {
}
export function findTrackLyrics(track) {
return (dispatch, getState) => {
return (dispatch, getState) => {
let query = '';
query += `${track.artists[0].name} `;
query += track.name;

View File

@ -1,4 +1,5 @@
import axios from 'axios';
const coreActions = require('../core/actions');
const uiActions = require('../ui/actions');
const helpers = require('../../helpers');
@ -18,57 +19,47 @@ export function set(data) {
* @param params = string, the url params to send
* @params signed = boolean, whether we've got a signed request with baked-in api_key
* */
const sendRequest = (dispatch, getState, params, signed = false) => new Promise((resolve, reject) => {
const loader_key = helpers.generateGuid();
const sendRequest = (dispatch, getState, params, signed = false) => new Promise((resolve, reject) => {
let url = `https://ws.audioscrobbler.com/2.0/?format=json&${params}`;
let http_method = 'GET';
let method = params.substring(params.indexOf('method=') + 7, params.length);
method = method.substring(0, method.indexOf('&'));
dispatch(uiActions.startLoading(loader_key, `lastfm_${method}`));
const config = {
method: 'GET',
cache: true,
timeout: 30000,
url: `https://ws.audioscrobbler.com/2.0/?format=json&${params}`,
};
// Signed requests don't need our api_key as the proxy has it's own
if (!signed) {
config.url += '&api_key=4320a3ef51c9b3d69de552ac083c55e3';
url += '&api_key=4320a3ef51c9b3d69de552ac083c55e3';
} else {
config.method = 'POST';
http_method = 'POST';
}
$.ajax(config).then(
(response) => {
dispatch(uiActions.stopLoading(loader_key));
if (response.error) {
reject({
config,
error: response,
});
} else {
resolve(response);
}
},
(xhr, status, error) => {
dispatch(uiActions.stopLoading(loader_key));
const config = {
method: http_method,
timeout: 30000,
cache: "force-cache",
};
// Snatch a more meaningful error
let description = null;
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
description = xhr.responseJSON.message;
}
const loader_key = helpers.generateGuid();
dispatch(uiActions.startLoading(loader_key, `lastfm_${method}`));
reject({
config,
error,
description,
status,
xhr,
});
},
);
function status(response) {
dispatch(uiActions.stopLoading(loader_key));
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
} else {
return Promise.reject(new Error(response.statusText))
}
}
fetch(url, config)
.then(status)
.then(response => response.json())
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
/**
@ -79,7 +70,7 @@ const sendRequest = (dispatch, getState, params, signed = false) => new Promise(
* @param params = string, the url params to send
* @param signed = boolean
* */
const sendSignedRequest = (dispatch, getState, params) => new Promise((resolve, reject) => {
const sendSignedRequest = (dispatch, getState, params) => new Promise((resolve, reject) => {
// Not authorized
if (!getState().lastfm.authorization) {
reject({
@ -227,7 +218,7 @@ export function getTrack(uri) {
};
}
export function getArtist(uri, artist, mbid = false) {
export function getArtist(uri, artist, mbid = false) {
return (dispatch, getState) => {
if (mbid) {
var params = `method=artist.getInfo&mbid=${mbid}`;
@ -271,7 +262,7 @@ export function getAlbum(uri, artist, album, mbid = false) {
sendRequest(dispatch, getState, params)
.then(
(response) => {
if (response.album) {
if (response.album) {
const existing_album = getState().core.albums[uri];
const album = {
@ -303,10 +294,10 @@ export function getAlbum(uri, artist, album, mbid = false) {
export function getImages(context, uri) {
return (dispatch, getState) => {
return (dispatch, getState) => {
let record = getState().core[context][uri];
if (record) {
switch (context) {
switch (context) {
case 'tracks':
if (record.mbid) {

View File

@ -14,7 +14,7 @@ const googleActions = require('../google/actions.js');
const lastfmActions = require('../lastfm/actions.js');
const discogsActions = require('../discogs/actions.js');
const MopidyMiddleware = (function () {
const MopidyMiddleware = (function () {
// container for the actual Mopidy socket
let socket = null;
@ -23,13 +23,13 @@ const MopidyMiddleware = (function () {
let progress_interval_counter = 0;
// handle all manner of socket messages
const handleMessage = (ws, store, type, data) => {
const handleMessage = (ws, store, type, data) => {
// if debug enabled
if (store.getState().ui.log_mopidy) {
console.log('Mopidy', type, data);
}
switch (type) {
switch (type) {
case 'state:online':
store.dispatch({ type: 'MOPIDY_CONNECTED' });
@ -46,7 +46,7 @@ const MopidyMiddleware = (function () {
// Every 1s update our play position (when playing)
progress_interval = setInterval(() => {
if (store.getState().mopidy.play_state == 'playing') {
if (store.getState().mopidy.play_state == 'playing') {
// Every 10s get real position from server, provided we're in-focus
if (progress_interval_counter % 5 == 0 && store.getState().ui.window_focus === true) {
store.dispatch(mopidyActions.getTimePosition());
@ -221,7 +221,7 @@ const MopidyMiddleware = (function () {
* it just proceeds to the next middleware, or default functionality
* */
return (store) => (next) => (action) => {
switch (action.type) {
switch (action.type) {
case 'MOPIDY_CONNECT':
if (socket != null) {
socket.close();
@ -358,7 +358,7 @@ const MopidyMiddleware = (function () {
store.dispatch({
type: 'MOPIDY_PLAY_STATE',
play_state: 'paused',
});
});
store.dispatch(pusherActions.deliverBroadcast(
'notification',
{
@ -600,7 +600,7 @@ const MopidyMiddleware = (function () {
var playlist = store.getState().core.playlists[action.uri];
// We have the playlist loaded already, and we've got at least 1 track to start playing
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) {
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) {
// We've got all of the tracks, so just play those; no further action required
if (playlist.tracks_total == playlist.tracks_uris.length) {
store.dispatch(mopidyActions.playURIs(playlist.tracks_uris, action.uri, action.shuffle));
@ -648,7 +648,7 @@ const MopidyMiddleware = (function () {
var playlist = store.getState().core.playlists[action.uri];
// We have the playlist loaded already, and we've got at least 1 track to start playing
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) {
if (playlist && playlist.tracks_uris && playlist.tracks_uris.length > 0) {
// We've got all of the tracks, so just play those; no further action required
if (playlist.tracks_total == playlist.tracks_uris.length) {
let tracks_uris = Object.assign([], playlist.tracks_uris);
@ -752,7 +752,7 @@ const MopidyMiddleware = (function () {
return;
// make sure we have some uris in the queue
} if (action.data.batches && action.data.batches.length > 0) {
} if (action.data.batches && action.data.batches.length > 0) {
var batches = Object.assign([], action.data.batches);
var batch = batches[0];
let total_uris = 0;
@ -790,7 +790,7 @@ const MopidyMiddleware = (function () {
var params = { uris: batch.uris };
// Play this batch next
if (batch.play_next) {
if (batch.play_next) {
// Make sure we're playing something first
if (current_track_index > -1) {
params.at_position = current_track_index + batch.offset + 1;
@ -864,7 +864,7 @@ const MopidyMiddleware = (function () {
// add our first track
request(socket, store, 'tracklist.add', { uri: first_uri, at_position: 0 })
.then(
(response) => {
(response) => {
// play it (only if we got a successful lookup)
if (response.length > 0) {
store.dispatch(mopidyActions.changeTrack(response[0].tlid));
@ -885,7 +885,7 @@ const MopidyMiddleware = (function () {
action.uris.splice(first_uri_index, 1);
// And add the rest of our uris (if any)
if (action.uris.length > 0) {
if (action.uris.length > 0) {
// Wait a moment so the server can trigger track_changed etc
// this means our UI feels snappier as the first track shows up quickly
setTimeout(
@ -1010,7 +1010,7 @@ const MopidyMiddleware = (function () {
},
));
switch (action.data.context) {
switch (action.data.context) {
// Albums
case 'albums':
@ -1283,7 +1283,7 @@ const MopidyMiddleware = (function () {
case 'all':
default:
var process_tracks = () => {
var process_tracks = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
@ -1324,7 +1324,7 @@ const MopidyMiddleware = (function () {
);
};
var process_albums = () => {
var process_albums = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
@ -1385,7 +1385,7 @@ const MopidyMiddleware = (function () {
);
};
var process_artists = () => {
var process_artists = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
@ -1454,7 +1454,7 @@ const MopidyMiddleware = (function () {
},
);
};
var process_playlists = () => {
var process_playlists = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
@ -1473,7 +1473,7 @@ const MopidyMiddleware = (function () {
request(socket, store, 'playlists.asList')
.then(
(response) => {
if (response.length > 0) {
if (response.length > 0) {
let playlists_uris = [];
for (var i = 0; i < response.length; i++) {
const playlist = response[i];
@ -1543,7 +1543,7 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_GET_LIBRARY_PLAYLISTS':
request(socket, store, 'playlists.asList')
.then((response) => {
.then((response) => {
// drop in our URI list
const playlist_uris = helpers.arrayOf('uri', response);
const playlist_uris_filtered = [];
@ -1614,9 +1614,9 @@ const MopidyMiddleware = (function () {
request(socket, store, 'library.lookup', { uris })
.then((response) => {
for (const uri in response) {
if (response.hasOwnProperty(uri)) {
if (response.hasOwnProperty(uri)) {
var track = response[uri][0];
if (track) {
if (track) {
// find the track reference, and drop in the full track data
function getByURI(trackReference) {
return track.uri == trackReference.uri;
@ -1701,11 +1701,11 @@ const MopidyMiddleware = (function () {
// Even though we have the full playlist in our index, our "playlists.save" request
// requires a Mopidy playlist object (with updates)
request(socket, store, 'playlists.lookup', { uri: action.key })
.then((response) => {
.then((response) => {
const mopidy_playlist = { ...response, name: action.name };
request(socket, store, 'playlists.save', { playlist: mopidy_playlist })
.then((response) => {
.then((response) => {
// Overwrite our playlist with the response to our save
// This is essential to get the updated URI from Mopidy
const playlist = {
@ -1717,7 +1717,7 @@ const MopidyMiddleware = (function () {
// When we rename a playlist, the URI also changes to reflect the name change.
// We need to update our index, as well as redirect our current page URL.
if (action.key !== playlist.uri) {
if (action.key !== playlist.uri) {
// Remove old playlist (by old key/uri) from index
// By providing the new key, the old playlist gets replaced with a redirector object
store.dispatch(coreActions.removeFromIndex('playlists', action.key, playlist.uri));
@ -1732,7 +1732,7 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_REORDER_PLAYLIST_TRACKS':
request(socket, store, 'playlists.lookup', { uri: action.key })
.then((response) => {
.then((response) => {
let playlist = { ...response };
const tracks = Object.assign([], playlist.tracks);
const tracks_to_move = [];
@ -1744,7 +1744,7 @@ const MopidyMiddleware = (function () {
if (insert_before > range_start) insert_before -= range_length;
// collate our tracks to be moved
for (var i = 0; i < range_length; i++) {
for (var i = 0; i < range_length; i++) {
// add to FRONT: we work backwards to avoid screwing up our indexes
tracks_to_move.unshift(tracks[range_start + i]);
}
@ -1824,7 +1824,7 @@ const MopidyMiddleware = (function () {
remaining: uris.length,
},
));
});
});
} else if (last_run.status == 'cancelled') {
store.dispatch(uiActions.resumeProcess('MOPIDY_LIBRARY_ALBUMS_PROCESSOR'));
} else if (last_run.status == 'finished') {
@ -1984,7 +1984,7 @@ const MopidyMiddleware = (function () {
if (response.length <= 0) return;
const uris = [];
for (let i = 0; i < response.length; i++) {
for (let i = 0; i < response.length; i++) {
// Convert local URI to actual artist URI
// See https://github.com/mopidy/mopidy-local-sqlite/issues/39
response[i].uri = response[i].uri.replace('local:directory?albumartist=', '');
@ -2058,7 +2058,7 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_GET_ARTIST':
request(socket, store, 'library.lookup', action.data)
.then((response) => {
.then((response) => {
if (response.length <= 0) {
return;
}
@ -2094,7 +2094,7 @@ const MopidyMiddleware = (function () {
// Get the artist object from the track. This is a bit ugly because it's a simplified
// (Mopidy) artist object but gives us enough to fetch their name and artwork.
for (const raw_artist of response[0].artists) {
for (const raw_artist of response[0].artists) {
// We're only interested in the artist we asked for
if (raw_artist.uri === artist.uri) {
artist = { ...raw_artist };
@ -2109,7 +2109,7 @@ const MopidyMiddleware = (function () {
// Load supprting information from LastFM and Discogs
const existing_artist = store.getState().core.artists[artist.uri];
if (existing_artist) {
if (existing_artist) {
if (!existing_artist.images) {
store.dispatch(discogsActions.getArtistImages(artist.uri, artist));
}
@ -2208,7 +2208,7 @@ const MopidyMiddleware = (function () {
store.dispatch(uiActions.setCurrentTrackTransition(false));
var track = helpers.formatTrack(action.tl_track);
if (track.uri) {
if (track.uri) {
// Deliver the data we've got already
store.dispatch({
type: 'CURRENT_TRACK_LOADED',
@ -2217,7 +2217,7 @@ const MopidyMiddleware = (function () {
});
// Now attempt to get supporting images
if (store.getState().core.tracks[track.uri] === undefined || store.getState().core.tracks[track.uri].images === undefined) {
if (store.getState().core.tracks[track.uri] === undefined || store.getState().core.tracks[track.uri].images === undefined) {
// We've got Spotify running, and it's a spotify track - go straight to the source!
if (store.getState().spotify.enabled && helpers.uriSource(track.uri) == 'spotify') {
store.dispatch(spotifyActions.getTrack(track.uri));
@ -2234,7 +2234,7 @@ const MopidyMiddleware = (function () {
request(socket, store, 'tracklist.getNextTlid')
.then(
(response) => {
if (response && response >= 0) {
if (response && response >= 0) {
// Get the full track object from our tracklist
// We know it will be here, as the tlid refers to an item in this list
const track = helpers.applyFilter('tlid', response, store.getState().core.queue, true);
@ -2246,7 +2246,7 @@ const MopidyMiddleware = (function () {
});
// We don't have the track (including images) already in our index
if (store.getState().core.tracks[track.uri] === undefined || store.getState().core.tracks[track.uri].images === undefined) {
if (store.getState().core.tracks[track.uri] === undefined || store.getState().core.tracks[track.uri].images === undefined) {
// We've got Spotify running, and it's a spotify track - go straight to the source!
if (store.getState().spotify.enabled && helpers.uriSource(track.uri) == 'spotify') {
store.dispatch(spotifyActions.getTrack(track.uri));
@ -2300,7 +2300,7 @@ const MopidyMiddleware = (function () {
records.push({
uri,
images,
});
});
} else {
store.dispatch(lastfmActions.getImages(action.context, uri));
}
@ -2330,7 +2330,7 @@ const MopidyMiddleware = (function () {
});
request(socket, store, 'library.browse', action.data)
.then((response) => {
.then((response) => {
const tracks_uris = [];
const subdirectories = [];
@ -2388,7 +2388,7 @@ const MopidyMiddleware = (function () {
default:
return next(action);
}
};
};
}());
export default MopidyMiddleware;

View File

@ -8,7 +8,7 @@ import TrackList from '../components/TrackList';
import Thumbnail from '../components/Thumbnail';
import Parallax from '../components/Parallax';
import ArtistSentence from '../components/ArtistSentence';
import ArtistGrid from '../components/ArtistGrid';
import Loader from '../components/Loader';
import FollowButton from '../components/Fields/FollowButton';
import NiceNumber from '../components/NiceNumber';
import Dater from '../components/Dater';
@ -139,21 +139,17 @@ export class Album extends React.Component {
`spotify_albums/${helpers.getFromUri('albumid', this.props.uri)}`,
])
) {
return (
<div className="body-loader loading">
<div className="loader" />
</div>
);
return <Loader body loading />;
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
Could not find album with URI "
{encodeURIComponent(this.props.uri)}
{encodeURIComponent(this.props.uri)}
"
</p>
</ErrorMessage>
);
);
}
const album = helpers.collate(this.props.album, {
@ -198,12 +194,12 @@ export class Album extends React.Component {
<Dater type="date" data={album.release_date} />
</li>
) : null}
{album.tracks ? (
<li>
{album.tracks.length}
{' '}
tracks
</li>
{album.tracks ? (
<li>
{album.tracks.length}
{' '}
tracks
</li>
) : null}
{!this.props.slim_mode && album.tracks ? (
<li>
@ -212,15 +208,15 @@ tracks
) : null}
{!this.props.slim_mode && album.play_count ? (
<li>
<NiceNumber value={album.play_count} />
{' '}
<NiceNumber value={album.play_count} />
{' '}
plays
</li>
) : null}
{!this.props.slim_mode && album.listeners ? (
<li>
<NiceNumber value={album.listeners} />
{' '}
<NiceNumber value={album.listeners} />
{' '}
listeners
</li>
) : null}
@ -263,8 +259,8 @@ listeners
<p>{album.wiki}</p>
<br />
<div className="mid_grey-text">
Published:
{' '}
Published:
{' '}
{album.wiki_publish_date}
</div>
</div>

View File

@ -2,7 +2,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Redirect } from 'react-router';
import { Route, Switch } from 'react-router-dom';
import ErrorMessage from '../components/ErrorMessage';
@ -18,6 +17,7 @@ import FollowButton from '../components/Fields/FollowButton';
import ContextMenuTrigger from '../components/ContextMenuTrigger';
import DropdownField from '../components/Fields/DropdownField';
import Icon from '../components/Icon';
import Loader from '../components/Loader';
import * as helpers from '../helpers';
import * as coreActions from '../services/core/actions';
@ -27,7 +27,7 @@ import * as pusherActions from '../services/pusher/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions';
class Artist extends React.Component {
class Artist extends React.Component {
constructor(props) {
super(props);
}
@ -37,9 +37,9 @@ class Artist extends React.Component {
this.props.coreActions.loadArtist(this.props.uri);
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps) {
if (nextProps.uri != this.props.uri) {
this.props.coreActions.loadArtist(nextProps.uri);
this.props.coreActions.loadArtist(nextProps.uri);
} else if (!this.props.mopidy_connected && nextProps.mopidy_connected) {
if (helpers.uriSource(this.props.uri) != 'spotify') {
this.props.coreActions.loadArtist(nextProps.uri);
@ -180,17 +180,17 @@ class Artist extends React.Component {
<div className="col col--w5" />
{artist.related_artists && artist.related_artists.length > 0 ? (
<div className="col col--w25 related-artists">
<h4>Related artists</h4>
<div className="list-wrapper"><RelatedArtists artists={artist.related_artists.slice(0, 6)} /></div>
<Link to={`/artist/${encodeURIComponent(this.props.uri)}/related-artists`} scrollTo="#sub-views-menu" className="button button--default">All related artists</Link>
</div>
{artist.related_artists && artist.related_artists.length > 0 ? (
<div className="col col--w25 related-artists">
<h4>Related artists</h4>
<div className="list-wrapper"><RelatedArtists artists={artist.related_artists.slice(0, 6)} /></div>
<Link to={`/artist/${encodeURIComponent(this.props.uri)}/related-artists`} scrollTo="#sub-views-menu" className="button button--default">All related artists</Link>
</div>
) : null}
<div className="cf" />
{artist.albums ? (
{artist.albums ? (
<div className="albums">
<h4>
Albums
@ -200,20 +200,20 @@ class Artist extends React.Component {
value={this.props.sort}
options={sort_options}
selected_icon={this.props.sort ? (this.props.sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
handleChange={(value) => { this.setSort(value); this.props.uiActions.hideContextMenu(); }}
handleChange={(value) => { this.setSort(value); this.props.uiActions.hideContextMenu(); }}
/>
<DropdownField
icon="filter_list"
name="Filter"
value={this.props.filter}
options={filter_options}
handleChange={(value) => { this.setFilter(value); this.props.uiActions.hideContextMenu(); }}
handleChange={(value) => { this.setFilter(value); this.props.uiActions.hideContextMenu(); }}
/>
{this.props.sort || this.props.filter ? (
{this.props.sort || this.props.filter ? (
<a className="button button--discrete button--small" onClick={(e) => { this.setFilter(null); this.setSort(null); }}>
<Icon name="clear" />
<Icon name="clear" />
Reset
</a>
</a>
) : null}
</h4>
@ -222,10 +222,10 @@ Reset
<LazyLoadListener
loadKey={artist.albums_more}
showLoader={artist.albums_more}
loadMore={() => this.loadMore()}
loadMore={() => this.loadMore()}
/>
</section>
</div>
</div>
) : null}
</div>
);
@ -283,7 +283,7 @@ Reset
const thumbnails = [];
if (artist.images && artist.images.length > 0) {
for (const images of artist.images) {
for (const images of artist.images) {
// Capture potential 'null' images
// TODO: Prevent these from existing in the first place
if (images.huge) {
@ -301,52 +301,52 @@ Reset
<div className="col col--w40 tiles artist-stats">
{thumbnails}
{artist.followers ? (
<div className="tile">
<span className="content">
<Icon type="fontawesome" name="users" />
{artist.followers.toLocaleString()}
{' '}
followers
</span>
</div>
{artist.followers ? (
<div className="tile">
<span className="content">
<Icon type="fontawesome" name="users" />
{artist.followers.toLocaleString()}
{' '}
followers
</span>
</div>
) : null}
{artist.popularity ? (
<div className="tile">
<span className="content">
<Icon type="fontawesome" name="fire" />
{artist.popularity }
% popularity
</span>
</div>
{artist.popularity ? (
<div className="tile">
<span className="content">
<Icon type="fontawesome" name="fire" />
{artist.popularity }
% popularity
</span>
</div>
) : null}
{artist.listeners ? (
<div className="tile">
<span className="content">
<Icon type="fontawesome" name="headphones" />
{ artist.listeners.toLocaleString() }
{' '}
listeners
</span>
</div>
{artist.listeners ? (
<div className="tile">
<span className="content">
<Icon type="fontawesome" name="headphones" />
{ artist.listeners.toLocaleString() }
{' '}
listeners
</span>
</div>
) : null }
</div>
<div className="col col--w60 biography">
<section>
{ artist.biography ? (
<div className="biography-text">
<p>{artist.biography}</p>
{ artist.biography ? (
<div className="biography-text">
<p>{artist.biography}</p>
<br />
<div className="mid_grey-text">
Published:
{ artist.biography_publish_date }
<div className="mid_grey-text">
Published:
{ artist.biography_publish_date }
</div>
<div className="mid_grey-text">
Origin:
<a href={artist.biography_link} target="_blank">{ artist.biography_link }</a>
</div>
</div>
<div className="mid_grey-text">
Origin:
<a href={artist.biography_link} target="_blank">{ artist.biography_link }</a>
</div>
</div>
) : null }
</section>
</div>
@ -359,21 +359,17 @@ Origin:
if (!this.props.artist) {
if (helpers.isLoading(this.props.load_queue, [`spotify_artists/${helpers.getFromUri('artistid', this.props.uri)}`, 'lastfm_method=artist.getInfo'])) {
return (
<div className="body-loader loading">
<div className="loader" />
</div>
);
return <Loader body loading />;
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
Could not find artist with URI "
{encodeURIComponent(this.props.uri)}
"
<p>
Could not find artist with URI "
{encodeURIComponent(this.props.uri)}
"
</p>
</ErrorMessage>
);
);
}
if (this.props.artist && this.props.artist.images && this.props.artist.images.length > 0) {
@ -419,33 +415,33 @@ Could not find artist with URI "
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}`}
scrollTo="#sub-views-menu"
scrollTo="#sub-views-menu"
>
<h4>Overview</h4>
</Link>
{this.props.artist.tracks_uris && this.props.artist.tracks_uris.length > 10 ? (
{this.props.artist.tracks_uris && this.props.artist.tracks_uris.length > 10 ? (
<Link
exact
history={this.props.history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}/tracks`}
scrollTo="#sub-views-menu"
scrollTo="#sub-views-menu"
>
<h4>Tracks</h4>
</Link>
</Link>
) : null}
{this.props.artist.related_artists_uris ? (
{this.props.artist.related_artists_uris ? (
<Link
exact
history={this.props.history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}/related-artists`}
scrollTo="#sub-views-menu"
scrollTo="#sub-views-menu"
>
<h4>Related artists</h4>
</Link>
</Link>
) : null}
<Link
exact
@ -453,7 +449,7 @@ Could not find artist with URI "
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}/about`}
scrollTo="#sub-views-menu"
scrollTo="#sub-views-menu"
>
<h4>About</h4>
</Link>

View File

@ -14,7 +14,7 @@ import * as pusherActions from '../services/pusher/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
class Debug extends React.Component {
class Debug extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -48,10 +48,10 @@ class Debug extends React.Component {
setTimeout(location.reload(), 1000);
}
render() {
render() {
const options = (
<a className="button button--default button--no-hover" onClick={(e) => this.props.history.push('/settings')}>
<Icon name="keyboard_backspace" />
<Icon name="keyboard_backspace" />
Back
</a>
);
@ -81,7 +81,7 @@ Back
type="checkbox"
name="debug_info"
checked={this.props.debug_info}
onChange={(e) => this.props.uiActions.set({ debug_info: !this.props.debug_info })}
onChange={(e) => this.props.uiActions.set({ debug_info: !this.props.debug_info })}
/>
<span className="label">Show debug overlay</span>
</label>
@ -95,7 +95,7 @@ Back
type="checkbox"
name="log_actions"
checked={this.props.log_actions}
onChange={(e) => this.props.uiActions.set({ log_actions: !this.props.log_actions })}
onChange={(e) => this.props.uiActions.set({ log_actions: !this.props.log_actions })}
/>
<span className="label">Log actions</span>
</label>
@ -104,7 +104,7 @@ Back
type="checkbox"
name="log_mopidy"
checked={this.props.log_mopidy}
onChange={(e) => this.props.uiActions.set({ log_mopidy: !this.props.log_mopidy })}
onChange={(e) => this.props.uiActions.set({ log_mopidy: !this.props.log_mopidy })}
/>
<span className="label">Log Mopidy</span>
</label>
@ -113,7 +113,7 @@ Back
type="checkbox"
name="log_pusher"
checked={this.props.log_pusher}
onChange={(e) => this.props.uiActions.set({ log_pusher: !this.props.log_pusher })}
onChange={(e) => this.props.uiActions.set({ log_pusher: !this.props.log_pusher })}
/>
<span className="label">Log Pusher</span>
</label>
@ -123,6 +123,7 @@ Back
<div className="name" />
<div className="input">
<a className="button button--default" onClick={(e) => this.props.uiActions.createNotification({ content: 'Test notification' })}>Create notification</a>
<a className="button button--default" onClick={(e) => this.props.uiActions.startProcess('test_process', "Test process", {remaining: 50, total: 100})}>Create process notification</a>
<a className="button button--default" onClick={(e) => this.props.pusherActions.request('test')}>Run test process</a>
</div>
</div>
@ -135,7 +136,7 @@ Back
<input
type="text"
onChange={(e) => this.props.spotifyActions.authorizationGranted({ access_token: e.target.value })}
value={this.state.access_token}
value={this.state.access_token}
/>
</div>
</label>
@ -148,7 +149,7 @@ Back
<input
type="text"
onChange={(e) => this.setState({ mopidy_call: e.target.value })}
value={this.state.mopidy_call}
value={this.state.mopidy_call}
/>
</div>
</label>
@ -157,7 +158,7 @@ Back
<div className="input">
<textarea
onChange={(e) => this.setState({ mopidy_data: e.target.value })}
value={this.state.mopidy_data}
value={this.state.mopidy_data}
/>
</div>
</label>
@ -194,7 +195,7 @@ Back
<div className="input">
<textarea
onChange={(e) => this.setState({ pusher_data: e.target.value })}
value={this.state.pusher_data}
value={this.state.pusher_data}
/>
</div>
</label>
@ -235,4 +236,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Debug);
export default connect(mapStateToProps, mapDispatchToProps)(Debug);

View File

@ -11,10 +11,9 @@ import Thumbnail from '../components/Thumbnail';
import Parallax from '../components/Parallax';
import NiceNumber from '../components/NiceNumber';
import Dater from '../components/Dater';
import ConfirmationButton from '../components/Fields/ConfirmationButton';
import LazyLoadListener from '../components/LazyLoadListener';
import FollowButton from '../components/Fields/FollowButton';
import Header from '../components/Header';
import Loader from '../components/Loader';
import ContextMenuTrigger from '../components/ContextMenuTrigger';
import URILink from '../components/URILink';
import Icon from '../components/Icon';
@ -25,7 +24,7 @@ import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
class Playlist extends React.Component {
class Playlist extends React.Component {
constructor(props) {
super(props);
}
@ -46,7 +45,7 @@ class Playlist extends React.Component {
this.props.coreActions.loadPlaylist(this.props.uri);
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps) {
// Follow a URI moved_to instruction
if (this.props.playlist && nextProps.playlist && this.props.playlist.moved_to != nextProps.playlist.moved_to) {
this.props.history.push(`/playlist/${encodeURIComponent(nextProps.playlist.moved_to)}`);
@ -136,7 +135,7 @@ class Playlist extends React.Component {
}
renderActions() {
switch (helpers.uriSource(this.props.uri)) {
switch (helpers.uriSource(this.props.uri)) {
case 'm3u':
return (
<div className="actions">
@ -179,21 +178,17 @@ class Playlist extends React.Component {
if (!this.props.playlist) {
if (helpers.isLoading(this.props.load_queue, [`spotify_playlists/${playlist_id}?`])) {
return (
<div className="body-loader loading">
<div className="loader" />
</div>
);
return <Loader body loading />
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
Could not find playlist with URI "
{encodeURIComponent(this.props.uri)}
"
<p>
Could not find playlist with URI "
{encodeURIComponent(this.props.uri)}
"
</p>
</ErrorMessage>
);
);
}
const playlist = helpers.collate(this.props.playlist, { tracks: this.props.tracks, users: this.props.users });
@ -225,24 +220,24 @@ Could not find playlist with URI "
<ul className="details">
{!this.props.slim_mode ? <li className="source"><Icon type="fontawesome" name={helpers.sourceIcon(playlist.uri)} /></li> : null }
{playlist.user_uri ? <li><URILink type="user" uri={playlist.user_uri}>{playlist.user ? playlist.user.name : helpers.getFromUri('userid', playlist.user_uri)}</URILink></li> : null }
<li>
{playlist.tracks_total ? playlist.tracks_total : '0'}
{' '}
tracks
<li>
{playlist.tracks_total ? playlist.tracks_total : '0'}
{' '}
tracks
</li>
{!this.props.slim_mode && playlist.tracks && playlist.tracks_total > 0 ? <li><Dater type="total-time" data={playlist.tracks} /></li> : null }
{!this.props.slim_mode && playlist.followers !== undefined ? (
<li>
<NiceNumber value={playlist.followers} />
{' '}
followers
</li>
{!this.props.slim_mode && playlist.followers !== undefined ? (
<li>
<NiceNumber value={playlist.followers} />
{' '}
followers
</li>
) : null }
{!this.props.slim_mode && playlist.last_modified_date ? (
<li>
Edited
<Dater type="ago" data={playlist.last_modified_date} />
</li>
{!this.props.slim_mode && playlist.last_modified_date ? (
<li>
Edited
<Dater type="ago" data={playlist.last_modified_date} />
</li>
) : null }
</ul>
</div>
@ -256,12 +251,12 @@ Edited
track_context={context}
tracks={playlist.tracks}
removeTracks={(tracks_indexes) => this.removeTracks(tracks_indexes)}
reorderTracks={(indexes, index) => this.reorderTracks(indexes, index)}
reorderTracks={(indexes, index) => this.reorderTracks(indexes, index)}
/>
<LazyLoadListener
loadKey={playlist.tracks_more}
showLoader={is_loading_tracks || playlist.tracks_more}
loadMore={() => this.loadMore()}
loadMore={() => this.loadMore()}
/>
</section>
</div>
@ -269,7 +264,7 @@ Edited
}
}
const mapStateToProps = (state, ownProps) => {
const mapStateToProps = (state, ownProps) => {
// Decode the URI, and then re-encode selected characters
// This is needed as Mopidy encodes *some* characters in playlist URIs (but not other characters)
// We need to retain ":" because this a reserved URI separator
@ -305,4 +300,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Playlist);
export default connect(mapStateToProps, mapDispatchToProps)(Playlist);

View File

@ -111,10 +111,10 @@ class Queue extends React.Component {
return (
<div className="queue-stats mid_grey-text">
<span>
{this.props.current_tracklist.length}
{' '}
tracks
<span>
{this.props.current_tracklist.length}
{' '}
tracks
</span>
&nbsp;&nbsp;|&nbsp;&nbsp;
{this.props.current_tracklist.length > 0 ? (
@ -197,8 +197,7 @@ tracks
/>
</URILink>
<div className="current-track__added-from__text">
Playing from
{' '}
{`Playing from `}
<URILink type={item_type} uri={item.uri}>
{item.name}
</URILink>

View File

@ -21,7 +21,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions';
class Settings extends React.Component {
class Settings extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -127,22 +127,22 @@ class Settings extends React.Component {
return (
<span className={`${colour}-text`}>
<Icon className={className} name={icon} />
{' '}
<Icon className={className} name={icon} />
{' '}
{status}
</span>
);
}
render() {
render() {
const options = (
<span>
<a className="button button--default button--no-hover" onClick={(e) => this.props.history.push('/settings/debug')}>
<Icon name="code" />
<Icon name="code" />
Debug
</a>
<a className="button button--default button--no-hover" href="https://github.com/jaedb/Iris/wiki" target="_blank">
<Icon name="help" />
<Icon name="help" />
Help
</a>
</span>
@ -157,9 +157,9 @@ Help
<section className="content-wrapper">
<h4 className="underline">
Server
<a name="server" />
<h4 className="underline">
Server
<a name="server" />
</h4>
<div className="field">
@ -176,7 +176,7 @@ Server
<div className="input">
<TextField
onChange={(value) => this.props.pusherActions.setUsername(value.replace(/\W/g, ''))}
value={this.state.pusher_username}
value={this.state.pusher_username}
/>
<div className="description">
A non-unique string used to identify this client (no special characters)
@ -193,7 +193,7 @@ Server
onChange={(e) => this.setState({ mopidy_host: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'mopidy_host' })}
onBlur={(e) => this.setState({ input_in_focus: null })}
value={this.state.mopidy_host}
value={this.state.mopidy_host}
/>
</div>
</label>
@ -205,23 +205,23 @@ Server
onChange={(e) => this.setState({ mopidy_port: e.target.value })}
onFocus={(e) => this.setState({ input_in_focus: 'mopidy_port' })}
onBlur={(e) => this.setState({ input_in_focus: null })}
value={this.state.mopidy_port}
value={this.state.mopidy_port}
/>
</div>
</label>
{this.renderApplyButton()}
</form>
<h4 className="underline">
Services
<a name="services" />
<h4 className="underline">
Services
<a name="services" />
</h4>
<Route path="/settings/:service?" component={Services} />
<h4 className="underline">
Interface
<a name="interface" />
<h4 className="underline">
Interface
<a name="interface" />
</h4>
<div className="field radio">
@ -235,7 +235,7 @@ Interface
name="theme"
value="dark"
checked={this.props.ui.theme == 'dark'}
onChange={(e) => this.props.uiActions.set({ theme: e.target.value })}
onChange={(e) => this.props.uiActions.set({ theme: e.target.value })}
/>
<span className="label">Dark</span>
</label>
@ -245,7 +245,7 @@ Interface
name="theme"
value="light"
checked={this.props.ui.theme == 'light'}
onChange={(e) => this.props.uiActions.set({ theme: e.target.value })}
onChange={(e) => this.props.uiActions.set({ theme: e.target.value })}
/>
<span className="label">Light</span>
</label>
@ -260,7 +260,7 @@ Interface
type="checkbox"
name="log_actions"
checked={this.props.ui.clear_tracklist_on_play}
onChange={(e) => this.props.uiActions.set({ clear_tracklist_on_play: !this.props.ui.clear_tracklist_on_play })}
onChange={(e) => this.props.uiActions.set({ clear_tracklist_on_play: !this.props.ui.clear_tracklist_on_play })}
/>
<span className="label tooltip">
Clear tracklist on play of URI(s)
@ -272,7 +272,7 @@ Interface
type="checkbox"
name="hotkeys_enabled"
checked={this.props.ui.hotkeys_enabled}
onChange={(e) => this.props.uiActions.set({ hotkeys_enabled: !this.props.ui.hotkeys_enabled })}
onChange={(e) => this.props.uiActions.set({ hotkeys_enabled: !this.props.ui.hotkeys_enabled })}
/>
<span className="label">
Enable hotkeys
@ -283,7 +283,7 @@ Interface
type="checkbox"
name="smooth_scrolling_enabled"
checked={this.props.ui.smooth_scrolling_enabled}
onChange={(e) => this.props.uiActions.set({ smooth_scrolling_enabled: !this.props.ui.smooth_scrolling_enabled })}
onChange={(e) => this.props.uiActions.set({ smooth_scrolling_enabled: !this.props.ui.smooth_scrolling_enabled })}
/>
<span className="label">
Enable smooth scrolling
@ -294,7 +294,7 @@ Interface
type="checkbox"
name="playback_controls_touch_enabled"
checked={this.props.ui.playback_controls_touch_enabled}
onChange={(e) => this.props.uiActions.set({ playback_controls_touch_enabled: !this.props.ui.playback_controls_touch_enabled })}
onChange={(e) => this.props.uiActions.set({ playback_controls_touch_enabled: !this.props.ui.playback_controls_touch_enabled })}
/>
<span className="label tooltip">
Enable touch events on play controls
@ -306,7 +306,7 @@ Interface
type="checkbox"
name="wide_scrollbar_enabled"
checked={this.props.ui.wide_scrollbar_enabled}
onChange={(e) => this.props.uiActions.set({ wide_scrollbar_enabled: !this.props.ui.wide_scrollbar_enabled })}
onChange={(e) => this.props.uiActions.set({ wide_scrollbar_enabled: !this.props.ui.wide_scrollbar_enabled })}
/>
<span className="label">
Use wide scrollbars
@ -323,7 +323,7 @@ Interface
<SourcesPriority
uri_schemes={this.props.mopidy.uri_schemes ? this.props.mopidy.uri_schemes : []}
uri_schemes_priority={this.props.ui.uri_schemes_priority ? this.props.ui.uri_schemes_priority : []}
uiActions={this.props.uiActions}
uiActions={this.props.uiActions}
/>
<div className="description">
Drag-and-drop to prioritize search providers and results
@ -331,7 +331,7 @@ Interface
</div>
</div>
{helpers.isHosted() ? null : (
{helpers.isHosted() ? null : (
<div className="field checkbox">
<div className="name">Reporting</div>
<div className="input">
@ -340,19 +340,19 @@ Interface
type="checkbox"
name="allow_reporting"
checked={this.props.ui.allow_reporting}
onChange={(e) => this.props.uiActions.set({ allow_reporting: !this.props.ui.allow_reporting })}
onChange={(e) => this.props.uiActions.set({ allow_reporting: !this.props.ui.allow_reporting })}
/>
<span className="label">
Allow reporting of anonymous usage statistics
</span>
</label>
<div className="description">
This helps identify errors and potential features that make Iris better for everyone. See
<a href="https://github.com/jaedb/Iris/wiki/Terms-of-use#privacy-policy" target="_blank">privacy policy</a>
.
<div className="description">
This helps identify errors and potential features that make Iris better for everyone. See
<a href="https://github.com/jaedb/Iris/wiki/Terms-of-use#privacy-policy" target="_blank">privacy policy</a>
.
</div>
</div>
</div>
</div>
)}
<div className="field commands-setup" id="commands-setup">
@ -363,15 +363,15 @@ This helps identify errors and potential features that make Iris better for ever
<Commands
commands={this.props.pusher.commands}
runCommand={(id, notify) => this.props.pusherActions.runCommand(id, notify)}
onChange={(commands) => this.props.pusherActions.setCommands(commands)}
onChange={(commands) => this.props.pusherActions.setCommands(commands)}
/>
<Link to="/edit-command" className="button button--default">Add new</Link>
</div>
</div>
<h4 className="underline">
Advanced
<a name="advanced" />
<h4 className="underline">
Advanced
<a name="advanced" />
</h4>
<div className="field">
@ -381,7 +381,7 @@ Advanced
type="text"
value={this.state.mopidy_library_artists_uri}
onChange={(e) => this.setState({ mopidy_library_artists_uri: e.target.value })}
onBlur={(e) => this.handleBlur('mopidy', 'library_artists_uri', e.target.value)}
onBlur={(e) => this.handleBlur('mopidy', 'library_artists_uri', e.target.value)}
/>
<div className="description">
URI used for collecting library artists
@ -396,7 +396,7 @@ Advanced
type="text"
value={this.state.mopidy_library_albums_uri}
onChange={(e) => this.setState({ mopidy_library_albums_uri: e.target.value })}
onBlur={(e) => this.handleBlur('mopidy', 'library_albums_uri', e.target.value)}
onBlur={(e) => this.handleBlur('mopidy', 'library_albums_uri', e.target.value)}
/>
<div className="description">
URI used for collecting library albums
@ -417,19 +417,19 @@ Advanced
<div className="name">Version</div>
<div className="input">
<span className="text">
{this.props.pusher.version.current}
{' '}
installed
{this.props.pusher.version.upgrade_available ? (
<span className="flag dark">
<Icon name="cloud_download" className="blue-text" />
&nbsp; Upgrade available
</span>
) : (
<span className="flag dark">
<Icon name="check" className="green-text" />
&nbsp; Up-to-date
</span>
{this.props.pusher.version.current}
{' '}
installed
{this.props.pusher.version.upgrade_available ? (
<span className="flag dark">
<Icon name="cloud_download" className="blue-text" />
&nbsp; Upgrade available
</span>
) : (
<span className="flag dark">
<Icon name="check" className="green-text" />
&nbsp; Up-to-date
</span>
)}
</span>
</div>
@ -441,45 +441,55 @@ installed
</div>
<div className="field">
{this.props.pusher.version.upgrade_available ? (
<button className="button button--secondary" onClick={(e) => this.props.pusherActions.upgrade()}>
Upgrade to
{ this.props.pusher.version.latest }
</button>
{this.props.pusher.version.upgrade_available ? (
<button className="button button--secondary" onClick={(e) => this.props.pusherActions.upgrade()}>
{`Upgrade to ${this.props.pusher.version.latest}`}
</button>
) : null }
<button className={`button button--destructive${this.props.mopidy.restarting ? ' button--working' : ''}`} onClick={(e) => this.props.pusherActions.restart()}>Restart server</button>
<ConfirmationButton className="button--destructive" content="Reset all settings" confirmingContent="Are you sure?" onConfirm={() => this.resetAllSettings()} />
<ConfirmationButton
className="button--destructive"
content="Reset all settings"
confirmingContent="Are you sure?"
onConfirm={() => this.resetAllSettings()}
/>
<button
className="button button--destructive"
onClick={() => helpers.cache.clear()}
>
Clear request cache
</button>
</div>
<h4 className="underline">
About
<a name="about" />
<h4 className="underline">
About
<a name="about" />
</h4>
<div className="field">
<div>
<em><a href="https://github.com/jaedb/Iris" target="_blank">Iris</a></em>
{' '}
is an open-source project by
<a href="https://github.com/jaedb" target="_blank">James Barnsley</a>
<em><a href="https://github.com/jaedb/Iris" target="_blank">Iris</a></em>
{' '}
is an open-source project by
<a href="https://github.com/jaedb" target="_blank">James Barnsley</a>
. It is provided free and with absolutely no warranty. If you paid someone for this software, please let me know.
</div>
<br />
<br />
<br />
<div>
<a className="button button--default" href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=james%40barnsley%2enz&lc=NZ&item_name=James%20Barnsley&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted" target="_blank">
<Icon type="fontawesome" name="paypal" />
{' '}
<Icon type="fontawesome" name="paypal" />
{' '}
Donate
</a>
<a className="button button--default" href="https://github.com/jaedb/Iris" target="_blank">
<Icon type="fontawesome" name="github" />
{' '}
<Icon type="fontawesome" name="github" />
{' '}
GitHub
</a>
<a className="button button--default" href="http://creativecommons.org/licenses/by-nc/4.0/" target="_blank">
<Icon type="fontawesome" name="creative-commons" />
&nbsp;Licence
<a className="button button--default" href="http://creativecommons.org/licenses/by-nc/4.0/" target="_blank">
<Icon type="fontawesome" name="creative-commons" />
&nbsp;Licence
</a>
</div>
</div>
@ -501,4 +511,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Settings);
export default connect(mapStateToProps, mapDispatchToProps)(Settings);

View File

@ -17,7 +17,7 @@ import LazyLoadListener from '../components/LazyLoadListener';
import ContextMenuTrigger from '../components/ContextMenuTrigger';
import URILink from '../components/URILink';
import Icon from '../components/Icon';
import Popularity from '../components/Popularity';
import Loader from '../components/Loader';
import * as helpers from '../helpers';
import * as coreActions from '../services/core/actions';
@ -27,7 +27,7 @@ import * as spotifyActions from '../services/spotify/actions';
import * as lastfmActions from '../services/lastfm/actions';
import * as geniusActions from '../services/genius/actions';
class Track extends React.Component {
class Track extends React.Component {
constructor(props) {
super(props);
}
@ -51,7 +51,7 @@ class Track extends React.Component {
this.props.uiActions.showContextMenu(e, data, 'track', 'click');
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps) {
// if our URI has changed, fetch new track
if (nextProps.uri != this.props.uri) {
this.props.coreActions.loadTrack(nextProps.uri);
@ -68,7 +68,7 @@ class Track extends React.Component {
}
// We have just received our full track or our track artists
if ((!this.props.track && nextProps.track) || (!this.props.track.artists && nextProps.track.artists)) {
if ((!this.props.track && nextProps.track) || (!this.props.track.artists && nextProps.track.artists)) {
this.setWindowTitle(nextProps.track);
// Ready to load LastFM
@ -118,7 +118,7 @@ class Track extends React.Component {
renderLyricsSelector() {
if (this.props.track.lyrics_results === undefined || this.props.track.lyrics_results === null) {
return null;
return null;
} if (this.props.track.lyrics_results.length <= 0) {
return (
<div className="field lyrics-selector">
@ -136,14 +136,14 @@ class Track extends React.Component {
<div className="field lyrics-selector">
<div className="input">
<select
onChange={(e) => this.props.geniusActions.getTrackLyrics(this.props.track.uri, e.target.value)}
onChange={(e) => this.props.geniusActions.getTrackLyrics(this.props.track.uri, e.target.value)}
>
{
this.props.track.lyrics_results.map((result) => (
<option
key={result.path}
value={result.path}
defaultValue={result.path == this.props.track.lyrics_path}
defaultValue={result.path == this.props.track.lyrics_path}
>
{result.title}
</option>
@ -162,9 +162,7 @@ class Track extends React.Component {
if (helpers.isLoading(this.props.load_queue, ['genius_'])) {
return (
<div className="lyrics">
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
} if (this.props.track.lyrics) {
@ -172,8 +170,8 @@ class Track extends React.Component {
<div className="lyrics">
<div className="content" dangerouslySetInnerHTML={{ __html: this.props.track.lyrics }} />
<div className="origin mid_grey-text">
Origin:
{' '}
Origin:
{' '}
<a href={`https://genius.com${this.props.track.lyrics_path}`} target="_blank">{`https://genius.com${this.props.track.lyrics_path}`}</a>
</div>
</div>
@ -181,22 +179,18 @@ class Track extends React.Component {
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
Could not find track with URI "
{encodeURIComponent(this.props.uri)}
"
<p>
Could not find track with URI "
{encodeURIComponent(this.props.uri)}
"
</p>
</ErrorMessage>
);
);
}
render() {
if (helpers.isLoading(this.props.load_queue, [`spotify_track/${helpers.getFromUri('trackid', this.props.uri)}`])) {
return (
<div className="body-loader loading">
<div className="loader" />
</div>
);
return <Loader body loading />
}
if (!this.props.track) {
@ -217,13 +211,13 @@ Could not find track with URI "
return (
<div className="view track-view content-wrapper">
{this.props.slim_mode ? (
{this.props.slim_mode ? (
<Header
icon="music"
title="Track"
handleContextMenuTrigger={(e) => this.handleContextMenu(e)}
uiActions={this.props.uiActions}
/>
uiActions={this.props.uiActions}
/>
) : null}
<div className="thumbnail-wrapper">
@ -237,8 +231,8 @@ Could not find track with URI "
{track.album && track.album.uri ? <Link to={`/album/${track.album.uri}`}>{track.album.name}</Link> : null}
{track.album && !track.album.uri ? track.album.name : null}
{!track.album ? 'Unknown album' : null}
&nbsp;by
{' '}
&nbsp;by
{' '}
<ArtistSentence artists={track.artists} />
</h2>
@ -247,26 +241,26 @@ Could not find track with URI "
{track.date ? <li><Dater type="date" data={track.date} /></li> : null}
{track.explicit ? <li><span className="flag dark">EXPLICIT</span></li> : null}
<li>
{track.disc_number ? (
<span>
Disc
{track.disc_number}
</span>
{track.disc_number ? (
<span>
Disc
{track.disc_number}
</span>
) : null}
{track.disc_number && track.track_number ? <span>, </span> : null}
{track.track_number ? (
<span>
Track
{track.track_number}
</span>
{track.track_number ? (
<span>
Track
{track.track_number}
</span>
) : null}
</li>
{track.duration ? <li><Dater type="length" data={track.duration} /></li> : null}
{track.popularity ? (
<li>
{track.popularity}
% popularity
</li>
{track.popularity ? (
<li>
{track.popularity}
% popularity
</li>
) : null}
</ul>
</div>
@ -277,12 +271,12 @@ Track
<ContextMenuTrigger onTrigger={(e) => this.handleContextMenu(e)} />
</div>
{!this.props.genius_authorized ? (
<p className="no-results">
Want track lyrics? Authorize Genius under
<Link to="/settings/genius" scrollTo="#services-menu">Settings</Link>
.
</p>
{!this.props.genius_authorized ? (
<p className="no-results">
Want track lyrics? Authorize Genius under
<Link to="/settings/genius" scrollTo="#services-menu">Settings</Link>
.
</p>
) : null}
{this.props.genius_authorized ? this.renderLyricsSelector() : null}
{this.props.genius_authorized ? this.renderLyrics() : null}

View File

@ -10,7 +10,7 @@ import FollowButton from '../components/Fields/FollowButton';
import LazyLoadListener from '../components/LazyLoadListener';
import Parallax from '../components/Parallax';
import NiceNumber from '../components/NiceNumber';
import ContextMenuTrigger from '../components/ContextMenuTrigger';
import Loader from '../components/Loader';
import Icon from '../components/Icon';
import * as helpers from '../helpers';
@ -19,7 +19,7 @@ import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
class User extends React.Component {
class User extends React.Component {
constructor(props) {
super(props);
}
@ -70,21 +70,17 @@ class User extends React.Component {
if (!this.props.user) {
if (helpers.isLoading(this.props.load_queue, [`spotify_users/${user_id}`, `spotify_users/${user_id}/playlists/?`])) {
return (
<div className="body-loader loading">
<div className="loader" />
</div>
);
return <Loader body loading />
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
Could not find user with URI "
{encodeURIComponent(this.props.uri)}
"
<p>
Could not find user with URI "
{encodeURIComponent(this.props.uri)}
"
</p>
</ErrorMessage>
);
);
}
const user = helpers.collate(this.props.user, { playlists: this.props.playlists });
@ -117,19 +113,19 @@ Could not find user with URI "
<h2>
<ul className="details">
{!this.props.slim_mode ? <li className="source"><Icon type="fontawesome" name={helpers.sourceIcon(user.uri)} /></li> : null}
{user.playlists_total ? (
<li>
<NiceNumber value={user.playlists_total} />
{' '}
playlists
</li>
{user.playlists_total ? (
<li>
<NiceNumber value={user.playlists_total} />
{' '}
playlists
</li>
) : null}
{user.followers ? (
<li>
<NiceNumber value={user.followers} />
{' '}
followers
</li>
{user.followers ? (
<li>
<NiceNumber value={user.followers} />
{' '}
followers
</li>
) : null}
{this.isMe() ? <li><span className="blue-text">You</span></li> : null}
</ul>
@ -147,7 +143,7 @@ followers
<LazyLoadListener
loadKey={user.playlists_more}
showLoader={user.playlists_more}
loadMore={() => this.loadMore()}
loadMore={() => this.loadMore()}
/>
</section>
</div>

View File

@ -6,17 +6,18 @@ import { bindActionCreators } from 'redux';
import Header from '../../components/Header';
import Icon from '../../components/Icon';
import CategoryGrid from '../../components/CategoryGrid';
import Loader from '../../components/Loader';
import * as helpers from '../../helpers';
import * as uiActions from '../../services/ui/actions';
import * as spotifyActions from '../../services/spotify/actions';
class DiscoverCategories extends React.Component {
class DiscoverCategories extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
componentDidMount() {
// Check for an empty category index, or where we've only got one loaded
// This would be the case if you've refreshed from within a category and only loaded
// the single record.
@ -31,9 +32,7 @@ class DiscoverCategories extends React.Component {
return (
<div className="view discover-categories-view">
<Header icon="grid" title="Genre / Mood" />
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
}
@ -79,4 +78,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverCategories);
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverCategories);

View File

@ -7,12 +7,13 @@ import Header from '../../components/Header';
import Icon from '../../components/Icon';
import PlaylistGrid from '../../components/PlaylistGrid';
import LazyLoadListener from '../../components/LazyLoadListener';
import Loader from '../../components/Loader';
import * as helpers from '../../helpers';
import * as uiActions from '../../services/ui/actions';
import * as spotifyActions from '../../services/spotify/actions';
class DiscoverCategory extends React.Component {
class DiscoverCategory extends React.Component {
constructor(props) {
super(props);
}
@ -69,9 +70,7 @@ class DiscoverCategory extends React.Component {
<Icon name="mood" type="material" />
{(this.props.category ? this.props.category.name : 'Category')}
</Header>
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
}
@ -95,7 +94,7 @@ class DiscoverCategory extends React.Component {
<LazyLoadListener
loadKey={category.playlists_more}
showLoader={category.playlists_more}
loadMore={() => this.loadMore()}
loadMore={() => this.loadMore()}
/>
</div>
</div>
@ -114,4 +113,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverCategory);
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverCategory);

View File

@ -8,16 +8,14 @@ import PlaylistGrid from '../../components/PlaylistGrid';
import Header from '../../components/Header';
import Icon from '../../components/Icon';
import Parallax from '../../components/Parallax';
import Thumbnail from '../../components/Thumbnail';
import URILink from '../../components/URILink';
import Dater from '../../components/Dater';
import Loader from '../../components/Loader';
import * as helpers from '../../helpers';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
class DiscoverFeatured extends React.Component {
class DiscoverFeatured extends React.Component {
constructor(props) {
super(props);
}
@ -56,7 +54,7 @@ class DiscoverFeatured extends React.Component {
<div className="intro">
<Parallax disabled={this.props.disable_parallax} />
</div>
);
);
}
render() {
@ -67,9 +65,7 @@ class DiscoverFeatured extends React.Component {
<Icon name="star" type="material" />
Featured playlists
</Header>
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
}
@ -89,7 +85,7 @@ class DiscoverFeatured extends React.Component {
const options = (
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.props.spotifyActions.getFeaturedPlaylists(); }}>
<Icon name="refresh" />
<Icon name="refresh" />
Refresh
</a>
);
@ -122,4 +118,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverFeatured);
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverFeatured);

View File

@ -8,16 +8,15 @@ import Header from '../../components/Header';
import Icon from '../../components/Icon';
import AlbumGrid from '../../components/AlbumGrid';
import Parallax from '../../components/Parallax';
import Thumbnail from '../../components/Thumbnail';
import ArtistSentence from '../../components/ArtistSentence';
import LazyLoadListener from '../../components/LazyLoadListener';
import Loader from '../../components/Loader';
import * as helpers from '../../helpers';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
class DiscoverNewReleases extends React.Component {
class DiscoverNewReleases extends React.Component {
constructor(props) {
super(props);
}
@ -67,7 +66,7 @@ class DiscoverNewReleases extends React.Component {
<div className="intro preserve-3d">
<Parallax />
</div>
);
);
}
render() {
@ -78,9 +77,7 @@ class DiscoverNewReleases extends React.Component {
<Icon name="new_releases" type="material" />
New releases
</Header>
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
}
@ -102,7 +99,7 @@ class DiscoverNewReleases extends React.Component {
const options = (
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.props.spotifyActions.getNewReleases(); }}>
<Icon name="refresh" />
<Icon name="refresh" />
Refresh
</a>
);
@ -120,7 +117,7 @@ Refresh
<LazyLoadListener
loadKey={this.props.new_releases_more}
showLoader={this.props.new_releases_more}
loadMore={() => this.loadMore()}
loadMore={() => this.loadMore()}
/>
</div>
);
@ -143,4 +140,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverNewReleases);
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverNewReleases);

View File

@ -3,7 +3,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from '../../components/Link';
import Loader from '../../components/Loader';
import Header from '../../components/Header';
import List from '../../components/List';
import TrackList from '../../components/TrackList';
@ -18,7 +18,7 @@ import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
class LibraryBrowseDirectory extends React.Component {
class LibraryBrowseDirectory extends React.Component {
constructor(props) {
super(props);
}
@ -28,7 +28,7 @@ class LibraryBrowseDirectory extends React.Component {
this.loadDirectory();
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps) {
// mopidy goes online
if (!this.props.mopidy_connected && nextProps.mopidy_connected) {
this.loadDirectory(nextProps);
@ -61,7 +61,7 @@ class LibraryBrowseDirectory extends React.Component {
this.props.uiActions.hideContextMenu();
}
renderBreadcrumbs() {
renderBreadcrumbs() {
if (this.props.uri) {
var parent_uri = this.props.uri;
} else {
@ -74,7 +74,7 @@ class LibraryBrowseDirectory extends React.Component {
return (
<h4>
{uri_elements.map((uri_element, index) => {
{uri_elements.map((uri_element, index) => {
// Reconstruct a URL to this element
let uri = 'file://';
for (let i = 0; i <= index; i++) {
@ -83,12 +83,12 @@ class LibraryBrowseDirectory extends React.Component {
return (
<span key={uri}>
{index > 0 ? (
<span>
<Icon type="fontawesome" name="angle-right" />
{' '}
&nbsp;&nbsp;
</span>
{index > 0 ? (
<span>
<Icon type="fontawesome" name="angle-right" />
{' '}
&nbsp;&nbsp;
</span>
) : null}
<URILink type="browse" uri={uri}>
{decodeURI(uri_element)}
@ -111,7 +111,7 @@ class LibraryBrowseDirectory extends React.Component {
rows={subdirectories}
className="library-local-directory-list"
link_prefix="/library/browse/"
nocontext
nocontext
/>
);
}
@ -124,12 +124,12 @@ class LibraryBrowseDirectory extends React.Component {
type="browse"
link={`/library/browse/${encodeURIComponent(subdirectory.uri)}`}
item={subdirectory}
nocontext
nocontext
/>
))
}
</div>
);
);
}
render() {
@ -144,9 +144,7 @@ class LibraryBrowseDirectory extends React.Component {
return (
<div className="view library-local-view">
<Header icon="music" title={title} uiActions={this.props.uiActions} />
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
}
@ -175,16 +173,16 @@ class LibraryBrowseDirectory extends React.Component {
name="View"
value={this.props.view}
options={view_options}
handleChange={(value) => { this.props.uiActions.set({ library_directory_view: value }); this.props.uiActions.hideContextMenu(); }}
handleChange={(value) => { this.props.uiActions.set({ library_directory_view: value }); this.props.uiActions.hideContextMenu(); }}
/>
{tracks ? (
{tracks ? (
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.playAll(e, tracks); }}>
<Icon name="play_circle_filled" />
<Icon name="play_circle_filled" />
Play all
</a>
</a>
) : null }
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.goBack(e); }}>
<Icon name="keyboard_backspace" />
<Icon name="keyboard_backspace" />
Back
</a>
</span>
@ -203,12 +201,12 @@ Back
{subdirectories ? this.renderSubdirectories(subdirectories) : null}
{tracks ? (
{tracks ? (
<TrackList
tracks={this.props.directory.tracks}
uri={`iris:browse:${this.props.uri}`}
className="library-local-track-list"
/>
className="library-local-track-list"
/>
) : null }
</ErrorBoundary>
@ -218,7 +216,7 @@ Back
}
}
const mapStateToProps = (state, ownProps) => {
const mapStateToProps = (state, ownProps) => {
// Decode the URI, and then re-encode selected characters
// This is needed as Mopidy encodes *some* characters in URIs (but not other characters)
// We need to retain ":" because this a reserved URI separator
@ -246,4 +244,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(LibraryBrowseDirectory);
export default connect(mapStateToProps, mapDispatchToProps)(LibraryBrowseDirectory);

View File

@ -2,8 +2,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from '../../components/Link';
import Loader from '../../components/Loader';
import TrackList from '../../components/TrackList';
import Header from '../../components/Header';
import LazyLoadListener from '../../components/LazyLoadListener';
@ -14,7 +14,7 @@ import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
class LibraryTracks extends React.Component {
class LibraryTracks extends React.Component {
constructor(props) {
super(props);
}
@ -44,15 +44,13 @@ class LibraryTracks extends React.Component {
this.props.spotifyActions.getLibraryTracksAndPlay();
}
render() {
render() {
// Note trailing "?" makes sure our context menu in_library checks doesn't interfere
if (helpers.isLoading(this.props.load_queue, ['spotify_me/tracks?'])) {
return (
<div className="view library-tracks-view">
<Header icon="music" title="My tracks" />
<div className="body-loader loading">
<div className="loader" />
</div>
<Loader body loading />
</div>
);
}
@ -69,7 +67,7 @@ class LibraryTracks extends React.Component {
const options = (
<a className="button button--no-hover" onClick={(e) => this.playAll(e)}>
<Icon name="play_circle_filled" />
<Icon name="play_circle_filled" />
Play all
</a>
);
@ -85,7 +83,7 @@ Play all
<LazyLoadListener
loadKey={this.props.library_tracks_more}
showLoader={this.props.library_tracks_more}
loadMore={() => this.loadMore()}
loadMore={() => this.loadMore()}
/>
</section>
</div>
@ -114,4 +112,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(LibraryTracks);
export default connect(mapStateToProps, mapDispatchToProps)(LibraryTracks);

View File

@ -3,8 +3,8 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ReactGA from 'react-ga';
import Link from '../../components/Link';
import Loader from '../../components/Loader';
import Modal from './Modal';
import Icon from '../../components/Icon';
import Thumbnail from '../../components/Thumbnail';
@ -14,7 +14,7 @@ import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
import * as helpers from '../../helpers';
class AddToPlaylist extends React.Component {
class AddToPlaylist extends React.Component {
constructor(props) {
super(props);
@ -41,26 +41,19 @@ class AddToPlaylist extends React.Component {
playlists = helpers.sortItems(playlists, 'name');
let loader = null;
if (this.props.spotify_library_playlists_status == 'running') {
loader = (
<div className="lazy-loader body-loader loading">
<div className="loader" />
</div>
);
}
const isLoading = this.props.spotify_library_playlists_status == 'running';
return (
<Modal className="modal--add-to-playlist">
<h1>Add to playlist</h1>
<h2 className="mid_grey-text">
Select playlist to add
{this.props.uris.length}
{' '}
track
{this.props.uris.length > 1 ? 's' : null}
{' '}
to
<h2 className="mid_grey-text">
Select playlist to add
{this.props.uris.length}
{' '}
track
{this.props.uris.length > 1 ? 's' : null}
{' '}
to
</h2>
{playlists.length <= 0 ? <div className="no-results">No playlists available</div> : null}
<div className="list small playlists">
@ -71,21 +64,21 @@ to
<h4 className="list__item__name">{ playlist.name }</h4>
<ul className="list__item__details details">
<li><Icon type="fontawesome" className="source" name={helpers.sourceIcon(playlist.uri)} /></li>
<li>
{ playlist.tracks_total ? (
<span className="mid_grey-text">
{ playlist.tracks_total }
{' '}
tracks
</span>
) : null }
<li>
{ playlist.tracks_total ? (
<span className="mid_grey-text">
{ playlist.tracks_total }
{' '}
tracks
</span>
) : null }
</li>
</ul>
</div>
))
}
</div>
{loader}
{isLoading && <Loader body lazy loading />}
</Modal>
);
}
@ -111,4 +104,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(AddToPlaylist);
export default connect(mapStateToProps, mapDispatchToProps)(AddToPlaylist);

View File

@ -11,7 +11,7 @@ import * as uiActions from '../../services/ui/actions';
import * as pusherActions from '../../services/pusher/actions';
import * as helpers from '../../helpers';
class ShareConfiguration extends React.Component {
class ShareConfiguration extends React.Component {
constructor(props) {
super(props);
@ -73,7 +73,7 @@ class ShareConfiguration extends React.Component {
);
}
window.history.back();
window.history.back();
}
render() {
@ -86,25 +86,27 @@ class ShareConfiguration extends React.Component {
if (connections.length > 0) {
var recipients = (
<div className="input">
<div className="input checkbox-group">
{
connections.map((connection, index) => (
<label key={connection.connection_id}>
<input
type="checkbox"
name={`connection_${connection.connection_id}`}
checked={this.state.recipients.includes(connection.connection_id)}
onChange={(e) => this.toggleRecipient(connection.connection_id)}
/>
<span className="label">
<div className="title">{ connection.username }</div>
<div className="description mid_grey-text">
(
{ connection.ip }
)
</div>
</span>
</label>
<div key={connection.connection_id} className="checkbox-group__item">
<label>
<input
type="checkbox"
name={`connection_${connection.connection_id}`}
checked={this.state.recipients.includes(connection.connection_id)}
onChange={(e) => this.toggleRecipient(connection.connection_id)}
/>
<div className="label">
<div>
<div className="title">{ connection.username }</div>
<div className="description mid_grey-text">
({ connection.ip })
</div>
</div>
</div>
</label>
</div>
))
}
</div>
@ -134,71 +136,82 @@ class ShareConfiguration extends React.Component {
<div className="field checkbox checkbox--block">
<div className="name">Configurations</div>
<div className="input">
<label>
<input
type="checkbox"
name="interface"
checked={this.state.ui}
onChange={(e) => this.setState({ ui: !this.state.ui })}
/>
<span className="label">
<span className="title">UI customisation (theme, sorting, filters)</span>
</span>
</label>
{this.props.spotify_me && this.props.spotify_authorization ? (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="spotify"
checked={this.state.spotify}
onChange={(e) => this.setState({ spotify: !this.state.spotify })}
name="interface"
checked={this.state.ui}
onChange={(e) => this.setState({ ui: !this.state.ui })}
/>
<span className="label">
<div className="title">Spotify authorization</div>
<div className="description mid_grey-text">
Logged in as
{this.props.spotify_me.name}
</div>
<span className="title">UI customisation (theme, sorting, filters)</span>
</span>
</label>
) : null}
</label>
</div>
{this.props.lastfm_me && this.props.lastfm_authorization ? (
<label>
<input
type="checkbox"
name="lastfm_authorization"
checked={this.state.lastfm}
onChange={(e) => this.setState({ lastfm: !this.state.lastfm })}
/>
<span className="label">
<div className="title">LastFM authorization</div>
<div className="description mid_grey-text">
Logged in as
{this.props.lastfm_me.name}
{this.props.spotify_me && this.props.spotify_authorization && (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="spotify"
checked={this.state.spotify}
onChange={(e) => this.setState({ spotify: !this.state.spotify })}
/>
<div className="label">
<div>
<div className="title">Spotify authorization</div>
<div className="description mid_grey-text">
{`Logged in as ${this.props.spotify_me.name}`}
</div>
</div>
</div>
</span>
</label>
) : null}
</label>
</div>
)}
{this.props.genius_me && this.props.genius_authorization ? (
<label>
<input
type="checkbox"
name="genius_authorization"
checked={this.state.genius}
onChange={(e) => this.setState({ genius: !this.state.genius })}
/>
<span className="label">
<div className="title">Genius authorization</div>
<div className="description mid_grey-text">
Logged in as
{this.props.genius_me.name}
{this.props.lastfm_me && this.props.lastfm_authorization && (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="lastfm_authorization"
checked={this.state.lastfm}
onChange={(e) => this.setState({ lastfm: !this.state.lastfm })}
/>
<div className="label">
<div>
<div className="title">LastFM authorization</div>
<div className="description mid_grey-text">
{`Logged in as ${this.props.lastfm_me.name}`}
</div>
</div>
</div>
</span>
</label>
) : null}
</label>
</div>
)}
{this.props.genius_me && this.props.genius_authorization && (
<div className="checkbox-group__item">
<label>
<input
type="checkbox"
name="genius_authorization"
checked={this.state.genius}
onChange={(e) => this.setState({ genius: !this.state.genius })}
/>
<div className="label">
<div>
<div className="title">Genius authorization</div>
<div className="description mid_grey-text">
{`Logged in as ${this.props.genius_me.name}`}
</div>
</div>
</div>
</label>
</div>
)}
</div>
</div>
@ -228,4 +241,4 @@ const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(ShareConfiguration);
export default connect(mapStateToProps, mapDispatchToProps)(ShareConfiguration);

View File

@ -1,5 +1,5 @@
{
"manifest_version": 2.8,
"manifest_version": 3.40,
"short_name": "Iris",
"name": "Iris",
"icons": [

View File

@ -8,6 +8,8 @@
display: inline-block;
.label {
display: flex;
.icon {
padding-right: 8px;
}

View File

@ -1,88 +1,91 @@
.loader {
height: 120px;
width: 120px;
margin: 0 auto;
position: relative;
opacity: 0;
&:before,
&:after {
content: '';
display: block;
&__offline {
padding: 2rem;
color: colour(middle_grey);
text-align: center;
font-size: 1rem;
.icon {
display: block;
font-size: 3rem;
padding-bottom: 0.5rem;
}
}
&__spinner {
height: 120px;
width: 120px;
margin: 0 auto;
width: 100%;
height: 100%;
box-sizing: border-box;
border: {
radius: 100%;
style: solid;
width: 2px;
}
}
position: relative;
&:before {
position: absolute;
top: 0;
left: 0;
border-color: lighten(colour(dark_grey), 5%);
.light-theme & {
border-color: colour(faint_grey);
}
}
&:after {
@include spin();
border-top-color: transparent;
border-left-color: transparent;
border-bottom-color: transparent;
border-right-color: colour(blue);
}
}
.body-loader {
&.loading {
padding: 35vh 0;
.loader {
opacity: 1;
}
}
&.lazy-loader {
.loader {
opacity: 0;
}
&.loading {
padding: 20px;
height: 40px;
.loader {
height: 40px;
width: 40px;
opacity: 1;
&:before,
&:after {
content: '';
display: block;
margin: 0 auto;
width: 100%;
height: 100%;
box-sizing: border-box;
border: {
radius: 100%;
style: solid;
width: 2px;
}
}
}
}
.mini-loader {
.loader {
width: 20px;
height: 20px;
opacity: 1;
&:before {
border-color: darken(colour(secondary_grey), 5%);
position: absolute;
top: 0;
left: 0;
border-color: rgba(128,128,128,0.15);
}
&:after {
@include spin();
border-top-color: transparent;
border-left-color: transparent;
border-bottom-color: transparent;
border-right-color: colour(blue);
}
}
.light-theme & {
border-color: colour(faint_grey);
&--white {
.loader__spinner {
&:before {
opacity: 0;
}
&:after {
border-right-color: colour(white);
}
}
}
}
&--body {
padding: 35vh 0;
}
&--mini {
padding: 0;
.loader__spinner {
width: 20px;
height: 20px;
}
.loader__offline {
display: none;
}
}
&--lazy {
padding: 20px;
.loader__spinner {
width: 40px;
height: 40px;
}
}
}

View File

@ -1,5 +1,4 @@
.notifications {
position: fixed;
bottom: 75px;
@ -109,22 +108,9 @@
}
.loader {
width: 20px;
height: 20px;
position: absolute;
top: 0;
left: 0;
opacity: 1;
z-index: 2;
margin: 0.85rem;
&:before {
border-color: transparent;
}
&:after {
border-right-color: colour(white);
}
top: 13px;
left: 13px;
.progress {
position: absolute;

View File

@ -1,5 +1,5 @@
.playback-controls {
position: fixed;
position: absolute;
z-index: 99;
bottom: 0;
right: 0;

View File

@ -1,5 +1,5 @@
.sidebar {
position: fixed;
position: absolute;
top: 0;
left: 0;
bottom: 0;

View File

@ -76,6 +76,7 @@ body {
.app-inner {
width: 100%;
height: 100%;
position: relative;
&.light-theme {
color: colour(darkest_grey);
@ -89,7 +90,7 @@ body {
}
main {
position: fixed;
position: absolute;
top: 0;
left: 0;
bottom: 60px;
@ -224,6 +225,7 @@ h1 {
line-height: 3.4rem;
letter-spacing: -0.15rem;
color: colour(white);
display: flex;
.light-theme & {
color: colour(darkest_grey);

View File

@ -180,9 +180,9 @@ select {
@include animate();
@include feature_font();
@include noselect();
display: inline-flex;
font-size: 14px;
color: colour(black);
display: inline-block;
padding: 9px 30px 7px;
margin: 0 20px 10px 0;
box-sizing: border-box;

View File

@ -233,8 +233,16 @@
}
&--share-configuration {
.checkbox-group {
&__item {
padding-bottom: 0.75rem;
}
}
.field.checkbox {
.label {
display: flex;
.title {
font-size: 1.2rem;
}

View File

@ -1,3 +1,62 @@
self.addEventListener('fetch', (event) => {
// Nothing doing!
});
const noCacheList = [
'jamesbarnsley.co.nz'
];
function inNoCacheList(url) {
for (let item of noCacheList) {
if (url.indexOf(item) >= 0){
console.log(`${url} is in our no-cache list`);
return true;
}
}
}
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(cacheName => {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
return true;
}).map(cacheName => {
return caches.delete(cacheName);
})
);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
// Opens Cache objects that start with 'font'.
caches.open('iris').then(cache => {
return cache.match(event.request)
.then(response => {
// Cached response exists, so just return that (unless it's in our no-cache list)
if (response && !inNoCacheList(event.request.url)) {
return response;
}
// Not cached, so we make the request, return that and also save response in cache
return fetch(event.request)
.then(networkResponse => {
if (networkResponse.status >= 200 && networkResponse.status < 400) {
cache.put(event.request, networkResponse.clone());
}
return networkResponse;
})
.catch(error => {
throw error;
});
// Exceptions from match() or fetch()
}).catch(error => {
throw error;
});
})
);
});