Sorting playlist tracks, fixes #568
This commit is contained in:
@ -7,6 +7,7 @@ import * as mopidyActions from '../services/mopidy/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import { isTouchDevice } from '../util/helpers';
|
||||
import { arrayOf } from '../util/arrays';
|
||||
import { i18n } from '../locale';
|
||||
|
||||
class TrackList extends React.Component {
|
||||
constructor(props) {
|
||||
@ -105,14 +106,18 @@ class TrackList extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
handleDrop(e, track_key) {
|
||||
if (this.props.dragger && this.props.dragger.active) {
|
||||
// if this tracklist handles sorting, handle it
|
||||
if (this.props.reorderTracks !== undefined) {
|
||||
const indexes = this.props.dragger.victims_indexes;
|
||||
const tracks = this.digestTracksKeys([track_key]);
|
||||
return this.props.reorderTracks(indexes, tracks[0].index);
|
||||
}
|
||||
handleDrop = (e, track_key) => {
|
||||
const {
|
||||
dragger: {
|
||||
active,
|
||||
victims_indexes,
|
||||
} = {},
|
||||
reorderTracks: doReorderTracks,
|
||||
} = this.props;
|
||||
|
||||
if (active && doReorderTracks) {
|
||||
const tracks = this.digestTracksKeys([track_key]);
|
||||
return doReorderTracks(victims_indexes, tracks[0].index);
|
||||
}
|
||||
this.touch_dragging_tracks_keys = false;
|
||||
}
|
||||
@ -280,42 +285,53 @@ class TrackList extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
playTracks(tracks_keys = null) {
|
||||
if (tracks_keys !== null) {
|
||||
var selected_tracks = this.digestTracksKeys(tracks_keys);
|
||||
} else {
|
||||
var selected_tracks = this.digestTracksKeys();
|
||||
}
|
||||
const selected_tracks_indexes = arrayOf('index', selected_tracks);
|
||||
playTracks = (tracks_keys = null) => {
|
||||
const {
|
||||
uri,
|
||||
uiActions: {
|
||||
createNotification,
|
||||
},
|
||||
mopidyActions: {
|
||||
playURIs,
|
||||
},
|
||||
playTracks: doPlayTracks,
|
||||
} = this.props;
|
||||
|
||||
const selected_tracks = tracks_keys !== null
|
||||
? this.digestTracksKeys(tracks_keys)
|
||||
: this.digestTracksKeys();
|
||||
|
||||
if (selected_tracks.length <= 0) {
|
||||
return this.props.uiActions.createNotification({ content: 'No tracks selected', level: 'error' });
|
||||
createNotification({ content: i18n('errors.nothing_selected'), level: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Our parent handles playing
|
||||
if (this.props.playTracks !== undefined) {
|
||||
return this.props.playTracks(selected_tracks);
|
||||
|
||||
// Default to playing the URIs
|
||||
if (doPlayTracks) {
|
||||
doPlayTracks(selected_tracks);
|
||||
return;
|
||||
}
|
||||
const selected_tracks_uris = arrayOf('uri', selected_tracks);
|
||||
return this.props.mopidyActions.playURIs(selected_tracks_uris, this.props.uri);
|
||||
playURIs(selected_tracks_uris, uri);
|
||||
}
|
||||
|
||||
removeTracks() {
|
||||
removeTracks = () => {
|
||||
const {
|
||||
uiActions: {
|
||||
createNotification,
|
||||
},
|
||||
removeTracks: doRemoveTracks,
|
||||
} = this.props;
|
||||
|
||||
const selected_tracks = this.digestTracksKeys();
|
||||
|
||||
// Our parent has a handler for this
|
||||
if (this.props.removeTracks !== undefined) {
|
||||
const selected_tracks_indexes = arrayOf('index', selected_tracks);
|
||||
return this.props.removeTracks(selected_tracks_indexes);
|
||||
|
||||
// No handler? We can't really do anything then, so notify user
|
||||
if (!doRemoveTracks) {
|
||||
createNotification({ content: `Cannot delete ${selected_tracks.length > 1 ? 'these tracks' : 'this track'}`, level: 'error' });
|
||||
return;
|
||||
}
|
||||
this.props.uiActions.createNotification({ content: `Cannot delete ${selected_tracks.length > 1 ? 'these tracks' : 'this track'}`, level: 'error' });
|
||||
const selected_tracks_indexes = arrayOf('index', selected_tracks);
|
||||
doRemoveTracks(selected_tracks_indexes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build the track key
|
||||
* This is our unique reference to a track in a particular tracklist
|
||||
@ -333,7 +349,6 @@ class TrackList extends React.Component {
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Digest our selected tracks
|
||||
*
|
||||
@ -379,7 +394,6 @@ class TrackList extends React.Component {
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
if (!this.props.tracks || Object.prototype.toString.call(this.props.tracks) !== '[object Array]') {
|
||||
return null;
|
||||
@ -398,25 +412,25 @@ class TrackList extends React.Component {
|
||||
const track_key = this.buildTrackKey(track, index);
|
||||
track.key = track_key;
|
||||
return (
|
||||
<Track
|
||||
show_source_icon={this.props.show_source_icon}
|
||||
key={track_key}
|
||||
mini_zones={this.props.slim_mode || isTouchDevice()}
|
||||
track={track}
|
||||
track_context={this.props.track_context}
|
||||
can_sort={this.props.track_context == 'queue' || this.props.track_context == 'editable-playlist'}
|
||||
selected={this.props.selected_tracks.includes(track_key)}
|
||||
play_state={this.props.play_state}
|
||||
dragger={this.props.dragger}
|
||||
handleClick={(e) => this.handleClick(e, track_key)}
|
||||
handleDoubleClick={(e) => this.handleDoubleClick(e, track_key)}
|
||||
handleContextMenu={(e) => this.handleContextMenu(e, track_key)}
|
||||
handleDrag={(e) => this.handleDrag(e, track_key)}
|
||||
handleDrop={(e) => this.handleDrop(e, track_key)}
|
||||
handleTap={(e) => this.handleTap(e, track_key)}
|
||||
handleDoubleTap={(e) => this.handleDoubleTap(e, track_key)}
|
||||
handleTouchDrag={(e) => this.handleTouchDrag(e, track_key)}
|
||||
/>
|
||||
<Track
|
||||
show_source_icon={this.props.show_source_icon}
|
||||
key={track_key}
|
||||
mini_zones={this.props.slim_mode || isTouchDevice()}
|
||||
track={track}
|
||||
track_context={this.props.track_context}
|
||||
can_sort={this.props.track_context == 'queue' || this.props.track_context == 'editable-playlist'}
|
||||
selected={this.props.selected_tracks.includes(track_key)}
|
||||
play_state={this.props.play_state}
|
||||
dragger={this.props.dragger}
|
||||
handleClick={(e) => this.handleClick(e, track_key)}
|
||||
handleDoubleClick={(e) => this.handleDoubleClick(e, track_key)}
|
||||
handleContextMenu={(e) => this.handleContextMenu(e, track_key)}
|
||||
handleDrag={(e) => this.handleDrag(e, track_key)}
|
||||
handleDrop={(e) => this.handleDrop(e, track_key)}
|
||||
handleTap={(e) => this.handleTap(e, track_key)}
|
||||
handleDoubleTap={(e) => this.handleDoubleTap(e, track_key)}
|
||||
handleTouchDrag={(e) => this.handleTouchDrag(e, track_key)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)
|
||||
@ -426,7 +440,7 @@ class TrackList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
play_state: state.mopidy.play_state,
|
||||
slim_mode: state.ui.slim_mode,
|
||||
selected_tracks: state.ui.selected_tracks,
|
||||
|
||||
@ -14,6 +14,10 @@ errors:
|
||||
required: Required
|
||||
unknown_error: Unknown error
|
||||
need_to_be_online: You need to be online load this resource
|
||||
nothing_selected: Nothing selected
|
||||
cannot_reorder:
|
||||
title: Cannot reorder items
|
||||
description: Remove custom sort and try again
|
||||
actions:
|
||||
play: Play
|
||||
play_all: Play all
|
||||
@ -39,6 +43,9 @@ actions:
|
||||
delete: Delete
|
||||
remove: Remove
|
||||
cancel: Cancel
|
||||
created: Created %{name}
|
||||
saved: Saved %{name}
|
||||
deleted: Deleted %{name}
|
||||
common:
|
||||
popularity: Popularity
|
||||
name: Name
|
||||
@ -281,6 +288,15 @@ playlist:
|
||||
title: Playlist
|
||||
title_window: '%{name} (playlist)'
|
||||
title_plural: Playlists
|
||||
tracks:
|
||||
title: Tracks
|
||||
sort:
|
||||
default: Default
|
||||
sort_id: As loaded
|
||||
added_at: Date added
|
||||
name: Name
|
||||
artist: Artist
|
||||
album: Album
|
||||
queue_history:
|
||||
title: Playback history
|
||||
search:
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
formatTracks,
|
||||
formatTrack,
|
||||
formatSimpleObject,
|
||||
injectSortId,
|
||||
} from '../../util/format';
|
||||
import { handleException } from './actions';
|
||||
|
||||
@ -284,6 +285,8 @@ const CoreMiddleware = (function () {
|
||||
break;
|
||||
}
|
||||
|
||||
// This applies our new sort order based on the origional request (rather than a response)
|
||||
// This means we don't need to re-fetch the whole playlist after every sort.
|
||||
case 'PLAYLIST_TRACKS_REORDERED': {
|
||||
const {
|
||||
key,
|
||||
@ -306,9 +309,11 @@ const CoreMiddleware = (function () {
|
||||
tracks.splice(insert_before, 0, tracks_to_move[i]);
|
||||
}
|
||||
|
||||
console.log({ action, tracks: injectSortId(tracks) });
|
||||
|
||||
store.dispatch(coreActions.itemLoaded({
|
||||
...playlist,
|
||||
tracks,
|
||||
tracks: injectSortId(tracks),
|
||||
snapshot_id,
|
||||
}));
|
||||
break;
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
formatArtists,
|
||||
formatArtist,
|
||||
formatPlaylist,
|
||||
injectSortId,
|
||||
} from '../../util/format';
|
||||
import {
|
||||
arrayOf,
|
||||
@ -1177,7 +1178,7 @@ const MopidyMiddleware = (function () {
|
||||
...(fullTracks.length ? fullTracks[0] : {}),
|
||||
};
|
||||
});
|
||||
playlist.tracks = formatTracks(tracks);
|
||||
playlist.tracks = injectSortId(formatTracks(tracks));
|
||||
store.dispatch(coreActions.itemLoaded(playlist));
|
||||
});
|
||||
}
|
||||
@ -1188,13 +1189,9 @@ const MopidyMiddleware = (function () {
|
||||
case 'MOPIDY_ADD_PLAYLIST_TRACKS':
|
||||
request(store, 'playlists.lookup', { uri: action.key })
|
||||
.then((response) => {
|
||||
const tracks = [];
|
||||
for (let i = 0; i < action.tracks_uris.length; i++) {
|
||||
tracks.push({
|
||||
__model__: 'Track',
|
||||
uri: action.tracks_uris[i],
|
||||
});
|
||||
}
|
||||
const tracks = injectSortId(
|
||||
action.tracks_uris.map((uri) => ({ __model__: 'Track', uri })),
|
||||
);
|
||||
|
||||
const playlist = { ...response };
|
||||
if (playlist.tracks) {
|
||||
@ -1204,7 +1201,7 @@ const MopidyMiddleware = (function () {
|
||||
}
|
||||
|
||||
request(store, 'playlists.save', { playlist })
|
||||
.then((response) => {
|
||||
.then(() => {
|
||||
store.dispatch({
|
||||
type: 'PLAYLIST_TRACKS_ADDED',
|
||||
key: action.key,
|
||||
@ -1288,7 +1285,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 (let i = 0; i < range_length; i += 1) {
|
||||
// add to FRONT: we work backwards to avoid screwing up our indexes
|
||||
tracks_to_move.unshift(tracks[range_start + i]);
|
||||
}
|
||||
@ -1297,18 +1294,19 @@ const MopidyMiddleware = (function () {
|
||||
tracks.splice(range_start, range_length);
|
||||
|
||||
// now plug them back in, in their new location
|
||||
for (var i = 0; i < tracks_to_move.length; i++) {
|
||||
for (let i = 0; i < tracks_to_move.length; i += 1) {
|
||||
tracks.splice(insert_before, 0, tracks_to_move[i]);
|
||||
}
|
||||
|
||||
// update playlist
|
||||
playlist = { ...playlist, tracks };
|
||||
request(store, 'playlists.save', { playlist })
|
||||
.then((response) => {
|
||||
.then(() => {
|
||||
store.dispatch({
|
||||
type: 'MOPIDY_RESOLVE_PLAYLIST_TRACKS',
|
||||
tracks: playlist.tracks,
|
||||
key: playlist.uri,
|
||||
type: 'PLAYLIST_TRACKS_REORDERED',
|
||||
key: action.key,
|
||||
range_start,
|
||||
range_length,
|
||||
insert_before: action.insert_before, // We've adjusted this, so use original
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1321,7 +1319,9 @@ const MopidyMiddleware = (function () {
|
||||
...formatPlaylist(response),
|
||||
...action,
|
||||
};
|
||||
store.dispatch(uiActions.createNotification({ content: 'Created playlist' }));
|
||||
store.dispatch(uiActions.createNotification({
|
||||
content: i18n('actions.created', { name: i18n('playlist.title') }),
|
||||
}));
|
||||
store.dispatch(coreActions.addToLibrary('mopidy:library:playlists', playlist));
|
||||
});
|
||||
break;
|
||||
@ -1329,7 +1329,9 @@ const MopidyMiddleware = (function () {
|
||||
case 'MOPIDY_DELETE_PLAYLIST':
|
||||
request(store, 'playlists.delete', { uri: action.uri })
|
||||
.then(() => {
|
||||
store.dispatch(uiActions.createNotification({ content: 'Deleted playlist' }));
|
||||
store.dispatch(uiActions.createNotification({
|
||||
content: i18n('actions.deleted', { name: i18n('playlist.title') }),
|
||||
}));
|
||||
store.dispatch(coreActions.removeFromLibrary('mopidy:library:playlists', action.uri));
|
||||
});
|
||||
break;
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
formatAlbums,
|
||||
formatImages,
|
||||
formatTrack,
|
||||
injectSortId,
|
||||
} from '../../util/format';
|
||||
import URILink from '../../components/URILink';
|
||||
import { getItem } from '../../util/selectors';
|
||||
@ -1328,7 +1329,7 @@ export function getPlaylist(uri, { full, forceRefetch, callbackAction } = {}) {
|
||||
request(dispatch, getState, endpoint)
|
||||
.then(
|
||||
(response) => {
|
||||
let tracks = formatTracks(response.tracks.items);
|
||||
let tracks = injectSortId(formatTracks(response.tracks.items));
|
||||
|
||||
// convert links in description
|
||||
let description = null;
|
||||
@ -1358,7 +1359,7 @@ export function getPlaylist(uri, { full, forceRefetch, callbackAction } = {}) {
|
||||
} else {
|
||||
dispatch(coreActions.itemLoaded({
|
||||
uri,
|
||||
tracks,
|
||||
tracks: injectSortId(tracks),
|
||||
}));
|
||||
|
||||
if (callbackAction) {
|
||||
@ -1452,10 +1453,14 @@ export function deleteTracksFromPlaylist(uri, snapshot_id, tracks_indexes) {
|
||||
|
||||
export function reorderPlaylistTracks(uri, range_start, range_length, insert_before, snapshot_id) {
|
||||
return (dispatch, getState) => {
|
||||
request(dispatch, getState, `playlists/${getFromUri('playlistid', uri)}/tracks`, 'PUT', {
|
||||
uri, range_start, range_length, insert_before, snapshot_id,
|
||||
})
|
||||
.then(
|
||||
request(
|
||||
dispatch,
|
||||
getState,
|
||||
`playlists/${getFromUri('playlistid', uri)}/tracks`, 'PUT',
|
||||
{
|
||||
uri, range_start, range_length, insert_before, snapshot_id,
|
||||
},
|
||||
).then(
|
||||
(response) => {
|
||||
dispatch({
|
||||
type: 'PLAYLIST_TRACKS_REORDERED',
|
||||
|
||||
@ -929,6 +929,17 @@ const collate = function (obj, indexes = {}) {
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the array index as our sort_id.
|
||||
* This allows us a source of truth for the original order of items when we have applied
|
||||
* sort rules in the UI.
|
||||
*
|
||||
* @param {Array} array of objects to have sort_id added
|
||||
*/
|
||||
const injectSortId = (array) => {
|
||||
return array.map((item, index) => ({ ...item, sort_id: index }));
|
||||
};
|
||||
|
||||
export {
|
||||
toJSON,
|
||||
getTrackIcon,
|
||||
@ -952,6 +963,7 @@ export {
|
||||
formatCategories,
|
||||
collate,
|
||||
collateLibrary,
|
||||
injectSortId,
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -977,4 +989,5 @@ export default {
|
||||
formatCategories,
|
||||
collate,
|
||||
collateLibrary,
|
||||
injectSortId,
|
||||
};
|
||||
|
||||
@ -5,7 +5,6 @@ import { bindActionCreators } from 'redux';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import ErrorMessage from '../components/ErrorMessage';
|
||||
import Link from '../components/Link';
|
||||
import LazyLoadListener from '../components/LazyLoadListener';
|
||||
import TrackList from '../components/TrackList';
|
||||
import AlbumGrid from '../components/AlbumGrid';
|
||||
import Thumbnail from '../components/Thumbnail';
|
||||
@ -240,35 +239,37 @@ class Artist extends React.Component {
|
||||
|
||||
<div className="albums">
|
||||
<h4>
|
||||
<div><I18n path="artist.overview.albums" /></div>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name="Sort"
|
||||
value={sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
selected_icon={sort ? (sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
||||
handleChange={this.onChangeSort}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="filter_list"
|
||||
name="Filter"
|
||||
value={filter}
|
||||
valueAsLabel
|
||||
options={filter_options}
|
||||
handleChange={this.onChangeFilter}
|
||||
/>
|
||||
{(sort || filter) && (
|
||||
<Button
|
||||
discrete
|
||||
type="destructive"
|
||||
size="small"
|
||||
onClick={this.onResetFilters}
|
||||
>
|
||||
<Icon name="clear" />
|
||||
<I18n path="actions.reset" />
|
||||
</Button>
|
||||
)}
|
||||
<I18n path="artist.overview.albums" />
|
||||
<div className="actions-wrapper">
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name="Sort"
|
||||
value={sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
selected_icon={sort ? (sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
||||
handleChange={this.onChangeSort}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="filter_list"
|
||||
name="Filter"
|
||||
value={filter}
|
||||
valueAsLabel
|
||||
options={filter_options}
|
||||
handleChange={this.onChangeFilter}
|
||||
/>
|
||||
{(sort || filter) && (
|
||||
<Button
|
||||
discrete
|
||||
type="destructive"
|
||||
size="small"
|
||||
onClick={this.onResetFilters}
|
||||
>
|
||||
<Icon name="clear" />
|
||||
<I18n path="actions.reset" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<section className="grid-wrapper no-top-padding">
|
||||
|
||||
@ -99,7 +99,7 @@ class Debug extends React.Component {
|
||||
<div className="view debugger-view">
|
||||
<Header options={options} uiActions={uiActions}>
|
||||
<Icon name="settings" type="material" />
|
||||
<I18n path="debug.title" />
|
||||
<I18n path="debug.title" />
|
||||
</Header>
|
||||
|
||||
<div className="content-wrapper">
|
||||
|
||||
@ -14,6 +14,7 @@ import Loader from '../components/Loader';
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger';
|
||||
import URILink from '../components/URILink';
|
||||
import Icon from '../components/Icon';
|
||||
import DropdownField from '../components/Fields/DropdownField';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
@ -24,6 +25,7 @@ import {
|
||||
sourceIcon,
|
||||
decodeMopidyUri,
|
||||
} from '../util/helpers';
|
||||
import { trackEvent } from '../components/Trackable';
|
||||
import { i18n, I18n } from '../locale';
|
||||
import { makeItemSelector, makeLoadingSelector } from '../util/selectors';
|
||||
import { sortItems } from '../util/arrays';
|
||||
@ -126,17 +128,61 @@ class Playlist extends React.Component {
|
||||
deletePlaylist(uri);
|
||||
}
|
||||
|
||||
onChangeSort = (value) => {
|
||||
const {
|
||||
sort,
|
||||
sort_reverse,
|
||||
uiActions: {
|
||||
set,
|
||||
hideContextMenu,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
let reverse = false;
|
||||
if (value !== null && sort === value) {
|
||||
reverse = !sort_reverse;
|
||||
}
|
||||
|
||||
set({
|
||||
playlist_tracks_sort_reverse: reverse,
|
||||
playlist_tracks_sort: value,
|
||||
});
|
||||
hideContextMenu();
|
||||
trackEvent({ category: 'Playlist', action: 'SortTracks', label: `${value} ${reverse ? 'DESC' : 'ASC'}` });
|
||||
}
|
||||
|
||||
reorderTracks = (indexes, index) => {
|
||||
const {
|
||||
coreActions: {
|
||||
reorderPlaylistTracks,
|
||||
},
|
||||
uiActions: {
|
||||
createNotification,
|
||||
},
|
||||
playlist: {
|
||||
uri,
|
||||
snapshot_id,
|
||||
tracks,
|
||||
},
|
||||
sort,
|
||||
sort_reverse,
|
||||
} = this.props;
|
||||
|
||||
if (sort !== 'sort_id') {
|
||||
createNotification({
|
||||
content: i18n('errors.cannot_reorder.title'),
|
||||
description: i18n('errors.cannot_reorder.description'),
|
||||
level: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sort_reverse) {
|
||||
const count = tracks.length - 1;
|
||||
index = count - index + 1;
|
||||
indexes = indexes.map((index) => count - index);
|
||||
}
|
||||
|
||||
reorderPlaylistTracks(uri, indexes, index, snapshot_id);
|
||||
}
|
||||
|
||||
@ -274,6 +320,8 @@ class Playlist extends React.Component {
|
||||
playlist,
|
||||
loading,
|
||||
slim_mode,
|
||||
sort,
|
||||
sort_reverse,
|
||||
} = this.props;
|
||||
|
||||
if (!playlist) {
|
||||
@ -293,6 +341,42 @@ class Playlist extends React.Component {
|
||||
if (playlist.can_edit) {
|
||||
context = 'editable-playlist';
|
||||
}
|
||||
let {
|
||||
playlist: {
|
||||
tracks,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (sort && tracks) {
|
||||
tracks = sortItems(tracks, sort, sort_reverse);
|
||||
}
|
||||
|
||||
const sort_options = [
|
||||
{
|
||||
value: null,
|
||||
label: i18n('playlist.tracks.sort.default'),
|
||||
},
|
||||
{
|
||||
value: 'sort_id',
|
||||
label: i18n('playlist.tracks.sort.sort_id'),
|
||||
},
|
||||
{
|
||||
value: 'added_at',
|
||||
label: i18n('playlist.tracks.sort.added_at'),
|
||||
},
|
||||
{
|
||||
value: 'name',
|
||||
label: i18n('playlist.tracks.sort.name'),
|
||||
},
|
||||
{
|
||||
value: 'artists.first.name',
|
||||
label: i18n('playlist.tracks.sort.artist'),
|
||||
},
|
||||
{
|
||||
value: 'album.name',
|
||||
label: i18n('playlist.tracks.sort.album'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="view playlist-view content-wrapper preserve-3d">
|
||||
@ -347,12 +431,27 @@ class Playlist extends React.Component {
|
||||
|
||||
{this.renderActions()}
|
||||
|
||||
<section className="list-wrapper">
|
||||
<h4 className="no-bottom-margin">
|
||||
<I18n path="playlist.tracks.title" />
|
||||
<div className="actions-wrapper">
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name="Sort"
|
||||
value={sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
selected_icon={sort ? (sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
||||
handleChange={this.onChangeSort}
|
||||
/>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<section className="list-wrapper no-top-padding">
|
||||
<TrackList
|
||||
uri={playlist.uri}
|
||||
className="playlist-track-list"
|
||||
track_context={context}
|
||||
tracks={playlist.tracks}
|
||||
tracks={tracks}
|
||||
removeTracks={this.removeTracks}
|
||||
reorderTracks={this.reorderTracks}
|
||||
/>
|
||||
@ -397,6 +496,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
local_library_playlists,
|
||||
spotify_authorized,
|
||||
spotify_userid: (me && me.id) || null,
|
||||
sort: (state.ui.playlist_tracks_sort ? state.ui.playlist_tracks_sort : null),
|
||||
sort_reverse: (!!state.ui.playlist_tracks_sort_reverse),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ class Settings extends React.Component {
|
||||
}
|
||||
|
||||
resetAllSettings = () => {
|
||||
localForage.clear(() => {
|
||||
localForage.clear().then(() => {
|
||||
console.debug('Cleared settings, reloading...');
|
||||
window.location = '#';
|
||||
window.location.reload(true);
|
||||
|
||||
@ -472,7 +472,7 @@ class Discover extends React.Component {
|
||||
<section className="col col--w70 tracks">
|
||||
<h4>
|
||||
<I18n path="discover.recommendations.tracks" />
|
||||
<div className="pull-right">
|
||||
<div className="actions-wrapper">
|
||||
<ContextMenuTrigger onTrigger={this.handleContextMenu} />
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@ -279,10 +279,15 @@ h3 {
|
||||
|
||||
h4 {
|
||||
@include feature_font();
|
||||
display: flex;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 700;
|
||||
|
||||
&.no-bottom-margin {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.underline {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 2px solid rgba(128, 128, 128, 0.35);
|
||||
@ -292,8 +297,14 @@ h4 {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.dropdown-field {
|
||||
margin-left: 10px;
|
||||
.actions-wrapper {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dropdown-field {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
Reference in New Issue
Block a user