Inheriting added_at with last_updated for better cross-platform support;, fixes #523; Adding 'Shuffle' to queue, fixes #522

This commit is contained in:
James Barnsley
2020-04-19 20:57:43 +12:00
parent 67855d207d
commit 233ce9b207
24 changed files with 267 additions and 115 deletions

View File

@ -90,20 +90,25 @@ export default memo((props) => {
case 'ago':
var date = new Date(props.data);
var diff = new Date() - date;
var seconds = Math.floor(diff / 1000);
var minutes = Math.floor(diff / (1000 * 60));
var hours = Math.floor(diff / (1000 * 60 * 60));
var days = Math.floor(diff / (1000 * 60 * 60 * 24));
var weeks = Math.floor(diff / (1000 * 60 * 60 * 24 * 7));
var years = Math.floor(diff / (1000 * 60 * 60 * 24 * 7 * 52));
if (seconds < 60) {
return `${seconds} seconds`;
return `${seconds} second${seconds > 1 ? 's' : ''}`;
} if (minutes < 60) {
return `${minutes} minutes`;
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
} if (hours < 24) {
return `${hours} hours`;
return `${hours} hour${hours > 1 ? 's' : ''}`;
} if (days < 7) {
return `${days} day${days > 1 ? 's' : ''}`;
} if (weeks < 54) {
return `${weeks} week${weeks > 1 ? 's' : ''}`;
}
return `${days} days`;
return `${years} year${years > 1 ? 's' : ''}`;
default:
return null;
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import ReactGA from 'react-ga';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -29,6 +30,7 @@ class Hotkeys extends React.Component {
history,
modal,
dragging,
allow_reporting,
} = this.props;
let { volume } = this.props;
const key = e.key.toLowerCase();
@ -55,9 +57,11 @@ class Hotkeys extends React.Component {
if (play_state == 'playing') {
mopidyActions.pause();
uiActions.createNotification({ content: 'pause', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Pause' });
} else {
mopidyActions.play();
uiActions.createNotification({ content: 'play_arrow', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Play' });
}
prevent = true;
break;
@ -66,35 +70,42 @@ class Hotkeys extends React.Component {
if (dragging) {
uiActions.dragEnd();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
} else if (modal) {
window.history.back();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
}
break;
case 's':
history.push('/search');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
break;
case 'q':
history.push('/queue');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
break;
case 'k':
history.push('/kiosk-mode');
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
break;
case ',':
window.history.back();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Back' });
break;
case '.':
window.history.forward();
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Forward' });
break;
case '=':
@ -110,6 +121,7 @@ class Hotkeys extends React.Component {
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
}
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume up' });
break;
case '-':
@ -125,15 +137,18 @@ class Hotkeys extends React.Component {
}
uiActions.createNotification({ content: 'volume_down', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume down' });
break;
case '0':
if (mute) {
mopidyActions.setMute(false);
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Unmute' });
} else {
mopidyActions.setMute(true);
uiActions.createNotification({ content: 'volume_off', type: 'shortcut' });
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Mute' });
}
prevent = true;
break;
@ -146,24 +161,28 @@ class Hotkeys extends React.Component {
mopidyActions.setTimePosition(new_position);
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
break;
case "'":
mopidyActions.setTimePosition(play_time_position + 30000);
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
break;
case '[':
mopidyActions.previous();
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
break;
case ']':
mopidyActions.next();
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
prevent = true;
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
break;
default:
@ -186,6 +205,7 @@ const mapStateToProps = (state, ownProps) => ({
play_state: state.mopidy.play_state,
play_time_position: parseInt(state.mopidy.time_position),
dragging: state.ui.dragger && state.ui.dragger.dragging,
allow_reporting: state.ui.allow_reporting,
});
const mapDispatchToProps = (dispatch) => ({

View File

@ -84,7 +84,7 @@ export default class ListItem extends React.Component {
<span>
{value}
{' '}
tracks
tracks
</span>
);
}
@ -93,17 +93,29 @@ tracks
<span>
{value.toLocaleString()}
{' '}
followers
followers
</span>
);
}
if (key_string === 'added_at') {
return (
<span>
Added
Added
{' '}
<Dater type="ago" data={value} />
{' '}
ago
ago
</span>
);
}
if (key_string === 'last_modified') {
return (
<span>
Updated
{' '}
<Dater type="ago" data={value} />
{' '}
ago
</span>
);
}

View File

@ -82,7 +82,7 @@ class Notifications extends React.Component {
default:
return (
<div className={`notification notification--${notification.type}${notification.closing ? ' closing' : ''}`} key={notification.key} data-key={notification.key} data-duration={notification.duration}>
<div className={`notification notification--${notification.level}${notification.closing ? ' closing' : ''}`} key={notification.key} data-key={notification.key} data-duration={notification.duration}>
<Icon name="close" className="notification__close-button" onClick={(e) => this.props.uiActions.removeNotification(notification.key, true)} />
{notification.title ? <h4 className="notification__title">{notification.title}</h4> : null}
{notification.content ? <div className="notification__content">{notification.content}</div> : null}

2
src/js/components/ResizeListener.js Normal file → Executable file
View File

@ -15,4 +15,4 @@ export default (props) => {
};
return null;
};
};

View File

@ -15,7 +15,7 @@ const SnapcastClients = ({ actions, group, groups, show_disconnected_clients })
}
if (!clients || clients.length <= 0) {
return <p className="no-results">No clients</p>;
return <p className="no-results">No connected clients</p>;
}
return (

View File

@ -4,6 +4,7 @@ import Link from './Link';
import Icon from './Icon';
export default memo((props) => {
const { placeholder = true } = props;
const mapImageSizes = () => {
// Single image
if (props.image) {
@ -59,9 +60,7 @@ export default memo((props) => {
return (
<div className={class_name}>
{!image && (
<Icon className="thumbnail__placeholder" name={iconName()} />
)}
{placeholder && <Icon className="thumbnail__placeholder" name={iconName()} />}
{props.useImageTag && image ? (
<img
alt="Artwork thumbnail"

View File

@ -353,6 +353,12 @@ export function clearTracklist() {
};
}
export function shuffleTracklist() {
return {
type: 'MOPIDY_SHUFFLE_TRACKLIST',
};
}
/**
* Asset-oriented actions

View File

@ -948,7 +948,7 @@ const MopidyMiddleware = (function () {
// add our first track
request(socket, store, 'tracklist.move', { start: action.range_start, end: action.range_start + action.range_length, to_position: action.insert_before })
.then(
(response) => {
() => {
// TODO: when complete, send event to confirm success/failure
},
(error) => {
@ -963,14 +963,30 @@ const MopidyMiddleware = (function () {
case 'MOPIDY_CLEAR_TRACKLIST':
request(socket, store, 'tracklist.clear')
.then(
(response) => {
() => {
store.dispatch(coreActions.clearCurrentTrack());
store.dispatch(pusherActions.deliverBroadcast(
'notification',
{
notification: {
content: `${store.getState().pusher.username} cleared queue`,
content: `${store.getState().pusher.username} cleared the playback queue`,
},
},
));
},
);
break;
case 'MOPIDY_SHUFFLE_TRACKLIST':
request(socket, store, 'tracklist.shuffle', { start: 1 })
.then(
() => {
store.dispatch(pusherActions.deliverBroadcast(
'notification',
{
notification: {
content: `${store.getState().pusher.username} shuffled the playback queue`,
},
},
));
@ -979,7 +995,7 @@ const MopidyMiddleware = (function () {
break;
/**
/**
* =============================================================== SEARCHING ============
* ======================================================================================
* */
@ -1622,7 +1638,6 @@ const MopidyMiddleware = (function () {
request(socket, store, 'playlists.lookup', action.data)
.then((response) => {
const playlist = {
...response,
uri: response.uri,
type: 'playlist',
@ -1929,6 +1944,7 @@ const MopidyMiddleware = (function () {
artists_uris,
tracks_uris,
tracks_total: tracks_uris.length,
last_modified: tracks[0].last_modified,
...tracks[0].album,
};

View File

@ -273,6 +273,7 @@ const formatAlbum = function (data) {
'provider',
'name',
'type',
'last_modified',
'added_at',
'release_date',
'listeners',
@ -285,7 +286,7 @@ const formatAlbum = function (data) {
'tracks_uris',
'tracks_total',
'tracks_more',
'artists', // Array of simple records
'artists', // Array of simple records
];
// Loop fields and import from data
@ -299,6 +300,11 @@ const formatAlbum = function (data) {
album.images = formatImages(album.images);
}
if (data.last_modified && album.added_at === undefined) {
album.added_at = data.last_modified;
} else if (data.added_at && album.last_modified === undefined) {
album.last_modified = data.added_at;
}
if (data.date && !album.date) {
album.release_date = data.date;
}
@ -398,8 +404,7 @@ const formatPlaylist = function (data) {
'images',
'popularity',
'followers',
'added_at',
'last_modified_date',
'last_modified',
'can_edit',
'owner',
'user_uri',
@ -419,6 +424,10 @@ const formatPlaylist = function (data) {
playlist.images = formatImages(playlist.images);
}
if (data.last_modified_date && playlist.last_modified === undefined) {
playlist.last_modified = data.last_modified_date;
}
if (data.followers && data.followers.total !== undefined) {
playlist.followers = data.followers.total;
}
@ -427,6 +436,12 @@ const formatPlaylist = function (data) {
playlist.tracks_total = data.tracks.total;
}
if (playlist.last_modified && playlist.added_at === undefined) {
playlist.added_at = data.last_modified;
} else if (playlist.added_at && playlist.last_modified === undefined) {
playlist.last_modified = data.added_at;
}
if (data.owner) {
playlist.owner = {
id: data.owner.id,
@ -523,13 +538,15 @@ const formatTrack = function (data) {
'followers',
'popularity',
'userloved',
'last_modified',
'added_at',
'is_explicit',
'is_local',
'lyrics',
'lyrics_path',
'lyrics_results',
'artists', // Array of simple records
'album', // Array of simple records
'artists', // Array of simple records
'album', // Array of simple records
];
// Nested track object (eg in spotify playlist)
@ -560,6 +577,12 @@ const formatTrack = function (data) {
track.followers = data.followers.total;
}
if (track.last_modified && track.added_at === undefined) {
track.added_at = track.last_modified;
} else if (track.added_at && track.last_modified === undefined) {
track.last_modified = track.added_at;
}
if (track.duration === undefined && data.duration_ms !== undefined) {
track.duration = data.duration_ms;
} else if (track.duration === undefined && data.length !== undefined) {

View File

@ -275,11 +275,19 @@ class Queue extends React.Component {
<ul className="details">
<li>{`${queue_tracks.length} tracks`}</li>
<li><Dater type="total-time" data={queue_tracks} /></li>
{queue_tracks.length > 0 && (
<li>
<a onClick={this.props.mopidyActions.shuffleTracklist}>
<Icon name="shuffle" />
Shuffle
</a>
</li>
)}
{queue_tracks.length > 0 && (
<li>
<a onClick={this.props.mopidyActions.clearTracklist}>
<Icon name="delete_sweep" />
Clear queue
Clear
</a>
</li>
)}

View File

@ -216,7 +216,7 @@ class LibraryAlbums extends React.Component {
handleContextMenu={(e, item) => this.handleContextMenu(e, item)}
rows={albums}
thumbnail
details={['artists', 'tracks_uris.length']}
details={['artists', 'tracks_uris.length', 'last_modified']}
right_column={['added_at']}
className="albums"
link_prefix="/album/"
@ -295,8 +295,8 @@ class LibraryAlbums extends React.Component {
label: 'Artist',
},
{
value: 'added_at',
label: 'Added',
value: 'last_modified',
label: 'Updated',
},
{
value: 'tracks_uris.length',

View File

@ -150,7 +150,7 @@ class LibraryPlaylists extends React.Component {
handleContextMenu={(e, item) => this.handleContextMenu(e, item)}
rows={playlists}
thumbnail
details={['owner', 'tracks_total']}
details={['owner', 'tracks_total', 'last_modified']}
right_column={['source']}
className="playlists"
link_prefix="/playlist/"
@ -217,6 +217,10 @@ class LibraryPlaylists extends React.Component {
value: 'name',
label: 'Name',
},
{
value: 'last_modified',
label: 'Updated',
},
{
value: 'can_edit',
label: 'Editable',

View File

@ -179,7 +179,7 @@ class KioskMode extends React.Component {
className="modal--kiosk-mode"
extraControls={extraControls}
>
<Thumbnail className="background" images={images} />
<Thumbnail className="background" images={images} placeholder={false} />
<div className={`player player--${show_lyrics ? 'with' : 'without'}-lyrics`}>