Merge branch 'develop' of github.com:jaedb/Iris into develop

This commit is contained in:
James Barnsley
2020-03-06 08:28:13 +13:00
15 changed files with 433 additions and 588 deletions

View File

@ -2,7 +2,7 @@
import React, { memo } from 'react';
import Icon from './Icon';
export default memo((props) => {
export default memo((props) => {
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
@ -15,7 +15,7 @@ export default memo((props) => {
}
return (
<span className={className} onClick={(e) => handleClick(e)}>
<span className={className} onClick={handleClick}>
<Icon name="more_horiz" />
</span>
);

View File

@ -1,5 +1,5 @@
import React, { memo } from 'react';
import { memo } from 'react';
/**
* Format time duration

View File

@ -2,15 +2,11 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { storage } from '../util';
import { get as getStorage } from '../util/storage';
import { isTouchDevice } from '../util/helpers';
import * as uiActions from '../services/ui/actions';
class DebugInfo extends React.Component {
constructor(props) {
super(props);
}
localStorageSize() {
let data = '';
@ -33,26 +29,9 @@ class DebugInfo extends React.Component {
}
renderLoadQueue() {
if (!this.props.ui.load_queue) {
return null;
}
const { ui: { load_queue } } = this.props;
if (!load_queue) return <div className="debug-info-item mid_grey-text">Nothing loading</div>;
const { load_queue } = this.props.ui;
const queue = [];
return (
<div className="debug-info-item">
{queue}
</div>
);
}
renderLoadQueue() {
if (!this.props.ui.load_queue) {
return <div className="debug-info-item mid_grey-text">Nothing loading</div>;
}
const { load_queue } = this.props.ui;
const queue = [];
for (const key in load_queue) {
if (load_queue.hasOwnProperty(key)) {
@ -134,7 +113,7 @@ class DebugInfo extends React.Component {
<div className="debug-info-item">
Cached URLs:
{' '}
{Object.keys(storage.get('cache')).length}
{Object.keys(getStorage('cache')).length}
</div>
</div>

View File

@ -7,7 +7,7 @@ import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
class Dragger extends React.Component {
class Dragger extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
@ -31,14 +31,16 @@ class Dragger extends React.Component {
}
handleMouseMove(e) {
if (!this.props.dragger) return null;
const { dragger, uiActions: { dragActive } } = this.props;
const { target } = e;
if (!dragger) return null;
const threshold = 10;
if (
e.clientX > this.props.dragger.start_x + threshold
|| e.clientX < this.props.dragger.start_x - threshold
|| e.clientY > this.props.dragger.start_y + threshold
|| e.clientY < this.props.dragger.start_y - threshold) {
e.clientX > dragger.start_x + threshold
|| e.clientX < dragger.start_x - threshold
|| e.clientY > dragger.start_y + threshold
|| e.clientY < dragger.start_y - threshold) {
this.setState({
position_x: e.clientX,
position_y: e.clientY,
@ -49,41 +51,41 @@ class Dragger extends React.Component {
dropzones[i].classList.remove('hover');
}
if (e.target.classList.contains('dropzone') && !e.target.classList.contains('hover')) {
e.target.className += ' hover';
if (target.classList.contains('dropzone') && !target.classList.contains('hover')) {
target.className += ' hover';
}
// if not already, activate
if (!this.props.dragger.active) this.props.uiActions.dragActive();
if (!dragger.active) dragActive();
}
}
handleMouseUp(e) {
if (!this.props.dragger) return null;
this.props.uiActions.dragEnd(e);
const { dragger, uiActions: { dragEnd } } = this.props;
if (!dragger) return null;
dragEnd(e);
}
render() {
if (!this.props.dragger || !this.props.dragger.active) return null;
const { dragger: { active, victims } = {} } = this.props;
const { position_x, position_y } = this.state;
const style = {
left: this.state.position_x,
top: this.state.position_y,
};
if (!active) return null;
return (
<div className="dragger" style={style}>
Dragging
{' '}
{ this.props.dragger.victims.length }
{' '}
things
<div
className="dragger" style={{
left: position_x,
top: position_y,
}}
>
{`Dragging ${victims.length} things`}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
const mapStateToProps = (state) => ({
dragger: state.ui.dragger,
});
@ -93,4 +95,4 @@ const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Dragger);
export default connect(mapStateToProps, mapDispatchToProps)(Dragger);

View File

@ -2,13 +2,13 @@
import React from 'react';
import ErrorMessage from './ErrorMessage';
export default class ErrorBoundary extends React.Component {
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
info: null,
info: {},
};
}
@ -21,14 +21,17 @@ export default class ErrorBoundary extends React.Component {
console.error(error, info);
}
render() {
if (this.state.hasError) {
render = () => {
const { hasError, info: { componentStack } = {} } = this.state;
const { children } = this.props;
if (hasError) {
return (
<ErrorMessage type="error-boundary">
{this.state.info ? <pre className="error-message__trace">{this.state.info.componentStack}</pre> : null}
{componentStack && <pre className="error-message__trace">{componentStack}</pre>}
</ErrorMessage>
);
}
return this.props.children;
return children;
}
}

View File

@ -1,18 +1,14 @@
import React, { memo } from 'react';
export default memo((props) => (
<div className={`error-message${props.type ? ` error-message--${props.type}` : ''}`}>
export default memo(({ type, title, children }) => (
<div className={`error-message${type ? ` error-message--${type}` : ''}`}>
<i className="error-message__icon icon icon--material">error</i>
<h4 className="error-message__title">
{props.title ? props.title : 'Unknown error'}
{title ? title : 'Unknown error'}
</h4>
<div className="error-message__content">
{props.children}
{children}
</div>
</div>
));

View File

@ -1,100 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import LinksSentence from './LinksSentence';
import Thumbnail from './Thumbnail';
import * as uiActions from '../services/ui/actions';
class GridSlider extends React.Component {
constructor(props) {
super(props);
this._pagelimit = 3;
this.state = {
page: 0,
};
}
handleClick(e, link) {
if (e.target.tagName.toLowerCase() !== 'a') {
this.props.history.push(link);
}
}
handleContextMenu(e, item) {
e.preventDefault();
const data = {
uris: [item.uri],
item,
};
this.props.uiActions.showContextMenu(e, data, 'album', 'click');
}
next() {
if (this.state.page >= this._pagelimit) return false;
this.setState({ page: this.state.page + 1 });
}
previous() {
if (this.state.page <= 0) return false;
this.setState({ page: this.state.page - 1 });
}
render() {
if (this.props.tracks) {
let className = 'grid-slider-wrapper';
if (this.props.className) className += ` ${this.props.className}`;
const style = {
left: `-${this.state.page * 100}%`,
};
return (
<div className={className}>
{ this.props.title ? this.props.title : null }
<div className="controls">
<Icon type="fontawesome" name="chevron-left" disabled={this.state.page <= 0} onClick={() => this.previous()} />
<Icon type="fontawesome" name="chevron-right" disabled={this.state.page >= this._pagelimit} onClick={() => this.next()} />
</div>
<div className="grid-slider">
<div className="grid artist-grid liner" style={style}>
{
this.props.tracks.map(
(track, index) => {
const album = { ...track.album, artists: track.artists };
return (
<div
className="grid-item"
key={index}
onClick={(e) => this.handleClick(e, `/album/${album.uri}`)}
onContextMenu={(e) => this.handleContextMenu(e, album)}
>
<Thumbnail size="medium" images={album.images} />
<div className="name">{ album.name }</div>
<div className="secondary">
{ album.artists ? <LinksSentence items={album.artists} /> : <span>-</span> }
</div>
</div>
);
},
)
}
</div>
</div>
</div>
);
}
return null;
}
}
const mapStateToProps = (state, ownProps) => ({});
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(GridSlider);

View File

@ -11,50 +11,63 @@ export default class Header extends React.Component {
};
}
handleContextMenuTrigger(e, options) {
// We have an override trigger (eg Album, Playlist)
if (this.props.handleContextMenuTrigger) {
return this.props.handleContextMenuTrigger(e);
}
handleContextMenuTrigger = (e, options) => {
const {
title,
handleContextMenuTrigger,
uiActions: {
showContextMenu,
},
} = this.props;
if (handleContextMenuTrigger) return handleContextMenuTrigger(e);
e.preventDefault();
const data = {
e,
context: 'custom',
title: this.props.title,
title,
options,
};
this.props.uiActions.showContextMenu(data);
showContextMenu(data);
}
renderContextMenuTrigger() {
// No custom trigger, nor any options
if (!this.props.handleContextMenuTrigger && !this.props.options) {
return null;
}
renderContextMenuTrigger = () => {
const {
handleContextMenuTrigger,
options,
} = this.props;
return <ContextMenuTrigger onTrigger={(e) => this.handleContextMenuTrigger(e, this.props.options)} />;
if (!handleContextMenuTrigger && !options) return null;
return <ContextMenuTrigger onTrigger={(e) => this.handleContextMenuTrigger(e, options)} />;
}
renderOptions() {
if (!this.props.options && !this.props.handleContextMenuTrigger) {
return null;
}
renderOptions = () => {
const {
handleContextMenuTrigger,
options,
} = this.props;
if (!options && !handleContextMenuTrigger) return null;
return (
<div className="header__options">
{this.renderContextMenuTrigger()}
<div className="header__options__wrapper">
{this.props.options || null}
{options || null}
</div>
</div>
);
}
render() {
render = () => {
const { className, children } = this.props;
return (
<header className={(this.props.className ? this.props.className : null)}>
<header className={className}>
<h1>
{this.props.children ? this.props.children : null}
{children}
</h1>
{this.renderOptions()}
</header>

View File

@ -2,39 +2,34 @@
import React, { memo } from 'react';
import FontAwesome from 'react-fontawesome';
export default memo((props) => {
if (!props.name || props.name === '') {
return null;
}
export default memo(({ name, type, className, onClick }) => {
if (!name || name === '') return null;
let className = `icon icon--${props.type ? props.type : 'material'}`;
if (props.className) {
className += ` ${props.className}`;
}
const fullClassName = `icon icon--${type || 'material'} ${className}`;
switch (props.type) {
switch (type) {
case 'svg':
return <img className={className} src={`/iris/assets/icons/${props.name}.svg`} onClick={(e) => (props.onClick ? props.onClick(e) : null)} />;
return <img className={fullClassName} src={`/iris/assets/icons/${name}.svg`} onClick={onClick} />;
case 'gif':
return <img className={className} src={`/iris/assets/icons/${props.name}.gif`} onClick={(e) => (props.onClick ? props.onClick(e) : null)} />;
return <img className={fullClassName} src={`/iris/assets/icons/${name}.gif`} onClick={onClick} />;
case 'fontawesome':
return <FontAwesome className={className} type="fontawesome" name={props.name} onClick={(e) => (props.onClick ? props.onClick(e) : null)} />;
return <FontAwesome className={fullClassName} type="fontawesome" name={name} onClick={onClick} />;
case 'css':
switch (props.name) {
case 'playing':
return (
<i className={`${className} icon--playing`}>
<span />
<span />
<span />
</i>
);
if (name === 'playing') {
return (
<i className={`${fullClassName} icon--playing`}>
<span />
<span />
<span />
</i>
);
}
break;
default:
return <i className={className} onClick={(e) => (props.onClick ? props.onClick(e) : null)}>{props.name}</i>;
return <i className={fullClassName} onClick={onClick}>{name}</i>;
}
});
});

View File

@ -2,7 +2,6 @@
import React from 'react';
import { withRouter } from 'react-router';
import { Link } from 'react-router-dom';
import { scrollTo } from '../util/helpers';
/**
@ -10,78 +9,79 @@ import { scrollTo } from '../util/helpers';
* which lets us scroll to the top of our <main> for a more traditional navigation experience
* */
class CustomLink extends React.Component {
constructor(props) {
super(props);
}
handleClick = () => {
const {
location: {
state,
},
history,
retainScroll,
scrollTo: scrollToProp,
} = this.props;
handleClick(e) {
// Fetch the current scroll position of our #main element and
// save to our history's state, so clicking 'back' etc will restore
// the previous scroll position.
// Note that this doesn't trigger lazy-load elements (unless scrolling
// exposes the LazyLoader component).
// Fetch the current scroll position of our #main element and save to our history's state, so
// clicking 'back' etc will restore the previous scroll position.
// This doesn't trigger lazy-load elements (unless scrolling exposes the LazyLoader component).
const main = document.getElementById('main');
const state = (this.props.location && this.props.location.state ? this.props.location.state : {});
state.scroll_position = main.scrollTop;
this.props.history.replace({ state });
history.replace({
...state,
scroll_position: main.scrollTop,
});
// Allow a link to disable auto-scrolling to the top of the page
// on navigation. Useful for tabs, etc.
if (!this.props.retainScroll) {
scrollTo(this.props.scrollTo, (this.props.scrollTo));
}
if (!retainScroll) scrollTo(scrollToProp, (scrollToProp));
}
handleContextMenu(e) {
if (this.props.onContextMenu) {
this.props.onContextMenu(e);
}
handleContextMenu = (e) => {
const { onContextMenu } = this.props;
if (onContextMenu) onContextMenu(e);
}
isLinkActive(link) {
// Decode both links
// This handles issues where one link is encoded and the other isn't,
// but they're otherwise identical
isLinkActive = (link) => {
const {
exact,
history: {
location: {
pathname,
} = {},
} = {},
} = this.props;
// Decode both links. This handles issues where one link is encoded and the other isn't, but
// they're otherwise identical
link = decodeURIComponent(link);
const current_link = decodeURIComponent(this.props.history.location.pathname);
const current_link = decodeURIComponent(pathname);
if (this.props.exact) {
return current_link === link;
}
if (exact) return current_link === link;
return current_link.startsWith(link);
}
render() {
let className = '';
if (this.props.className) {
className += this.props.className;
}
render = () => {
const {
className,
activeClassName,
to,
children,
history,
} = this.props;
if (!this.props.to) {
return <span className={className}>{this.props.children}</span>;
}
if (!to) return <span className={className}>{children}</span>;
// We have an active detector method
// This is used almost solely by the Sidebar navigation
if (this.props.history !== undefined) {
if (this.isLinkActive(this.props.to)) {
if (this.props.activeClassName) {
className += ` ${this.props.activeClassName}`;
} else {
className += ' active';
}
}
}
const active = history && this.isLinkActive(to) ? (activeClassName || 'active') : '';
return (
<Link
onClick={(e) => this.handleClick(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
className={className}
to={this.props.to}
onClick={this.handleClick}
onContextMenu={this.handleContextMenu}
className={`${className} ${active}`}
to={to}
>
{this.props.children}
{children}
</Link>
);
}

View File

@ -2,38 +2,41 @@
import React, { memo } from 'react';
import URILink from './URILink';
export default memo((props) => {
if (!props.items) {
return <span className={props.className ? `${props.className} links-sentence` : 'links-sentence'}>-</span>;
}
export default memo(({ items, className, nolinks }) => {
if (!items) return <span className={`${className} links-sentence`}>-</span>;
return (
<span className={props.className ? `${props.className} links-sentence` : 'links-sentence'}>
<span className={`${className} links-sentence`}>
{
props.items.map((item, index) => {
if (!item) {
return <span>-</span>;
}
items.map(({ name, uri }, index) => {
if (!name) return <span>-</span>;
let separator = null;
if (index == props.items.length - 2) {
if (index == items.length - 2) {
separator = ' and ';
} else if (index < props.items.length - 2) {
} else if (index < items.length - 2) {
separator = ', ';
}
if (!item.name) {
if (!name) {
var content = <span>-</span>;
} else if (!item.uri || props.nolinks) {
var content = <span>{ item.name }</span>;
} else if (!uri || nolinks) {
var content = <span>{name}</span>;
} else {
var content = <URILink className="links-sentence__item links-sentence__item--link" uri={item.uri}>{ item.name }</URILink>;
var content = (
<URILink
className="links-sentence__item links-sentence__item--link"
uri={uri}
>
{name}
</URILink>
);
}
return (
<span key={`index_${item.uri}`}>
{ content }
{ separator }
<span key={`index_${uri}`}>
{content}
{separator}
</span>
);
})

View File

@ -8,9 +8,7 @@ import Icon from '../../components/Icon';
import ErrorBoundary from '../../components/ErrorBoundary';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import {
formatImages,
} from '../../util/helpers';
import { formatImages } from '../../util/format';
class LibraryBrowse extends React.Component {
componentDidMount() {