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 (

Oops, that link could not be found

{hotkeys_enabled && } {userHasInteracted && } {userHasInteracted && ('mediaSession' in navigator) && } {debug_info && }
); } } 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);