Service worker caching, Loader component
This commit is contained in:
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
44
src/js/components/Loader.js
Executable 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>
|
||||
);
|
||||
});
|
||||
@ -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() {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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];
|
||||
};
|
||||
};
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
*/
|
||||
};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
|
||||
{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>
|
||||
|
||||
@ -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" />
|
||||
Upgrade available
|
||||
</span>
|
||||
) : (
|
||||
<span className="flag dark">
|
||||
<Icon name="check" className="green-text" />
|
||||
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" />
|
||||
Upgrade available
|
||||
</span>
|
||||
) : (
|
||||
<span className="flag dark">
|
||||
<Icon name="check" className="green-text" />
|
||||
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¤cy_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" />
|
||||
Licence
|
||||
<a className="button button--default" href="http://creativecommons.org/licenses/by-nc/4.0/" target="_blank">
|
||||
<Icon type="fontawesome" name="creative-commons" />
|
||||
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);
|
||||
|
||||
@ -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}
|
||||
by
|
||||
{' '}
|
||||
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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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" />
|
||||
{' '}
|
||||
|
||||
</span>
|
||||
{index > 0 ? (
|
||||
<span>
|
||||
<Icon type="fontawesome" name="angle-right" />
|
||||
{' '}
|
||||
|
||||
</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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_version": 2.8,
|
||||
"manifest_version": 3.40,
|
||||
"short_name": "Iris",
|
||||
"name": "Iris",
|
||||
"icons": [
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
display: inline-block;
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
|
||||
.icon {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.playback-controls {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -233,8 +233,16 @@
|
||||
}
|
||||
|
||||
&--share-configuration {
|
||||
.checkbox-group {
|
||||
&__item {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.field.checkbox {
|
||||
.label {
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user