2021-08-14 12:38:14 +12:00
|
|
|
import React, { useEffect, useState } from 'react';
|
2019-01-14 16:50:21 +13:00
|
|
|
import { connect } from 'react-redux';
|
|
|
|
|
import { bindActionCreators } from 'redux';
|
2019-01-22 21:46:56 +13:00
|
|
|
import ErrorMessage from '../components/ErrorMessage';
|
2020-08-15 21:05:00 +12:00
|
|
|
import Button from '../components/Button';
|
2019-09-05 15:36:11 +12:00
|
|
|
import TrackList from '../components/TrackList';
|
|
|
|
|
import Thumbnail from '../components/Thumbnail';
|
2020-08-08 22:17:31 +12:00
|
|
|
import PinButton from '../components/Fields/PinButton';
|
|
|
|
|
import { nice_number } from '../components/NiceNumber';
|
2020-07-08 20:15:14 +12:00
|
|
|
import { Dater, dater } from '../components/Dater';
|
2019-09-05 15:36:11 +12:00
|
|
|
import FollowButton from '../components/Fields/FollowButton';
|
2019-09-10 06:59:44 +12:00
|
|
|
import Loader from '../components/Loader';
|
2019-09-05 15:36:11 +12:00
|
|
|
import ContextMenuTrigger from '../components/ContextMenuTrigger';
|
|
|
|
|
import URILink from '../components/URILink';
|
2021-06-20 08:58:44 +12:00
|
|
|
import { SourceIcon } from '../components/Icon';
|
2020-11-07 16:46:09 +13:00
|
|
|
import DropdownField from '../components/Fields/DropdownField';
|
2020-11-07 22:03:58 +13:00
|
|
|
import FilterField from '../components/Fields/FilterField';
|
2019-09-05 15:36:11 +12:00
|
|
|
import * as coreActions from '../services/core/actions';
|
|
|
|
|
import * as uiActions from '../services/ui/actions';
|
|
|
|
|
import * as mopidyActions from '../services/mopidy/actions';
|
|
|
|
|
import * as spotifyActions from '../services/spotify/actions';
|
2021-06-20 08:58:44 +12:00
|
|
|
import { uriSource } from '../util/helpers';
|
2020-07-08 20:15:14 +12:00
|
|
|
import { i18n, I18n } from '../locale';
|
2021-06-20 08:58:44 +12:00
|
|
|
import { makeItemSelector, makeLoadingSelector, getSortSelector } from '../util/selectors';
|
2020-11-07 22:03:58 +13:00
|
|
|
import { sortItems, applyFilter } from '../util/arrays';
|
2021-01-23 16:24:07 +13:00
|
|
|
import { decodeUri, encodeUri } from '../util/format';
|
2019-09-05 15:36:11 +12:00
|
|
|
|
2021-08-08 14:21:28 +12:00
|
|
|
const SORT_KEY = 'playlist_tracks';
|
|
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
const Actions = ({
|
|
|
|
|
encodedUri,
|
|
|
|
|
playlist: {
|
|
|
|
|
uri,
|
|
|
|
|
can_edit,
|
|
|
|
|
name,
|
|
|
|
|
in_library,
|
|
|
|
|
},
|
|
|
|
|
onPlay,
|
|
|
|
|
handleContextMenu,
|
|
|
|
|
}) => {
|
|
|
|
|
switch (uriSource(uri)) {
|
|
|
|
|
case 'm3u':
|
|
|
|
|
return (
|
|
|
|
|
<div className="actions">
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
onClick={onPlay}
|
|
|
|
|
tracking={{ category: 'Playlist', action: 'Play' }}
|
|
|
|
|
>
|
|
|
|
|
<I18n path="actions.play" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
to={`/playlist/${encodedUri}/edit`}
|
|
|
|
|
tracking={{ category: 'Playlist', action: 'Edit' }}
|
|
|
|
|
>
|
|
|
|
|
<I18n path="actions.edit" />
|
|
|
|
|
</Button>
|
|
|
|
|
<PinButton item={{ uri, name }} />
|
|
|
|
|
<ContextMenuTrigger onTrigger={handleContextMenu} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2020-11-07 22:03:58 +13:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
case 'spotify':
|
|
|
|
|
if (can_edit) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="actions">
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
onClick={onPlay}
|
|
|
|
|
tracking={{ category: 'Playlist', action: 'Play' }}
|
|
|
|
|
>
|
|
|
|
|
<I18n path="actions.play" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
to={`/playlist/${encodedUri}/edit`}
|
|
|
|
|
tracking={{ category: 'Playlist', action: 'Edit' }}
|
|
|
|
|
>
|
|
|
|
|
<I18n path="actions.edit" />
|
|
|
|
|
</Button>
|
|
|
|
|
<PinButton item={{ uri, name }} />
|
|
|
|
|
<ContextMenuTrigger onTrigger={handleContextMenu} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div className="actions">
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
onClick={onPlay}
|
|
|
|
|
tracking={{ category: 'Playlist', action: 'Play' }}
|
|
|
|
|
>
|
|
|
|
|
<I18n path="actions.play" />
|
|
|
|
|
</Button>
|
|
|
|
|
<FollowButton
|
|
|
|
|
uri={uri}
|
|
|
|
|
is_following={in_library}
|
|
|
|
|
/>
|
|
|
|
|
<PinButton item={{ uri, name }} />
|
|
|
|
|
<ContextMenuTrigger onTrigger={handleContextMenu} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2019-09-05 15:36:11 +12:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
default:
|
|
|
|
|
return (
|
|
|
|
|
<div className="actions">
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
onClick={onPlay}
|
|
|
|
|
tracking={{ category: 'Playlist', action: 'Play' }}
|
|
|
|
|
>
|
|
|
|
|
<I18n path="actions.play" />
|
|
|
|
|
</Button>
|
|
|
|
|
<PinButton item={{ uri, name }} />
|
|
|
|
|
<ContextMenuTrigger onTrigger={handleContextMenu} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
2021-08-14 12:38:14 +12:00
|
|
|
}
|
2019-09-05 15:36:11 +12:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
const Playlist = ({
|
|
|
|
|
loading,
|
|
|
|
|
loading_tracks,
|
|
|
|
|
slim_mode,
|
|
|
|
|
history,
|
|
|
|
|
uri,
|
|
|
|
|
encodedUri,
|
|
|
|
|
name,
|
|
|
|
|
playlist: playlistProp,
|
|
|
|
|
sortField,
|
|
|
|
|
sortReverse,
|
|
|
|
|
coreActions: {
|
|
|
|
|
loadPlaylist,
|
|
|
|
|
reorderPlaylistTracks,
|
|
|
|
|
removeTracksFromPlaylist,
|
|
|
|
|
},
|
|
|
|
|
uiActions: {
|
|
|
|
|
setSort,
|
|
|
|
|
setWindowTitle,
|
|
|
|
|
showContextMenu,
|
|
|
|
|
hideContextMenu,
|
|
|
|
|
createNotification,
|
|
|
|
|
},
|
|
|
|
|
mopidyActions: {
|
|
|
|
|
playPlaylist,
|
|
|
|
|
},
|
|
|
|
|
}) => {
|
|
|
|
|
const [playlist, setPlaylist] = useState({});
|
|
|
|
|
const [filter, setFilter] = useState('');
|
|
|
|
|
|
|
|
|
|
useEffect(
|
|
|
|
|
() => {
|
|
|
|
|
if (uri) loadPlaylist(uri, { full: true, name });
|
|
|
|
|
},
|
|
|
|
|
[uri],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (playlistProp && playlist && playlistProp.moved_to !== playlist.moved_to) {
|
|
|
|
|
history.push(`/playlist/${encodeUri(playlistProp.moved_to)}`);
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
2021-08-14 12:38:14 +12:00
|
|
|
}, [playlistProp])
|
2019-09-05 15:36:11 +12:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (playlistProp) {
|
|
|
|
|
setWindowTitle(i18n('playlist.title_window', { name: playlistProp.name }));
|
|
|
|
|
} else {
|
|
|
|
|
setWindowTitle(i18n('playlist.title'));
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
2021-08-14 12:38:14 +12:00
|
|
|
setPlaylist(playlistProp);
|
|
|
|
|
}, [playlistProp]);
|
2019-09-05 15:36:11 +12:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
const handleContextMenu = (e) => showContextMenu({
|
|
|
|
|
e,
|
|
|
|
|
context: 'playlist',
|
|
|
|
|
items: [{ name, ...playlist }],
|
|
|
|
|
uris: [uri],
|
|
|
|
|
});
|
2020-11-07 16:46:09 +13:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
const onChangeSort = (field) => {
|
2020-11-07 16:46:09 +13:00
|
|
|
let reverse = false;
|
2021-08-08 14:21:28 +12:00
|
|
|
if (field !== null && sortField === field) {
|
2021-06-20 08:58:44 +12:00
|
|
|
reverse = !sortReverse;
|
2020-11-07 16:46:09 +13:00
|
|
|
}
|
|
|
|
|
|
2021-08-08 14:21:28 +12:00
|
|
|
setSort(SORT_KEY, field, reverse);
|
2020-11-07 16:46:09 +13:00
|
|
|
hideContextMenu();
|
2021-08-14 12:38:14 +12:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onPlay = () => playPlaylist(uri);
|
2020-11-07 16:46:09 +13:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
const reorderTracks = (indexes, index) => {
|
|
|
|
|
const { snapshot_id, tracks } = playlist;
|
2020-07-08 20:15:14 +12:00
|
|
|
|
2021-08-08 14:21:28 +12:00
|
|
|
if (sortField !== 'sort_id' || filter !== '') {
|
2020-11-07 16:46:09 +13:00
|
|
|
createNotification({
|
|
|
|
|
content: i18n('errors.cannot_reorder.title'),
|
|
|
|
|
description: i18n('errors.cannot_reorder.description'),
|
|
|
|
|
level: 'error',
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-08 14:21:28 +12:00
|
|
|
if (sortReverse) {
|
2020-11-07 16:46:09 +13:00
|
|
|
const count = tracks.length - 1;
|
|
|
|
|
index = count - index + 1;
|
|
|
|
|
indexes = indexes.map((index) => count - index);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-08 20:15:14 +12:00
|
|
|
reorderPlaylistTracks(uri, indexes, index, snapshot_id);
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
|
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
const removeTracks = (tracks_indexes) => {
|
2020-09-04 09:21:42 +12:00
|
|
|
removeTracksFromPlaylist(uri, tracks_indexes);
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
|
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
if (loading) {
|
|
|
|
|
return <Loader body loading />;
|
2019-09-05 15:36:11 +12:00
|
|
|
}
|
2021-08-14 12:38:14 +12:00
|
|
|
if (!playlist) {
|
|
|
|
|
return (
|
|
|
|
|
<ErrorMessage type="not-found" title="Not found">
|
|
|
|
|
<p>
|
|
|
|
|
<I18n path="errors.uri_not_found" uri={uri} />
|
|
|
|
|
</p>
|
|
|
|
|
</ErrorMessage>
|
|
|
|
|
);
|
2020-08-08 22:17:31 +12:00
|
|
|
}
|
|
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
let context = 'playlist';
|
|
|
|
|
if (playlist.can_edit) context = 'editable-playlist';
|
|
|
|
|
|
|
|
|
|
let tracks = playlist?.tracks || [];
|
|
|
|
|
if (sortField && tracks) tracks = sortItems(tracks, sortField, sortReverse);
|
|
|
|
|
if (filter && filter !== '') tracks = applyFilter('name', filter, tracks);
|
|
|
|
|
|
|
|
|
|
const sort_options = [
|
|
|
|
|
{
|
|
|
|
|
value: 'sort_id',
|
|
|
|
|
label: i18n('playlist.tracks.sort.sort_id'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'name',
|
|
|
|
|
label: i18n('playlist.tracks.sort.name'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'artist',
|
|
|
|
|
label: i18n('playlist.tracks.sort.artist'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'album',
|
|
|
|
|
label: i18n('playlist.tracks.sort.album'),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="view playlist-view content-wrapper preserve-3d">
|
|
|
|
|
|
|
|
|
|
<div className="thumbnail-wrapper">
|
|
|
|
|
<Thumbnail size="large" glow canZoom images={playlist.images} type="playlist" />
|
|
|
|
|
</div>
|
2020-07-08 20:15:14 +12:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
<div className="title">
|
|
|
|
|
<h1>{playlist.name}</h1>
|
|
|
|
|
{playlist.description && (
|
|
|
|
|
<h2
|
|
|
|
|
className="description"
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: playlist.description }}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2019-09-05 15:36:11 +12:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
<ul className="details details--one-line">
|
|
|
|
|
{!slim_mode && (
|
|
|
|
|
<li className="source">
|
|
|
|
|
<SourceIcon uri={playlist.uri} />
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
{playlist.user && (
|
|
|
|
|
<li>
|
|
|
|
|
<URILink
|
|
|
|
|
type="user"
|
|
|
|
|
uri={playlist.user.uri}
|
2020-08-16 20:00:10 +12:00
|
|
|
>
|
2021-08-14 12:38:14 +12:00
|
|
|
{playlist.user.name}
|
|
|
|
|
</URILink>
|
|
|
|
|
</li>
|
2020-07-08 20:15:14 +12:00
|
|
|
)}
|
2021-08-14 12:38:14 +12:00
|
|
|
<li>
|
|
|
|
|
<I18n path="specs.tracks" count={playlist.tracks ? playlist.tracks.length : 0} />
|
|
|
|
|
</li>
|
|
|
|
|
{!slim_mode && playlist.tracks && playlist.tracks_total > 0 && (
|
|
|
|
|
<li><Dater type="total-time" data={playlist.tracks} /></li>
|
|
|
|
|
)}
|
|
|
|
|
{!slim_mode && playlist.followers !== undefined && (
|
2019-09-10 06:59:44 +12:00
|
|
|
<li>
|
2021-08-14 12:38:14 +12:00
|
|
|
<I18n path="specs.followers" count={nice_number(playlist.followers)} />
|
2019-09-05 15:36:11 +12:00
|
|
|
</li>
|
2021-08-14 12:38:14 +12:00
|
|
|
)}
|
|
|
|
|
{!slim_mode && playlist.last_modified_date && (
|
|
|
|
|
<li>
|
|
|
|
|
<I18n path="specs.edited" date={dater('ago', playlist.last_modified_date)} />
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
2020-11-07 16:46:09 +13:00
|
|
|
|
2021-08-14 12:38:14 +12:00
|
|
|
<Actions
|
|
|
|
|
encodedUri={encodedUri}
|
|
|
|
|
playlist={playlist}
|
|
|
|
|
onPlay={onPlay}
|
|
|
|
|
handleContextMenu={handleContextMenu}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<h4 className="no-bottom-margin">
|
|
|
|
|
<I18n path="playlist.tracks.title" />
|
|
|
|
|
{loading_tracks && <Loader loading mini />}
|
|
|
|
|
<div className="actions-wrapper">
|
|
|
|
|
<FilterField
|
|
|
|
|
initialValue={filter}
|
|
|
|
|
handleChange={setFilter}
|
|
|
|
|
onSubmit={hideContextMenu}
|
2020-09-20 21:04:40 +12:00
|
|
|
/>
|
2021-08-14 12:38:14 +12:00
|
|
|
<DropdownField
|
|
|
|
|
icon="swap_vert"
|
|
|
|
|
name="Sort"
|
|
|
|
|
value={sortField}
|
|
|
|
|
valueAsLabel
|
|
|
|
|
options={sort_options}
|
|
|
|
|
selected_icon={sortField ? (sortReverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
|
|
|
|
handleChange={onChangeSort}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</h4>
|
|
|
|
|
|
|
|
|
|
<section className="list-wrapper no-top-padding">
|
|
|
|
|
<TrackList
|
|
|
|
|
uri={playlist.uri}
|
|
|
|
|
className="playlist-track-list"
|
|
|
|
|
track_context={context}
|
|
|
|
|
tracks={tracks}
|
|
|
|
|
removeTracks={removeTracks}
|
|
|
|
|
reorderTracks={reorderTracks}
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2016-10-20 21:38:01 +13:00
|
|
|
}
|
|
|
|
|
|
2019-09-10 06:59:44 +12:00
|
|
|
const mapStateToProps = (state, ownProps) => {
|
2020-05-03 13:54:34 +12:00
|
|
|
const {
|
|
|
|
|
ui: {
|
|
|
|
|
allow_reporting,
|
|
|
|
|
slim_mode,
|
|
|
|
|
theme,
|
|
|
|
|
} = {},
|
|
|
|
|
spotify: {
|
|
|
|
|
library_playlists: spotify_library_playlists,
|
|
|
|
|
authorization: spotify_authorized,
|
2020-05-12 20:32:48 +12:00
|
|
|
me = {},
|
2020-05-03 13:54:34 +12:00
|
|
|
} = {},
|
|
|
|
|
mopidy: {
|
|
|
|
|
library_playlists: local_library_playlists,
|
|
|
|
|
} = {},
|
|
|
|
|
} = state;
|
|
|
|
|
|
2021-01-23 16:24:07 +13:00
|
|
|
const uri = decodeUri(ownProps.match.params.uri);
|
2020-09-23 20:59:06 +12:00
|
|
|
const itemSelector = makeItemSelector(uri);
|
2021-04-12 21:29:43 +12:00
|
|
|
const loadingSelector = makeLoadingSelector([`(.*)${uri}(.*)`, '^((?!contains).)*$', '^((?!tracks).)*$', '^((?!followers).)*$']);
|
2021-01-31 09:27:40 +13:00
|
|
|
const loadingTracksSelector = makeLoadingSelector([`(.*)${uri}(.*)tracks(.*)`]);
|
2021-08-08 14:21:28 +12:00
|
|
|
const [sortField, sortReverse] = getSortSelector(state, SORT_KEY);
|
2019-09-05 15:36:11 +12:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
uri,
|
2021-01-31 09:27:40 +13:00
|
|
|
encodedUri: ownProps.match.params.uri,
|
2021-05-29 12:04:41 +12:00
|
|
|
name: ownProps.match.params.name,
|
2020-05-03 13:54:34 +12:00
|
|
|
allow_reporting,
|
|
|
|
|
slim_mode,
|
|
|
|
|
theme,
|
2020-09-23 20:59:06 +12:00
|
|
|
loading: loadingSelector(state),
|
2020-11-20 22:20:30 +13:00
|
|
|
loading_tracks: loadingTracksSelector(state),
|
2020-09-23 20:59:06 +12:00
|
|
|
playlist: itemSelector(state),
|
2020-05-03 13:54:34 +12:00
|
|
|
spotify_library_playlists,
|
|
|
|
|
local_library_playlists,
|
|
|
|
|
spotify_authorized,
|
2020-05-12 20:32:48 +12:00
|
|
|
spotify_userid: (me && me.id) || null,
|
2021-06-20 08:58:44 +12:00
|
|
|
sortField,
|
|
|
|
|
sortReverse,
|
2019-09-05 15:36:11 +12:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
|
|
|
coreActions: bindActionCreators(coreActions, dispatch),
|
|
|
|
|
uiActions: bindActionCreators(uiActions, dispatch),
|
|
|
|
|
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
|
|
|
|
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
|
|
|
|
});
|
|
|
|
|
|
2019-09-10 06:59:44 +12:00
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(Playlist);
|