Moving reset into dedicated Reset modal

This commit is contained in:
James Barnsley
2020-11-29 21:28:24 +13:00
parent d1f8aa8c44
commit a6869b16aa
15 changed files with 673 additions and 500 deletions

View File

@ -50,6 +50,7 @@ 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';
@ -289,6 +290,7 @@ export class App extends React.Component {
<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} />

View File

@ -1,12 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import localForage from 'localforage';
import { get as getStorage } from '../util/storage';
import { isTouchDevice } from '../util/helpers';
import * as uiActions from '../services/ui/actions';
import { indexToArray } from '../util/arrays';
import localForage from 'localforage';
class DebugInfo extends React.Component {
constructor(props) {

View File

@ -399,9 +399,7 @@ settings:
up_to_date: Up to date
share_configuration: Share configuration
restart: Restart server
reset_cache: Reset cache
reset_storage: Reset storage
reset: Reset all settings
reset: Reset settings
about:
title: About
blurb_1: ' is an open-source project by '
@ -561,3 +559,22 @@ modal:
do_you_want_to_import: Do you want to import this?
import_now: Import now
successful: Import successful
reset:
title: Reset Iris
subtitle: Clear your local configuration, storage information or cache
items: Reset to defaults
preferences:
label: Preferences
description: Interface customisation
database:
label: Database
description: Local browser-based database of libraries and assets
service_worker:
label: Service worker
description: Re-install service worker
cache:
label: Fetch cache
description: External API requests cache (requires Service Worker)
test_mode:
label: Test mode
description: Verbose logging for diagnosing issues

View File

@ -50,6 +50,12 @@ export function set(data) {
};
}
export function resetState() {
return {
type: 'RESET_STATE',
};
}
export function clearCurrentTrack() {
return {
type: 'CLEAR_CURRENT_TRACK',

View File

@ -27,9 +27,8 @@ import geniusMiddleware from '../services/genius/middleware';
import spotifyMiddleware from '../services/spotify/middleware';
import googleMiddleware from '../services/google/middleware';
import snapcastMiddleware from '../services/snapcast/middleware';
import localstorageMiddleware from '../services/localstorage/middleware';
let state = {
let initialState = {
core: {
outputs: [],
queue: [],
@ -135,7 +134,7 @@ state.snapcast = { ...state.snapcast, ...storage.get('snapcast') };
*/
// Run any migrations
state = migration(state);
initialState = migration(initialState);
const rootPersistConfig = {
key: 'root',
@ -230,7 +229,7 @@ const uiPersistConfig = {
debug: window.test_mode,
};
const rootReducer = combineReducers({
const appReducer = combineReducers({
core: persistReducer(corePersistConfig, core),
ui: persistReducer(uiPersistConfig, ui),
mopidy: persistReducer(mopidyPersistConfig, mopidy),
@ -241,12 +240,18 @@ const rootReducer = combineReducers({
google,
snapcast,
});
const rootReducer = (state, action) => {
if (action.type === 'RESET_STATE') {
state = initialState;
}
return appReducer(state, action);
};
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
const store = createStore(
persistedReducer,
state,
initialState,
applyMiddleware(
thunk,
coreMiddleware,

View File

@ -71,62 +71,6 @@ class Settings extends React.Component {
this.setState({ [name]: value });
}
resetAllSettings = () => {
localForage.clear().then(() => {
console.debug('Cleared settings, reloading...');
window.location = '#';
window.location.reload(true);
});
return false;
}
resetStorage = () => {
localForage.keys().then((keys) => {
const keysToKeep = ['persist:root', 'persist:ui', 'persist:spotify'];
const keysToRemove = keys.filter((key) => keysToKeep.indexOf(key) < 0);
keysToRemove.forEach((key, index) => {
localForage.removeItem(key).then(() => {
console.debug(`Removed ${key}`);
if (index === keysToRemove.length) {
console.debug('Reloading...');
window.location = '#';
window.location.reload(true);
}
});
});
});
}
resetServiceWorkerAndCache = () => {
const { coreActions: { handleException } } = this.props;
if ('serviceWorker' in navigator) {
// Hose out all our caches
caches.keys().then(function (cacheNames) {
cacheNames.forEach(function (cacheName) {
caches.delete(cacheName);
});
});
// Unregister all service workers
// This forces our SW to bugger off and a new one is registered on refresh
navigator.serviceWorker.getRegistrations().then(
(registrations) => {
for (let registration of registrations) {
registration.unregister();
}
}
);
window.location = '#';
window.location.reload(true);
} else {
handleException(i18n('errors.no_service_worker'));
}
}
doRestart = () => {
const { pusherActions: { restart } } = this.props;
restart();
@ -584,23 +528,11 @@ class Settings extends React.Component {
<I18n path="settings.advanced.restart" />
</Button>
<Button
to="/reset"
type="destructive"
onClick={this.resetServiceWorkerAndCache}
tracking={{ category: 'System', action: 'ResetCache' }}
>
<I18n path="settings.advanced.reset_cache" />
<I18n path="settings.advanced.reset" />
</Button>
<Button
type="destructive"
onClick={this.resetStorage}
tracking={{ category: 'System', action: 'ResetStorage' }}
>
<I18n path="settings.advanced.reset_storage" />
</Button>
<ConfirmationButton
content={i18n('settings.advanced.reset')}
onConfirm={this.resetAllSettings}
/>
</div>
<h4 className="underline">

View File

@ -0,0 +1,213 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import localForage from 'localforage';
import Modal from './Modal';
import * as uiActions from '../../services/ui/actions';
import * as coreActions from '../../services/core/actions';
import { i18n, I18n } from '../../locale';
import Button from '../../components/Button';
class Reset extends React.Component {
constructor(props) {
super(props);
this.state = {
test_mode: false,
preferences: false,
database: true,
cache: ('serviceWorker' in navigator),
service_worker: ('serviceWorker' in navigator),
working: false,
};
}
componentDidMount() {
this.props.uiActions.setWindowTitle(i18n('modal.reset.title'));
}
handleSubmit = (e) => {
const {
coreActions: {
resetState,
},
} = this.props;
const {
preferences,
database,
cache,
service_worker,
test_mode,
working,
} = this.state;
if (working) return null;
this.setState({ working: true });
e.preventDefault();
const tasks = [];
if (preferences) {
tasks.push(
new Promise((resolve) => {
const keysToRemove = ['persist:root', 'persist:ui', 'persist:spotify', 'persist:pusher'];
keysToRemove.forEach((key, index) => {
localForage.removeItem(key).then(() => {
console.debug(`Removed ${key}`);
if (index === keysToRemove.length - 1) {
resolve();
}
});
});
}),
);
}
if (database) {
tasks.push(
new Promise((resolve) => {
localForage.keys().then((keys) => {
const keysToKeep = ['persist:root', 'persist:ui', 'persist:spotify', 'persist:pusher'];
const keysToRemove = keys.filter((key) => keysToKeep.indexOf(key) < 0);
keysToRemove.forEach((key, index) => {
localForage.removeItem(key).then(() => {
console.debug(`Removed ${key}`);
if (index === keysToRemove.length - 1) {
resolve();
}
});
});
});
}),
);
}
if (cache) {
tasks.push(
new Promise((resolve, reject) => {
if ('serviceWorker' in navigator) {
caches.keys().then((cacheNames) => {
cacheNames.forEach((cacheName) => {
caches.delete(cacheName);
});
resolve();
});
} else {
reject();
}
}),
);
}
if (service_worker) {
tasks.push(
new Promise((resolve, reject) => {
if ('serviceWorker' in navigator) {
// Unregister all service workers
// This forces our SW to bugger off and a new one is registered on refresh
navigator.serviceWorker.getRegistrations().then(
(registrations) => {
for (const registration of registrations) {
registration.unregister();
}
resolve();
},
);
} else {
reject();
}
}),
);
}
Promise.all(tasks).then(() => {
console.log('Reset complete, refreshing...');
resetState();
setTimeout(
() => window.location = `/iris${test_mode ? '?test_mode=0' : ''}`,
1000,
);
});
}
render = () => {
const {
preferences,
database,
service_worker,
cache,
test_mode,
working,
} = this.state;
return (
<Modal className="modal--reset">
<h1>
<I18n path="modal.reset.title" />
</h1>
<h2>
<I18n path="modal.reset.subtitle" />
</h2>
<form onSubmit={this.handleSubmit}>
<div className="field checkbox checkbox--block">
<div className="name">
<I18n path="modal.reset.items" />
</div>
<div className="input">
{['preferences', 'database', 'cache', 'service_worker', 'test_mode'].map((name) => {
const { [name]: value } = this.state;
const disabled = (name === 'cache' || name === 'service_worker')
&& !('serviceWorker' in navigator);
return (
<div className="checkbox-group__item" key={name}>
<label>
<input
type="checkbox"
name="spotify"
checked={value}
disabled={disabled}
onChange={() => this.setState({ [name]: !value })}
/>
<div className="label">
<div>
<div className="title">
<I18n path={`modal.reset.${name}.label`} />
</div>
<div className="description mid_grey-text">
<I18n path={`modal.reset.${name}.description`} />
</div>
</div>
</div>
</label>
</div>
);
})}
</div>
</div>
<div className="actions centered-text">
<Button
type="primary"
size="large"
onClick={this.handleSubmit}
working={working}
disabled={!database && !cache && !service_worker && !preferences && !test_mode}
tracking={{ category: 'Reset', action: 'Submit' }}
>
<I18n path="actions.reset" />
</Button>
</div>
</form>
</Modal>
);
}
}
const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Reset);

View File

@ -239,7 +239,8 @@
}
}
&--share-configuration {
&--share-configuration,
&--reset {
.checkbox-group {
&__item {
padding-bottom: 0.75rem;
@ -255,7 +256,7 @@
}
.description {
padding-top: 4px;
padding-top: 0;
}
}
}