Refactoring App to functional component, to run pre-ready actions (preconfiguration, etc)

This commit is contained in:
James Barnsley
2021-10-29 15:13:37 +13:00
parent 4d4545e229
commit 663665a669
8 changed files with 17492 additions and 14308 deletions

View File

@ -79,8 +79,6 @@
height: 1rem;
position: relative;
width: 100%; }
/*# sourceMappingURL=index.css.map */
@charset "UTF-8";
/*!
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -64,7 +64,7 @@
max-width: 100px;
opacity: 0.25;
}
#app-loading-loader {
#app-loading-loader.loading {
position: relative;
margin: 30px auto 0;
width: 80vw;
@ -74,7 +74,7 @@
overflow: hidden;
background-color: #1A1A1A;
}
#app-loading-loader::before {
#app-loading-loader.loading::before {
position: absolute;
display: block;
content: '';
@ -85,6 +85,13 @@
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
#app-loading-message {
color: white;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
padding: 10px;
opacity: 0.5;
}
</style>
</head>
@ -94,7 +101,12 @@
<!-- ReactJS app gets injected here, replacing the loader -->
<div id="app-loading">
<img src="/iris/assets/app-icon.svg" id="app-loading-logo" />
<div id="app-loading-loader"></div>
<div id="app-loading-loader" class="loading"></div>
<div id="app-loading-message">
<div id="app-loading-message-base"></div>
<div id="app-loading-message-js"></div>
<div id="app-loading-message-css"></div>
</div>
</div>
</div>
@ -112,10 +124,22 @@
</script>
<script type="text/javascript">
var setMessage = function(type = '', content = '') {
var element = document.getElementById(`app-loading-message-${type}`);
if (element) {
element.innerHTML += content;
}
}
var setLoading = function(loading) {
var element = document.getElementById('app-loading-loader');
if (element) {
element.className = loading ? 'loading' : '';
}
}
// Release details
// These are automatically injected to built HTML
var build = "1635321372";
var build = "1635473511";
var version = "3.60.1";
// Construct the script tag
@ -137,19 +161,35 @@
} else {
window.test_mode = localStorage.getItem('test_mode');
}
if (window.test_mode) {
setMessage('base', 'Test mode detected<br />');
}
// Test mode, use un-minified code
if (window.test_mode){
console.debug("Test mode enabled, using un-minified code");
if (window.test_mode){ // Test mode, use un-minified code
css.href = '/iris/app.css?v='+build;
js.src = '/iris/app.js?v='+build;
// Production-grade, minified app
} else {
} else { // Production-grade, minified app
css.href = '/iris/app.min.css?v='+build;
js.src = '/iris/app.min.js?v='+build;
}
setMessage('js', 'Loading core... ');
setMessage('css', 'Loading interface... ');
js.onload = function(){
setMessage('js', 'done');
}
js.onerror = function(){
setLoading(false);
setMessage('js', 'failed');
}
css.onload = function(){
setMessage('css', 'done');
}
css.onerror = function(){
setLoading(false);
setMessage('css', 'failed');
}
// And finally inject our CSS/JS tags
document.body.appendChild(css);
document.body.appendChild(js);

View File

@ -101,8 +101,12 @@
<!-- ReactJS app gets injected here, replacing the loader -->
<div id="app-loading">
<img src="/iris/assets/app-icon.svg" id="app-loading-logo" />
<div id="app-loading-loader"></div>
<div id="app-loading-message"></div>
<div id="app-loading-loader" class="loading"></div>
<div id="app-loading-message">
<div id="app-loading-message-base"></div>
<div id="app-loading-message-js"></div>
<div id="app-loading-message-css"></div>
</div>
</div>
</div>
@ -120,17 +124,17 @@
</script>
<script type="text/javascript">
var setMessage = function(message, append = false) {
var element = document.getElementById('app-loading-message');
if (append) {
element.innerHTML += message;
} else {
element.innerHTML = message;
var setMessage = function(type = '', content = '') {
var element = document.getElementById(`app-loading-message-${type}`);
if (element) {
element.innerHTML += content;
}
}
var setLoading = function(loading) {
var element = document.getElementById('app-loading-loader');
element.className = loading ? 'loading' : '';
if (element) {
element.className = loading ? 'loading' : '';
}
}
// Release details
@ -158,34 +162,32 @@
window.test_mode = localStorage.getItem('test_mode');
}
if (window.test_mode) {
setMessage('Loading (TEST MODE)');
} else {
setMessage('Loading');
}
setMessage('base', 'Test mode detected<br />');
}
// Test mode, use un-minified code
if (window.test_mode){
console.debug("Test mode enabled, using un-minified code");
if (window.test_mode){ // Test mode, use un-minified code
css.href = '/iris/app.css?v='+build;
js.src = '/iris/app.js?v='+build;
// Production-grade, minified app
} else {
} else { // Production-grade, minified app
css.href = '/iris/app.min.css?v='+build;
js.src = '/iris/app.min.js?v='+build;
}
setMessage('js', 'Loading core... ');
setMessage('css', 'Loading interface... ');
js.onload = function(){
setMessage('Initializing...');
setMessage('js', 'done');
}
js.onerror = function(){
setMessage('<br />Could not load javascript', true);
setLoading(false);
setMessage('js', 'failed');
}
css.onload = function(){
setMessage('Initializing...');
setMessage('css', 'done');
}
css.onerror = function(){
setMessage('<br />Could not load CSS', true);
setLoading(false);
setMessage('css', 'failed');
}
// And finally inject our CSS/JS tags

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { bindActionCreators } from 'redux';
import { Route, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
@ -67,48 +67,46 @@ import * as snapcastActions from './services/snapcast/actions';
import MediaSession from './components/MediaSession';
import ErrorBoundary from './components/ErrorBoundary';
export class App extends React.Component {
constructor(props) {
super(props);
const { allow_reporting } = this.props;
this.state = {
userHasInteracted: false,
};
const App = ({
language,
allow_reporting,
initial_setup_complete,
snapcast_enabled,
context_menu,
history,
location,
debug_info,
theme,
dragging,
touch_dragging,
sidebar_open,
slim_mode,
wide_scrollbar_enabled,
hide_scrollbars,
smooth_scrolling_enabled,
hotkeys_enabled,
...props
}) => {
const {
coreActions,
mopidyActions,
pusherActions,
uiActions,
} = props;
const {
pathname,
state: {
scroll_position,
} = {},
} = location;
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;
window.language = language;
const [isReady, setIsReady] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);
// 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(/NotSupportedError: The element has no supported sources./i)
|| message.match(/Non-Error promise rejection captured with keys: call, message, value/i)
|| message.match(/Cannot read property 'addChunk' of undefined/i)
)
) {
return null;
}
return event;
},
});
}
// Accept incoming preconfiguration via URL parameters and inject into application state.
// For example: iris?snapcast={"enabled":true,"host":"myserver.local"}
// Accept incoming preconfiguration via URL parameters and inject into application state.
// For example: iris?snapcast={"enabled":true,"host":"myserver.local"}
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params) {
params.forEach((value, key) => {
@ -120,7 +118,7 @@ export class App extends React.Component {
case 'pusher':
case 'snapcast':
case 'mopidy':
this.props[`${key}Actions`].set(json);
props[`${key}Actions`].set(json);
console.info(`Applying preconfiguration for ${key}:`, value)
break;
default:
@ -131,91 +129,96 @@ export class App extends React.Component {
return;
}
});
// Wait a moment to allow actions to complete before we proceed
setTimeout(
() => setIsReady(true),
250,
);
} else {
setIsReady(true);
}
}, []);
this.handleInstallPrompt = this.handleInstallPrompt.bind(this);
this.handleFocusAndBlur = this.handleFocusAndBlur.bind(this);
window.language = props.language;
}
// Event listeners
useEffect(() => {
window.addEventListener('beforeinstallprompt', handleInstallPrompt, false);
window.addEventListener('focus', handleFocusAndBlur, false);
window.addEventListener('blur', handleFocusAndBlur, false);
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();
return () => {
window.removeEventListener('beforeinstallprompt', handleInstallPrompt, false);
window.removeEventListener('focus', handleFocusAndBlur, false);
window.removeEventListener('blur', handleFocusAndBlur, false);
}
uiActions.getBroadcasts();
}, []);
if (!initial_setup_complete) {
history.push('/initial-setup');
}
}
componentDidUpdate({
location: {
pathname: prevPathname,
},
}) {
const {
location: {
pathname,
state: {
scroll_position,
} = {},
} = {},
allow_reporting,
uiActions,
context_menu,
} = this.props;
// When we have navigated to a new route
if (pathname !== prevPathname) {
// Log our pageview
// Primary engines
useEffect(() => {
if (isReady) {
if (allow_reporting) {
ReactGA.set({ page: pathname });
ReactGA.pageview(pathname);
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(/NotSupportedError: The element has no supported sources./i)
|| message.match(/Non-Error promise rejection captured with keys: call, message, value/i)
|| message.match(/Cannot read property 'addChunk' of undefined/i)
)
) {
return null;
}
return event;
},
});
}
// 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
if (scroll_position) {
scrollTo(parseInt(scroll_position, 10), false);
mopidyActions.connect();
pusherActions.connect();
uiActions.getBroadcasts();
if (snapcast_enabled) {
snapcastActions.connect();
}
if (!initial_setup_complete) {
history.push('/initial-setup');
}
uiActions.toggleSidebar(false);
uiActions.setSelectedTracks([]);
if (context_menu) uiActions.hideContextMenu();
}
}
}, [isReady])
componentWillUnmount() {
window.removeEventListener(
'beforeinstallprompt',
this.handleInstallPrompt,
false,
);
window.removeEventListener('focus', this.handleFocusAndBlur, false);
window.removeEventListener('blur', this.handleFocusAndBlur, false);
}
// Path changed (aka app navigation)
useEffect(() => {
if (allow_reporting) {
ReactGA.set({ page: pathname });
ReactGA.pageview(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
if (scroll_position) {
scrollTo(parseInt(scroll_position, 10), false);
}
uiActions.toggleSidebar(false);
uiActions.setSelectedTracks([]);
if (context_menu) {
uiActions.hideContextMenu();
}
}, [pathname]);
/**
* Using Visibility API, detect whether the browser is in focus or not
@ -226,227 +229,196 @@ export class App extends React.Component {
*
* @param e Event
* */
handleFocusAndBlur() {
const { uiActions: { setWindowFocus } } = this.props;
setWindowFocus(document.hasFocus());
const handleFocusAndBlur = () => {
uiActions.setWindowFocus(document.hasFocus());
}
handleInstallPrompt(e) {
const { uiActions: { installPrompt } } = this.props;
const handleInstallPrompt = (e) => {
e.preventDefault();
console.log('Install prompt detected');
installPrompt(e);
uiActions.installPrompt(e);
}
userInteracted = () => {
const { userHasInteracted } = this.state;
if (userHasInteracted) return;
this.setState({ userHasInteracted: true });
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 (hide_scrollbars) {
className += ' hide-scrollbars';
}
if (isTouchDevice()) {
className += ' touch';
} else {
className += ' notouch';
}
render = () => {
const {
uiActions,
location,
history,
debug_info,
theme,
dragging,
touch_dragging,
context_menu,
sidebar_open,
slim_mode,
wide_scrollbar_enabled,
hide_scrollbars,
smooth_scrolling_enabled,
hotkeys_enabled,
} = this.props;
const {
userHasInteracted,
} = this.state;
return (
<div
className={className}
onClick={() => hasInteracted ? null : setHasInteracted(true)}
onKeyDown={() => hasInteracted ? null : setHasInteracted(true)}
>
<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="/hotkeys" component={HotkeysInfo} />
<Route path="/share-configuration" component={ShareConfiguration} />
<Route path="/reset" component={Reset} />
<Route path="/servers" component={Servers} />
<Route path="/edit-command/:id?" component={EditCommand} />
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 (hide_scrollbars) {
className += ' hide-scrollbars';
}
if (isTouchDevice()) {
className += ' touch';
} else {
className += ' notouch';
}
<Route path="/queue/radio" component={EditRadio} />
<Route path="/queue/add-uri" component={AddToQueue} />
<Route path="/playlist/create/:uris?" component={CreatePlaylist} />
<Route path="/playlist/:uri/edit" component={EditPlaylist} />
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="/hotkeys" component={HotkeysInfo} />
<Route path="/share-configuration" component={ShareConfiguration} />
<Route path="/reset" component={Reset} />
<Route path="/servers" component={Servers} />
<Route path="/edit-command/:id?" component={EditCommand} />
<Route>
<div>
<Sidebar
location={location}
history={history}
tabIndex="3"
/>
<PlaybackControls
history={history}
slim_mode={slim_mode}
tabIndex="2"
/>
<Route path="/queue/radio" component={EditRadio} />
<Route path="/queue/add-uri" component={AddToQueue} />
<Route path="/playlist/create/:uris?" component={CreatePlaylist} />
<Route path="/playlist/:uri/edit" component={EditPlaylist} />
<main id="main" className="smooth-scroll" tabIndex="1">
<Switch>
<Route exact path="/" component={Queue} />
<Route>
<div>
<Sidebar
location={location}
history={history}
tabIndex="3"
/>
<PlaybackControls
history={history}
slim_mode={slim_mode}
tabIndex="2"
/>
<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} />
<main id="main" className="smooth-scroll" tabIndex="1">
<Switch>
<Route exact path="/" component={Queue} />
<Route
exact
path="/search/:type?/:term?"
component={Search}
/>
<Route
exact
path="/artist/:uri/:sub_view?"
component={Artist}
/>
<Route exact path="/album/:uri/:name?" component={Album} />
<Route exact path="/playlist/:uri/:name?" component={Playlist} />
<Route exact path="/user/:uri/:name?" component={User} />
<Route exact path="/track/:uri/:name?" component={Track} />
<Route exact path="/uri/:uri/:name?" component={UriRedirect} />
<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="/discover/recommendations/:uri?"
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="/search/:type?/:term?"
component={Search}
/>
<Route
exact
path="/artist/:uri/:sub_view?"
component={Artist}
/>
<Route exact path="/album/:uri/:name?" component={Album} />
<Route exact path="/playlist/:uri/:name?" component={Playlist} />
<Route exact path="/user/:uri/:name?" component={User} />
<Route exact path="/track/:uri/:name?" component={Track} />
<Route exact path="/uri/:uri/:name?" component={UriRedirect} />
<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/:name/:uri"
component={LibraryBrowseDirectory}
/>
<Route
exact
path="/discover/recommendations/:uri?"
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/:name/: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 && <ErrorBoundary silent><Stream /></ErrorBoundary>}
{userHasInteracted && ('mediaSession' in navigator) && <MediaSession />}
{debug_info && <DebugInfo />}
<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 />
{hasInteracted && <ErrorBoundary silent><Stream /></ErrorBoundary>}
{hasInteracted && ('mediaSession' in navigator) && <MediaSession />}
{debug_info && <DebugInfo />}
</div>
);
};
const mapStateToProps = (state) => {
const {