Inheriting added_at with last_updated for better cross-platform support;, fixes #523; Adding 'Shuffle' to queue, fixes #522
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
2
src/js/components/ResizeListener.js
Normal file → Executable file
@ -15,4 +15,4 @@ export default (props) => {
|
||||
};
|
||||
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -353,6 +353,12 @@ export function clearTracklist() {
|
||||
};
|
||||
}
|
||||
|
||||
export function shuffleTracklist() {
|
||||
return {
|
||||
type: 'MOPIDY_SHUFFLE_TRACKLIST',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asset-oriented actions
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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`}>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user