490 lines
16 KiB
JavaScript
Executable File
490 lines
16 KiB
JavaScript
Executable File
import React from 'react';
|
|
import { bindActionCreators } from 'redux';
|
|
import { Route, Switch } from 'react-router-dom';
|
|
import { connect } from 'react-redux';
|
|
import ReactGA from 'react-ga';
|
|
import * as Sentry from '@sentry/browser';
|
|
|
|
import Sidebar from './components/Sidebar';
|
|
import PlaybackControls from './components/PlaybackControls';
|
|
import ContextMenu from './components/ContextMenu';
|
|
import Dragger from './components/Dragger';
|
|
import Notifications from './components/Notifications';
|
|
import ResizeListener from './components/ResizeListener';
|
|
import Hotkeys from './components/Hotkeys';
|
|
import DebugInfo from './components/DebugInfo';
|
|
import ErrorMessage from './components/ErrorMessage';
|
|
import Stream from './components/Stream';
|
|
|
|
import Album from './views/Album';
|
|
import Artist from './views/Artist';
|
|
import Playlist from './views/Playlist';
|
|
import User from './views/User';
|
|
import Track from './views/Track';
|
|
import Queue from './views/Queue';
|
|
import QueueHistory from './views/QueueHistory';
|
|
import Debug from './views/Debug';
|
|
import Search from './views/Search';
|
|
import Settings from './views/Settings';
|
|
|
|
import DiscoverRecommendations from './views/discover/DiscoverRecommendations';
|
|
import DiscoverFeatured from './views/discover/DiscoverFeatured';
|
|
import DiscoverCategories from './views/discover/DiscoverCategories';
|
|
import DiscoverCategory from './views/discover/DiscoverCategory';
|
|
import DiscoverNewReleases from './views/discover/DiscoverNewReleases';
|
|
|
|
import LibraryArtists from './views/library/LibraryArtists';
|
|
import LibraryAlbums from './views/library/LibraryAlbums';
|
|
import LibraryTracks from './views/library/LibraryTracks';
|
|
import LibraryPlaylists from './views/library/LibraryPlaylists';
|
|
import LibraryBrowse from './views/library/LibraryBrowse';
|
|
import LibraryBrowseDirectory from './views/library/LibraryBrowseDirectory';
|
|
|
|
import EditPlaylist from './views/modals/EditPlaylist';
|
|
import CreatePlaylist from './views/modals/CreatePlaylist';
|
|
import EditRadio from './views/modals/EditRadio';
|
|
import AddToQueue from './views/modals/AddToQueue';
|
|
import InitialSetup from './views/modals/InitialSetup';
|
|
import KioskMode from './views/modals/KioskMode';
|
|
import ShareConfiguration from './views/modals/ShareConfiguration';
|
|
import AddToPlaylist from './views/modals/AddToPlaylist';
|
|
import ImageZoom from './views/modals/ImageZoom';
|
|
import EditCommand from './views/modals/EditCommand';
|
|
import Reset from './views/modals/Reset';
|
|
|
|
import { scrollTo, isTouchDevice } from './util/helpers';
|
|
import storage from './util/storage';
|
|
import * as coreActions from './services/core/actions';
|
|
import * as uiActions from './services/ui/actions';
|
|
import * as pusherActions from './services/pusher/actions';
|
|
import * as mopidyActions from './services/mopidy/actions';
|
|
import * as spotifyActions from './services/spotify/actions';
|
|
import * as lastfmActions from './services/lastfm/actions';
|
|
import * as geniusActions from './services/genius/actions';
|
|
import * as snapcastActions from './services/snapcast/actions';
|
|
import MediaSession from './components/MediaSession';
|
|
|
|
export class App extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
const { allow_reporting } = this.props;
|
|
this.state = {
|
|
userHasInteracted: false,
|
|
};
|
|
|
|
if (allow_reporting) {
|
|
ReactGA.initialize('UA-64701652-3');
|
|
Sentry.init({
|
|
dsn: 'https://ca99fb6662fe40ae8ec4c18a466e4b4b@o99789.ingest.sentry.io/219026',
|
|
sampleRate: 0.25,
|
|
beforeSend: (event, hint) => {
|
|
const {
|
|
originalException: {
|
|
message,
|
|
} = {},
|
|
} = hint;
|
|
|
|
// Filter out issues that destroy our quota and that are not informative enough to
|
|
// actually resolve.
|
|
if (
|
|
message
|
|
&& (
|
|
message.match(/Websocket/i)
|
|
|| message.match(/NotSupportedError/i)
|
|
|| message.match(/Non-Error promise rejection captured with keys: call, message, value/i)
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return event;
|
|
},
|
|
});
|
|
}
|
|
|
|
// Load query param settings
|
|
const configs = ['ui', 'spotify', 'pusher', 'snapcast', 'mopidy', 'google', 'lastfm', 'genius']
|
|
const params = new URLSearchParams(window.location.search)
|
|
var changed = []
|
|
var urlParams = params.forEach((v, k) => {
|
|
if (!configs.includes(k)) return;
|
|
try {
|
|
storage.set(k, JSON.parse(v))
|
|
changed.push(k)
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
})
|
|
if (changed.length > 0) {
|
|
changed.forEach((k) => params.delete(k))
|
|
const url = window.location.toString().replace(window.location.search, params.toString())
|
|
console.log(`Settings changed, redirecting to ${url}`, changed);
|
|
window.location.assign(url)
|
|
}
|
|
|
|
this.handleInstallPrompt = this.handleInstallPrompt.bind(this);
|
|
this.handleFocusAndBlur = this.handleFocusAndBlur.bind(this);
|
|
window.language = props.language;
|
|
}
|
|
|
|
componentDidMount() {
|
|
const {
|
|
history,
|
|
snapcast_enabled,
|
|
initial_setup_complete,
|
|
mopidyActions,
|
|
pusherActions,
|
|
snapcastActions,
|
|
} = this.props;
|
|
|
|
window.addEventListener(
|
|
'beforeinstallprompt',
|
|
this.handleInstallPrompt,
|
|
false,
|
|
);
|
|
window.addEventListener('focus', this.handleFocusAndBlur, false);
|
|
window.addEventListener('blur', this.handleFocusAndBlur, false);
|
|
|
|
// Fire up our services
|
|
mopidyActions.connect();
|
|
pusherActions.connect();
|
|
if (snapcast_enabled) {
|
|
snapcastActions.connect();
|
|
}
|
|
uiActions.getBroadcasts();
|
|
|
|
if (!initial_setup_complete) {
|
|
history.push('/initial-setup');
|
|
}
|
|
}
|
|
|
|
componentDidUpdate({
|
|
location: prevLocation,
|
|
}) {
|
|
const {
|
|
location = {},
|
|
allow_reporting,
|
|
uiActions,
|
|
context_menu,
|
|
} = this.props;
|
|
|
|
// When we have navigated to a new route
|
|
if (location !== prevLocation) {
|
|
// Log our pageview
|
|
if (allow_reporting) {
|
|
ReactGA.set({ page: location.pathname });
|
|
ReactGA.pageview(location.pathname);
|
|
}
|
|
|
|
// If the location has a "scroll_position" state variable, scroll to it.
|
|
// This is invisibly injected to the history by the Link component when navigating, so
|
|
// hitting back in the browser allows us to restore the position
|
|
const location_state = location.state || {};
|
|
if (location_state.scroll_position) {
|
|
scrollTo(parseInt(location_state.scroll_position), false);
|
|
}
|
|
|
|
uiActions.toggleSidebar(false);
|
|
uiActions.setSelectedTracks([]);
|
|
if (context_menu) uiActions.hideContextMenu();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener(
|
|
'beforeinstallprompt',
|
|
this.handleInstallPrompt,
|
|
false,
|
|
);
|
|
window.removeEventListener('focus', this.handleFocusAndBlur, false);
|
|
window.removeEventListener('blur', this.handleFocusAndBlur, false);
|
|
}
|
|
|
|
/**
|
|
* Using Visibility API, detect whether the browser is in focus or not
|
|
*
|
|
* This is used to keep background requests lean, preventing a queue of requests building up for
|
|
* when focus is retained. Seems most obvious on mobile devices with Chrome as it has throttled
|
|
* quota significantly: https://developers.google.com/web/updates/2017/03/background_tabs
|
|
*
|
|
* @param e Event
|
|
* */
|
|
handleFocusAndBlur() {
|
|
const { uiActions: { setWindowFocus } } = this.props;
|
|
setWindowFocus(document.hasFocus());
|
|
}
|
|
|
|
handleInstallPrompt(e) {
|
|
const { uiActions: { installPrompt } } = this.props;
|
|
e.preventDefault();
|
|
console.log('Install prompt detected');
|
|
installPrompt(e);
|
|
}
|
|
|
|
userInteracted = () => {
|
|
const { userHasInteracted } = this.state;
|
|
|
|
if (userHasInteracted) return;
|
|
this.setState({ userHasInteracted: true });
|
|
}
|
|
|
|
render = () => {
|
|
const {
|
|
uiActions,
|
|
location,
|
|
history,
|
|
debug_info,
|
|
theme,
|
|
dragging,
|
|
touch_dragging,
|
|
context_menu,
|
|
sidebar_open,
|
|
slim_mode,
|
|
wide_scrollbar_enabled,
|
|
smooth_scrolling_enabled,
|
|
hotkeys_enabled,
|
|
} = this.props;
|
|
const {
|
|
userHasInteracted,
|
|
} = this.state;
|
|
|
|
let className = `${theme}-theme app-inner`;
|
|
className += ` ${navigator.onLine ? 'online' : 'offline'}`
|
|
if (wide_scrollbar_enabled) {
|
|
className += ' wide-scrollbar';
|
|
}
|
|
if (dragging) {
|
|
className += ' dragging';
|
|
}
|
|
if (sidebar_open) {
|
|
className += ' sidebar-open';
|
|
}
|
|
if (touch_dragging) {
|
|
className += ' touch-dragging';
|
|
}
|
|
if (context_menu) {
|
|
className += ' context-menu-open';
|
|
}
|
|
if (slim_mode) {
|
|
className += ' slim-mode';
|
|
}
|
|
if (smooth_scrolling_enabled) {
|
|
className += ' smooth-scrolling-enabled';
|
|
}
|
|
if (isTouchDevice()) {
|
|
className += ' touch';
|
|
} else {
|
|
className += ' notouch';
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
onClick={userHasInteracted ? null : this.userInteracted}
|
|
onKeyDown={userHasInteracted ? null : this.userInteracted}
|
|
>
|
|
<div className="body">
|
|
<Switch>
|
|
<Route path="/initial-setup" component={InitialSetup} />
|
|
<Route path="/kiosk-mode" component={KioskMode} />
|
|
<Route path="/add-to-playlist/:uris" component={AddToPlaylist} />
|
|
<Route path="/image-zoom" component={ImageZoom} />
|
|
<Route path="/share-configuration" component={ShareConfiguration} />
|
|
<Route path="/reset" component={Reset} />
|
|
<Route path="/edit-command/:id?" component={EditCommand} />
|
|
|
|
<Route path="/queue/radio" component={EditRadio} />
|
|
<Route path="/queue/add-uri" component={AddToQueue} />
|
|
<Route path="/playlist/create" component={CreatePlaylist} />
|
|
<Route path="/playlist/:uri/edit" component={EditPlaylist} />
|
|
|
|
<Route>
|
|
<div>
|
|
<Sidebar
|
|
location={location}
|
|
history={history}
|
|
tabIndex="3"
|
|
/>
|
|
<PlaybackControls
|
|
history={history}
|
|
slim_mode={slim_mode}
|
|
tabIndex="2"
|
|
/>
|
|
|
|
<main id="main" className="smooth-scroll" tabIndex="1">
|
|
<Switch>
|
|
<Route exact path="/" component={Queue} />
|
|
|
|
<Route exact path="/queue" component={Queue} />
|
|
<Route
|
|
exact
|
|
path="/queue/history"
|
|
component={QueueHistory}
|
|
/>
|
|
<Route exact path="/settings/debug" component={Debug} />
|
|
<Route path="/settings" component={Settings} />
|
|
|
|
<Route
|
|
exact
|
|
path="/search/:type?/:term?"
|
|
component={Search}
|
|
/>
|
|
<Route exact path="/album/:uri" component={Album} />
|
|
<Route
|
|
exact
|
|
path="/artist/:uri/:sub_view?"
|
|
component={Artist}
|
|
/>
|
|
<Route exact path="/playlist/:uri" component={Playlist} />
|
|
<Route exact path="/user/:uri" component={User} />
|
|
<Route exact path="/track/:uri" component={Track} />
|
|
|
|
<Route
|
|
exact
|
|
path="/discover/recommendations/:seeds?"
|
|
component={DiscoverRecommendations}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/discover/featured"
|
|
component={DiscoverFeatured}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/discover/categories/:uri"
|
|
component={DiscoverCategory}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/discover/categories"
|
|
component={DiscoverCategories}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/discover/new-releases"
|
|
component={DiscoverNewReleases}
|
|
/>
|
|
|
|
<Route
|
|
exact
|
|
path="/library/artists"
|
|
component={LibraryArtists}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/library/albums"
|
|
component={LibraryAlbums}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/library/tracks"
|
|
component={LibraryTracks}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/library/playlists"
|
|
component={LibraryPlaylists}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/library/browse"
|
|
component={LibraryBrowse}
|
|
/>
|
|
<Route
|
|
exact
|
|
path="/library/browse/:uri"
|
|
component={LibraryBrowseDirectory}
|
|
/>
|
|
|
|
<Route>
|
|
<ErrorMessage type="not-found" title="Not found">
|
|
<p>Oops, that link could not be found</p>
|
|
</ErrorMessage>
|
|
</Route>
|
|
</Switch>
|
|
</main>
|
|
</div>
|
|
</Route>
|
|
</Switch>
|
|
</div>
|
|
|
|
<ResizeListener
|
|
uiActions={uiActions}
|
|
slim_mode={slim_mode}
|
|
/>
|
|
{hotkeys_enabled && <Hotkeys history={history} />}
|
|
<ContextMenu />
|
|
<Dragger />
|
|
<Notifications />
|
|
{userHasInteracted && <Stream />}
|
|
{userHasInteracted && ('mediaSession' in navigator) && <MediaSession />}
|
|
{debug_info && <DebugInfo />}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
const mapStateToProps = (state) => {
|
|
const {
|
|
ui: {
|
|
language,
|
|
theme,
|
|
wide_scrollbar_enabled,
|
|
smooth_scrolling_enabled,
|
|
hotkeys_enabled,
|
|
allow_reporting,
|
|
touch_dragging,
|
|
initial_setup_complete,
|
|
slim_mode,
|
|
sidebar_open,
|
|
dragger: {
|
|
active: dragging,
|
|
} = {},
|
|
context_menu,
|
|
debug_info,
|
|
test_mode,
|
|
},
|
|
snapcast: {
|
|
enabled: snapcast_enabled,
|
|
},
|
|
spotify: {
|
|
authorization: spotify_authorized,
|
|
},
|
|
} = state;
|
|
|
|
return {
|
|
language,
|
|
theme,
|
|
wide_scrollbar_enabled,
|
|
smooth_scrolling_enabled,
|
|
hotkeys_enabled,
|
|
allow_reporting,
|
|
touch_dragging,
|
|
initial_setup_complete,
|
|
slim_mode,
|
|
sidebar_open,
|
|
dragging,
|
|
context_menu,
|
|
debug_info,
|
|
test_mode,
|
|
snapcast_enabled,
|
|
spotify_authorized,
|
|
};
|
|
};
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
coreActions: bindActionCreators(coreActions, dispatch),
|
|
uiActions: bindActionCreators(uiActions, dispatch),
|
|
pusherActions: bindActionCreators(pusherActions, dispatch),
|
|
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
|
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
|
lastfmActions: bindActionCreators(lastfmActions, dispatch),
|
|
geniusActions: bindActionCreators(geniusActions, dispatch),
|
|
snapcastActions: bindActionCreators(snapcastActions, dispatch),
|
|
});
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps,
|
|
)(App);
|