Links for grid items, may need to add more context options

This commit is contained in:
James Barnsley
2019-01-16 16:46:40 +13:00
parent 1efe083002
commit d5c946456f
12 changed files with 2196 additions and 1885 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -98,7 +98,7 @@
// Release details // Release details
// These are automatically injected to built HTML // These are automatically injected to built HTML
var build = "1547510940"; var build = "1547579515";
var version = "3.31.8"; var version = "3.31.8";
// Construct the script tag // Construct the script tag

View File

@ -36,6 +36,7 @@ import LibraryAlbums from './views/library/LibraryAlbums';
import LibraryTracks from './views/library/LibraryTracks'; import LibraryTracks from './views/library/LibraryTracks';
import LibraryPlaylists from './views/library/LibraryPlaylists'; import LibraryPlaylists from './views/library/LibraryPlaylists';
import LibraryBrowse from './views/library/LibraryBrowse'; import LibraryBrowse from './views/library/LibraryBrowse';
import LibraryBrowseDirectory from './views/library/LibraryBrowseDirectory';
import EditPlaylist from './views/modals/EditPlaylist'; import EditPlaylist from './views/modals/EditPlaylist';
import CreatePlaylist from './views/modals/CreatePlaylist'; import CreatePlaylist from './views/modals/CreatePlaylist';
@ -387,30 +388,31 @@ class App extends React.Component{
<Switch> <Switch>
<Route exact path="/" component={Queue} /> <Route exact path="/" component={Queue} />
<Route path="/queue" component={Queue} /> <Route exact path="/queue" component={Queue} />
<Route path="/queue/history" component={QueueHistory} /> <Route exact path="/queue/history" component={QueueHistory} />
<Route path="/settings/debug" component={Debug} /> <Route exact path="/settings/debug" component={Debug} />
<Route path="/settings" component={Settings} /> <Route exact path="/settings" component={Settings} />
<Route path="/settings/service/:sub_view?" component={Settings} /> <Route exact path="/settings/service/:sub_view?" component={Settings} />
<Route path="/search/:type/:term?" component={Search} /> <Route exact path="/search/(:type/:term)?" component={Search} />
<Route path="/album/:uri" component={Album} /> <Route exact path="/album/:uri" component={Album} />
<Route path="/artist/:uri/:sub_view?" component={Artist} /> <Route exact path="/artist/:uri/:sub_view?" component={Artist} />
<Route path="/playlist/:uri" component={Playlist} /> <Route exact path="/playlist/:uri" component={Playlist} />
<Route path="/user/:uri" component={User} /> <Route exact path="/user/:uri" component={User} />
<Route path="/track/:uri" component={Track} /> <Route exact path="/track/:uri" component={Track} />
<Route path="/discover/recommendations/:seeds?" component={DiscoverRecommendations} /> <Route exact path="/discover/recommendations/:seeds?" component={DiscoverRecommendations} />
<Route path="/discover/featured" component={DiscoverFeatured} /> <Route exact path="/discover/featured" component={DiscoverFeatured} />
<Route path="/discover/categories" component={DiscoverCategories} /> <Route exact path="/discover/categories/:id" component={DiscoverCategory} />
<Route path="/discover/categories/:id" component={DiscoverCategory} /> <Route exact path="/discover/categories" component={DiscoverCategories} />
<Route path="/discover/new-releases" component={DiscoverNewReleases} /> <Route exact path="/discover/new-releases" component={DiscoverNewReleases} />
<Route path="/library/artists" component={LibraryArtists} /> <Route exact path="/library/artists" component={LibraryArtists} />
<Route path="/library/albums" component={LibraryAlbums} /> <Route exact path="/library/albums" component={LibraryAlbums} />
<Route path="/library/tracks" component={LibraryTracks} /> <Route exact path="/library/tracks" component={LibraryTracks} />
<Route path="/library/playlists" component={LibraryPlaylists} /> <Route exact path="/library/playlists" component={LibraryPlaylists} />
<Route path="/library/browse/:uri?" component={LibraryBrowse} /> <Route exact path="/library/browse/:uri" component={LibraryBrowseDirectory} />
<Route exact path="/library/browse" component={LibraryBrowse} />
<Route> <Route>
<h1>I'm lost</h1> <h1>I'm lost</h1>

View File

@ -26,7 +26,7 @@ export default class CategoryGrid extends React.Component{
key={category.id} key={category.id}
type="category" type="category"
item={category} item={category}
onClick={e => {this.props.history.push(global.baseURL+'discover/categories/'+encodeURIComponent(category.id))}} link={global.baseURL+'discover/categories/'+encodeURIComponent(category.id)}
/> />
) )
}) })

View File

@ -107,16 +107,26 @@ export default class GridItem extends React.Component{
images = this.props.item.icons; images = this.props.item.icons;
} }
if (this.props.link){
var link = this.props.link;
} else {
var link = global.baseURL+this.props.type+'/'+encodeURIComponent(item.uri);
}
return ( return (
<Link className={"grid__item grid__item--"+this.props.type} to={global.baseURL+this.props.type+'/'+encodeURIComponent(item.uri)} onClick={e => helpers.scrollTo()} onContextMenu={e => this.handleContextMenu(e)}> <Link
<Thumbnail size="medium" className="grid__item__thumbnail" images={images} /> className={"grid__item grid__item--"+this.props.type}
<div className="grid__item__name"> to={link}
{item.name ? item.name : <span className="opaque-text">{item.uri}</span>} onClick={e => helpers.scrollTo()}
</div> onContextMenu={e => this.handleContextMenu(e)}>
<div className="grid__item__secondary"> <Thumbnail size="medium" className="grid__item__thumbnail" images={images} />
{this.props.show_source_icon ? <Icon name={helpers.sourceIcon(item.uri)} type="fontawesome" className="source" /> : null} <div className="grid__item__name">
{this.renderSecondary(item)} {item.name ? item.name : <span className="opaque-text">{item.uri}</span>}
</div> </div>
<div className="grid__item__secondary">
{this.props.show_source_icon ? <Icon name={helpers.sourceIcon(item.uri)} type="fontawesome" className="source" /> : null}
{this.renderSecondary(item)}
</div>
</Link> </Link>
); );
} }

View File

@ -1,7 +1,9 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react';
import { connect } from 'react-redux' import { connect } from 'react-redux';
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux';
import { Redirect } from 'react-router';
import { Route, Switch } from 'react-router-dom';
import Link from '../components/Link'; import Link from '../components/Link';
import LazyLoadListener from '../components/LazyLoadListener' import LazyLoadListener from '../components/LazyLoadListener'
@ -105,18 +107,7 @@ class Artist extends React.Component{
this.props.uiActions.set({artist_albums_filter: value}); this.props.uiActions.set({artist_albums_filter: value});
} }
renderSubViewMenu(){ renderOverview(){
return (
<div className="sub-views" id="sub-views-menu">
<Link className="option" activeClassName="active" to={global.baseURL+'artist/'+this.props.uri} scrollTo="sub-views-menu"><h4>Overview</h4></Link>
{this.props.artist.related_artists_uris ? <Link className="option" activeClassName="active" to={global.baseURL+'artist/'+this.props.uri+'/related-artists'} scrollTo="sub-views-menu"><h4>Related artists</h4></Link> : null}
<Link className="option" activeClassName="active" to={global.baseURL+'artist/'+this.props.uri+'/about'} scrollTo="sub-views-menu"><h4>About</h4></Link>
</div>
)
}
renderBody(){
var scheme = helpers.uriSource(this.props.uri);
var artist = helpers.collate( var artist = helpers.collate(
this.props.artist, this.props.artist,
{ {
@ -134,144 +125,154 @@ class Artist extends React.Component{
artist.albums = helpers.applyFilter('type', this.props.filter, artist.albums); artist.albums = helpers.applyFilter('type', this.props.filter, artist.albums);
} }
switch (this.props.match.params.sub_view){ var sort_options = [
{
value: null,
label: 'Default'
},
{
value: 'name',
label: 'Name'
},
{
value: 'date',
label: 'Date'
},
{
value: 'tracks_total',
label: 'Tracks'
}
];
case 'related-artists': var filter_options = [
return ( {
<div className="body related-artists"> value: null,
<section className="grid-wrapper no-top-padding"> label: 'All'
<ArtistGrid artists={artist.related_artists} /> },
</section> {
</div> value: 'album',
) label: 'Albums'
},
{
value: 'single',
label: 'Singles'
},
{
value: 'compilation',
label: 'Compilations'
}
];
case 'about': if (!artist.tracks_uris || (artist.tracks_uris && !artist.tracks) || (artist.tracks_uris.length !== artist.tracks.length)){
var is_loading_tracks = true;
var thumbnails = []; } else {
if (artist.images && artist.images.length > 0){ var is_loading_tracks = false;
for (var i = 0; i < artist.images.length; i++){
thumbnails.push(
<div className="tile thumbnail-wrapper" key={i}>
<Thumbnail size="huge" canZoom images={artist.images[i]} />
</div>
);
}
}
return (
<div className="body about">
<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> : null}
{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> : null }
</div>
<div className="col col--w60 biography">
<section>
{ artist.biography ? <div className="biography-text"><p>{artist.biography}</p><br />
<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> : null }
</section>
</div>
</div>
)
default:
var sort_options = [
{
value: null,
label: 'Default'
},
{
value: 'name',
label: 'Name'
},
{
value: 'date',
label: 'Date'
},
{
value: 'tracks_total',
label: 'Tracks'
}
];
var filter_options = [
{
value: null,
label: 'All'
},
{
value: 'album',
label: 'Albums'
},
{
value: 'single',
label: 'Singles'
},
{
value: 'compilation',
label: 'Compilations'
}
];
if (!artist.tracks_uris || (artist.tracks_uris && !artist.tracks) || (artist.tracks_uris.length !== artist.tracks.length)){
var is_loading_tracks = true;
} else {
var is_loading_tracks = false;
}
return (
<div className="body overview">
<div className={"top-tracks col col--w"+(artist.related_artists && artist.related_artists.length > 0 ? "70" : "100")}>
{artist.tracks ? <h4>Top tracks</h4> : null}
<div className="list-wrapper">
<TrackList className="artist-track-list" uri={artist.uri} tracks={artist.tracks} />
<LazyLoadListener showLoader={is_loading_tracks} />
</div>
</div>
<div className="col col--w5"></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={global.baseURL+'artist/'+artist.uri+'/related-artists'} scrollTo="sub-views-menu" className="button button--default">All related artists</Link></div> : null}
<div className="cf"></div>
{artist.albums ? <div className="albums">
<h4>
Albums
<DropdownField
icon="sort"
name="Sort"
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() }}
/>
<DropdownField
icon="filter_list"
name="Filter"
value={this.props.filter}
options={filter_options}
handleChange={value => {this.setFilter(value); this.props.uiActions.hideContextMenu() }}
/>
</h4>
<section className="grid-wrapper no-top-padding">
<AlbumGrid albums={artist.albums} />
<LazyLoadListener
loadKey={artist.albums_more}
showLoader={artist.albums_more}
loadMore={() => this.loadMore()}
/>
</section>
</div> : null}
</div>
)
} }
return (
<div className="body overview">
<div className={"top-tracks col col--w"+(artist.related_artists && artist.related_artists.length > 0 ? "70" : "100")}>
{artist.tracks ? <h4>Top tracks</h4> : null}
<div className="list-wrapper">
<TrackList className="artist-track-list" uri={artist.uri} tracks={artist.tracks} />
<LazyLoadListener showLoader={is_loading_tracks} />
</div>
</div>
<div className="col col--w5"></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={global.baseURL+'artist/'+artist.uri+'/related-artists'} scrollTo="sub-views-menu" className="button button--default">All related artists</Link></div> : null}
<div className="cf"></div>
{artist.albums ? <div className="albums">
<h4>
Albums
<DropdownField
icon="sort"
name="Sort"
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() }}
/>
<DropdownField
icon="filter_list"
name="Filter"
value={this.props.filter}
options={filter_options}
handleChange={value => {this.setFilter(value); this.props.uiActions.hideContextMenu() }}
/>
</h4>
<section className="grid-wrapper no-top-padding">
<AlbumGrid albums={artist.albums} />
<LazyLoadListener
loadKey={artist.albums_more}
showLoader={artist.albums_more}
loadMore={() => this.loadMore()}
/>
</section>
</div> : null}
</div>
);
}
renderRelatedArtists(){
var artist = helpers.collate(
this.props.artist,
{
artists: this.props.artists
}
);
return (
<div className="body related-artists">
<section className="grid-wrapper no-top-padding">
<ArtistGrid artists={artist.related_artists} />
</section>
</div>
)
}
renderAbout(){
var artist = helpers.collate(
this.props.artist,
{
artists: this.props.artists
}
);
var thumbnails = [];
if (artist.images && artist.images.length > 0){
for (var i = 0; i < artist.images.length; i++){
thumbnails.push(
<div className="tile thumbnail-wrapper" key={i}>
<Thumbnail size="huge" canZoom images={artist.images[i]} />
</div>
);
}
}
return (
<div className="body about">
<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> : null}
{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> : null }
</div>
<div className="col col--w60 biography">
<section>
{ artist.biography ? <div className="biography-text"><p>{artist.biography}</p><br />
<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> : null }
</section>
</div>
</div>
);
} }
render(){ render(){
@ -316,11 +317,49 @@ class Artist extends React.Component{
{is_spotify ? <FollowButton uri={this.props.uri} removeText="Remove from library" addText="Add to library" is_following={this.inLibrary()} /> : null} {is_spotify ? <FollowButton uri={this.props.uri} removeText="Remove from library" addText="Add to library" is_following={this.inLibrary()} /> : null}
<ContextMenuTrigger className="white" onTrigger={e => this.handleContextMenu(e)} /> <ContextMenuTrigger className="white" onTrigger={e => this.handleContextMenu(e)} />
</div> </div>
{ this.renderSubViewMenu() } <div className="sub-views" id="sub-views-menu">
<Link
nav
className="option"
activeClassName="active"
to={global.baseURL+'artist/'+this.props.uri+'/overview'}
scrollTo="sub-views-menu">
<h4>Overview</h4>
</Link>
{this.props.artist.related_artists_uris ? <Link
nav
className="option"
activeClassName="active"
to={global.baseURL+'artist/'+this.props.uri+'/related-artists'}
scrollTo="sub-views-menu">
<h4>Related artists</h4>
</Link> : null}
<Link
nav
className="option"
activeClassName="active"
to={global.baseURL+'artist/'+this.props.uri+'/about'}
scrollTo="sub-views-menu">
<h4>About</h4>
</Link>
</div>
</div> </div>
</div> </div>
<div className="content-wrapper"> <div className="content-wrapper">
{this.renderBody()} <Switch>
<Route exact path="/artist/:id/related-artists">
{this.renderRelatedArtists()}
</Route>
<Route exact path="/artist/:id/about">
{this.renderAbout()}
</Route>
<Route exact path="/artist/:id/overview">
{this.renderOverview()}
</Route>
<Route>
{this.renderOverview()}
</Route>
</Switch>
</div> </div>
</div> </div>
); );

View File

@ -230,7 +230,7 @@ class Playlist extends React.Component{
<TrackList <TrackList
uri={playlist.uri} uri={playlist.uri}
className="playlist-track-list" className="playlist-track-list"
context={context} track_context={context}
tracks={playlist.tracks} tracks={playlist.tracks}
removeTracks={tracks_indexes => this.removeTracks(tracks_indexes)} removeTracks={tracks_indexes => this.removeTracks(tracks_indexes)}
reorderTracks={(indexes, index) => this.reorderTracks(indexes, index)} reorderTracks={(indexes, index) => this.reorderTracks(indexes, index)}

View File

@ -52,7 +52,7 @@ class Search extends React.Component{
componentWillReceiveProps(nextProps){ componentWillReceiveProps(nextProps){
// Query changed // Query changed
if (nextProps.params.term !== this.props.params.term || nextProps.params.type !== this.props.params.type){ if (nextProps.term !== this.props.term || nextProps.type !== this.props.type){
this.digestUri(nextProps); this.digestUri(nextProps);
} }
@ -69,13 +69,13 @@ class Search extends React.Component{
// Digest the URI query property // Digest the URI query property
// Triggered when the URL changes // Triggered when the URL changes
digestUri(props = this.props){ digestUri(props = this.props){
if (props.params && props.params.type && props.params.term){ if (props.params && props.type && props.term){
this.setState({ this.setState({
type: props.params.type, type: props.type,
term: props.params.term term: props.term
}); });
this.search(props.params.type, props.params.term); this.search(props.type, props.term);
} }
} }
@ -429,6 +429,8 @@ class Search extends React.Component{
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
return { return {
type: ownProps.match.params.type,
term: ownProps.match.params.term,
mopidy_connected: state.mopidy.connected, mopidy_connected: state.mopidy.connected,
albums: (state.core.albums ? state.core.albums : []), albums: (state.core.albums ? state.core.albums : []),
artists: (state.core.artists ? state.core.artists : []), artists: (state.core.artists ? state.core.artists : []),

View File

@ -24,7 +24,7 @@ class DiscoverCategory extends React.Component{
} }
componentWillReceiveProps(nextProps){ componentWillReceiveProps(nextProps){
if (nextProps.params.id != this.props.params.id){ if (nextProps.match.params.id != this.props.match.params.id){
this.loadCategory(); this.loadCategory();
} }
@ -43,11 +43,11 @@ class DiscoverCategory extends React.Component{
loadCategory(){ loadCategory(){
if (!this.props.category){ if (!this.props.category){
this.props.spotifyActions.getCategory(this.props.params.id); this.props.spotifyActions.getCategory(this.props.match.params.id);
} }
if (!this.props.category.playlists_uris){ if (!this.props.category.playlists_uris){
this.props.spotifyActions.getCategoryPlaylists(this.props.params.id); this.props.spotifyActions.getCategoryPlaylists(this.props.match.params.id);
} }
} }
@ -57,7 +57,7 @@ class DiscoverCategory extends React.Component{
null, null,
{ {
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED_MORE', type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED_MORE',
uri: 'category:'+this.props.params.id uri: 'category:'+this.props.match.params.id
} }
); );
} }
@ -104,18 +104,11 @@ class DiscoverCategory extends React.Component{
} }
} }
/**
* Export our component
*
* We also integrate our global store, using connect()
**/
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
return { return {
load_queue: state.ui.load_queue, load_queue: state.ui.load_queue,
playlists: state.core.playlists, playlists: state.core.playlists,
category: (state.spotify.categories && state.spotify.categories['category:'+ownProps.params.id] !== undefined ? state.spotify.categories['category:'+ownProps.params.id] : false ) category: (state.spotify.categories && state.spotify.categories['category:'+ownProps.match.params.id] !== undefined ? state.spotify.categories['category:'+ownProps.match.params.id] : false )
} }
} }

View File

@ -1,23 +1,23 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react';
import { connect } from 'react-redux' import { connect } from 'react-redux';
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux';
import { Route, Switch } from 'react-router-dom';
import Link from '../../components/Link'; import Link from '../../components/Link';
import Header from '../../components/Header' import Header from '../../components/Header';
import List from '../../components/List' import List from '../../components/List';
import TrackList from '../../components/TrackList' import TrackList from '../../components/TrackList';
import GridItem from '../../components/GridItem' import GridItem from '../../components/GridItem';
import DropdownField from '../../components/Fields/DropdownField' import DropdownField from '../../components/Fields/DropdownField';
import Icon from '../../components/Icon' import Icon from '../../components/Icon';
import URILink from '../../components/URILink' import URILink from '../../components/URILink';
import ErrorBoundary from '../../components/ErrorBoundary' import ErrorBoundary from '../../components/ErrorBoundary';
import * as helpers from '../../helpers' import * as helpers from '../../helpers';
import * as uiActions from '../../services/ui/actions' import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions' import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions'
class LibraryBrowse extends React.Component{ class LibraryBrowse extends React.Component{
@ -36,182 +36,19 @@ class LibraryBrowse extends React.Component{
if (!this.props.mopidy_connected && nextProps.mopidy_connected){ if (!this.props.mopidy_connected && nextProps.mopidy_connected){
this.loadDirectory(nextProps); this.loadDirectory(nextProps);
} }
// our uri changes
if (nextProps.match.params.uri != this.props.match.params.uri){
this.loadDirectory(nextProps);
}
} }
loadDirectory(props = this.props){ loadDirectory(props = this.props){
if (props.mopidy_connected){ if (props.mopidy_connected){
var uri = null var uri = null
if (props.match.params.uri !== undefined){ if (props.uri !== undefined){
uri = props.match.params.uri; uri = props.uri;
} }
this.props.mopidyActions.getDirectory(uri); this.props.mopidyActions.getDirectory(uri);
} }
} }
playAll(e, tracks){ render(){
var tracks_uris = helpers.arrayOf('uri',tracks);
this.props.mopidyActions.playURIs(tracks_uris, "iris:browse:"+this.props.match.params.uri);
this.props.uiActions.hideContextMenu();
}
goBack(e){
window.history.back();
this.props.uiActions.hideContextMenu();
}
renderBreadcrumbs(){
if (this.props.match.params.uri){
var parent_uri = this.props.match.params.uri;
} else {
return null;
}
if (parent_uri.startsWith('file://')){
var uri = parent_uri.replace('file:///','');
var uri_elements = uri.split('/');
return (
<h4>
{uri_elements.map((uri_element, index) => {
// Reconstruct a URL to this element
var uri = "file://";
for (var i = 0; i <= index; i++){
uri += "/"+uri_elements[i];
}
return (
<span key={uri}>
{index > 0 ? <span>&nbsp; <Icon type="fontawesome" name="angle-right" /> &nbsp;&nbsp;</span> : null}
<URILink type="browse" uri={uri}>
{decodeURI(uri_element)}
</URILink>
</span>
);
})}
</h4>
);
}
return null;
}
renderSubdirectories(subdirectories){
if (this.props.view == 'list'){
return (
<List
nocontext
rows={subdirectories}
className="library-local-directory-list"
link_prefix={global.baseURL+'library/browse/'}
/>
);
} else {
return (
<div className="grid category-grid">
{
subdirectories.map(subdirectory => {
return (
<GridItem
key={subdirectory.uri}
type="category"
item={subdirectory}
onClick={e => {this.props.history.push(global.baseURL+'library/browse/'+encodeURIComponent(subdirectory.uri))}}
/>
);
})
}
</div>
);
}
}
renderDirectory(directory){
var title = 'Browse';
var uri_exploded = this.props.match.params.uri.split(':');
if (uri_exploded.length > 0){
title = uri_exploded[0];
title = title.charAt(0).toUpperCase() + title.slice(1);
}
if (!this.props.directory || helpers.isLoading(this.props.load_queue,['mopidy_browse'])){
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>
</div>
</div>
)
}
var tracks = (this.props.directory.tracks && this.props.directory.tracks.length > 0 ? this.props.directory.tracks : null);
tracks = helpers.sortItems(tracks, 'name');
var subdirectories = (this.props.directory.subdirectories && this.props.directory.subdirectories.length > 0 ? this.props.directory.subdirectories : null);
subdirectories = helpers.sortItems(subdirectories, 'name');
var view_options = [
{
label: 'Thumbnails',
value: 'thumbnails'
},
{
label: 'List',
value: 'list'
}
];
var options = (
<span>
<DropdownField
icon="visibility"
name="View"
value={this.props.view}
options={view_options}
handleChange={value => {this.props.uiActions.set({ library_directory_view: value }); this.props.uiActions.hideContextMenu()}}
/>
{tracks ? <a className="button button--no-hover" onClick={e => {this.props.uiActions.hideContextMenu(); this.playAll(e, tracks)}}>
<Icon name="play_circle_filled" />Play all
</a> : null }
<a className="button button--no-hover" onClick={e => {this.props.uiActions.hideContextMenu(); this.goBack(e)}}>
<Icon name="keyboard_backspace" />Back
</a>
</span>
);
return (
<div className="view library-local-view">
<Header options={options} uiActions={this.props.uiActions}>
<Icon name="folder" type="material" />
{title}
</Header>
<section className="content-wrapper">
<ErrorBoundary>
{this.renderBreadcrumbs()}
{subdirectories ? this.renderSubdirectories(subdirectories) : null}
{tracks ? <TrackList
tracks={this.props.directory.tracks}
uri={"iris:browse:"+this.props.match.params.uri}
className="library-local-track-list"
/> : null }
</ErrorBoundary>
</section>
</div>
);
}
renderIndex(){
var grid_items = [] var grid_items = []
if (this.props.directory){ if (this.props.directory){
for (var subdirectory of this.props.directory.subdirectories){ for (var subdirectory of this.props.directory.subdirectories){
@ -269,7 +106,7 @@ class LibraryBrowse extends React.Component{
return ( return (
<div className="view library-local-view"> <div className="view library-local-view">
<Header uiActions={this.props.uiActions}> <Header>
<Icon name="folder" type="material" /> <Icon name="folder" type="material" />
Browse Browse
</Header> </Header>
@ -284,7 +121,6 @@ class LibraryBrowse extends React.Component{
item={item} item={item}
key={index} key={index}
type="category" type="category"
onClick={e => this.props.history.push(item.link)}
/> />
) )
} }
@ -296,14 +132,6 @@ class LibraryBrowse extends React.Component{
</div> </div>
); );
} }
render(){
if (this.props.match.params.uri !== undefined && this.props.match.params.uri){
return this.renderDirectory();
} else {
return this.renderIndex();
}
}
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
@ -318,8 +146,7 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
uiActions: bindActionCreators(uiActions, dispatch), uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch)
spotifyActions: bindActionCreators(spotifyActions, dispatch)
} }
} }

View File

@ -0,0 +1,235 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from '../../components/Link';
import Header from '../../components/Header';
import List from '../../components/List';
import TrackList from '../../components/TrackList';
import GridItem from '../../components/GridItem';
import DropdownField from '../../components/Fields/DropdownField';
import Icon from '../../components/Icon';
import URILink from '../../components/URILink';
import ErrorBoundary from '../../components/ErrorBoundary';
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 LibraryBrowseDirectory extends React.Component{
constructor(props){
super(props);
}
componentDidMount(){
this.props.uiActions.setWindowTitle("Browse");
this.loadDirectory();
}
componentWillReceiveProps(nextProps){
// mopidy goes online
if (!this.props.mopidy_connected && nextProps.mopidy_connected){
this.loadDirectory(nextProps);
}
// our uri changes
if (nextProps.uri && nextProps.uri !== this.props.uri){
console.log(nextProps.uri,this.props.uri);
//this.loadDirectory(nextProps);
}
}
loadDirectory(props = this.props){
if (props.mopidy_connected){
var uri = null
if (props.uri !== undefined){
uri = props.uri;
}
this.props.mopidyActions.getDirectory(uri);
}
}
playAll(e, tracks){
var tracks_uris = helpers.arrayOf('uri',tracks);
this.props.mopidyActions.playURIs(tracks_uris, "iris:browse:"+this.props.uri);
this.props.uiActions.hideContextMenu();
}
goBack(e){
window.history.back();
this.props.uiActions.hideContextMenu();
}
renderBreadcrumbs(){
if (this.props.uri){
var parent_uri = this.props.uri;
} else {
return null;
}
if (parent_uri.startsWith('file://')){
var uri = parent_uri.replace('file:///','');
var uri_elements = uri.split('/');
return (
<h4>
{uri_elements.map((uri_element, index) => {
// Reconstruct a URL to this element
var uri = "file://";
for (var i = 0; i <= index; i++){
uri += "/"+uri_elements[i];
}
return (
<span key={uri}>
{index > 0 ? <span>&nbsp; <Icon type="fontawesome" name="angle-right" /> &nbsp;&nbsp;</span> : null}
<URILink type="browse" uri={uri}>
{decodeURI(uri_element)}
</URILink>
</span>
);
})}
</h4>
);
}
return null;
}
renderSubdirectories(subdirectories){
if (this.props.view == 'list'){
return (
<List
nocontext
rows={subdirectories}
className="library-local-directory-list"
link_prefix={global.baseURL+'library/browse/'}
/>
);
} else {
return (
<div className="grid category-grid">
{
subdirectories.map(subdirectory => {
return (
<GridItem
key={subdirectory.uri}
type="directory"
item={subdirectory}
/>
);
})
}
</div>
);
}
}
render(){
var title = 'Directory';
var uri_exploded = this.props.uri.split(':');
if (uri_exploded.length > 0){
title = uri_exploded[0];
title = title.charAt(0).toUpperCase() + title.slice(1);
}
if (!this.props.directory || helpers.isLoading(this.props.load_queue,['mopidy_browse'])){
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>
</div>
</div>
)
}
var tracks = (this.props.directory.tracks && this.props.directory.tracks.length > 0 ? this.props.directory.tracks : null);
tracks = helpers.sortItems(tracks, 'name');
var subdirectories = (this.props.directory.subdirectories && this.props.directory.subdirectories.length > 0 ? this.props.directory.subdirectories : null);
subdirectories = helpers.sortItems(subdirectories, 'name');
var view_options = [
{
label: 'Thumbnails',
value: 'thumbnails'
},
{
label: 'List',
value: 'list'
}
];
var options = (
<span>
<DropdownField
icon="visibility"
name="View"
value={this.props.view}
options={view_options}
handleChange={value => {this.props.uiActions.set({ library_directory_view: value }); this.props.uiActions.hideContextMenu()}}
/>
{tracks ? <a className="button button--no-hover" onClick={e => {this.props.uiActions.hideContextMenu(); this.playAll(e, tracks)}}>
<Icon name="play_circle_filled" />Play all
</a> : null }
<a className="button button--no-hover" onClick={e => {this.props.uiActions.hideContextMenu(); this.goBack(e)}}>
<Icon name="keyboard_backspace" />Back
</a>
</span>
);
return (
<div className="view library-local-view">
<Header options={options} uiActions={this.props.uiActions}>
<Icon name="folder" type="material" />
{title}
</Header>
<section className="content-wrapper">
<ErrorBoundary>
{this.renderBreadcrumbs()}
{subdirectories ? this.renderSubdirectories(subdirectories) : null}
{tracks ? <TrackList
tracks={this.props.directory.tracks}
uri={"iris:browse:"+this.props.uri}
className="library-local-track-list"
/> : null }
</ErrorBoundary>
</section>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
var uri = decodeURIComponent(ownProps.match.params.uri);
return {
uri: uri,
load_queue: state.ui.load_queue,
mopidy_connected: state.mopidy.connected,
directory: state.mopidy.directory,
view: state.ui.library_directory_view
}
}
const mapDispatchToProps = (dispatch) => {
return {
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LibraryBrowseDirectory)