soiz1's picture
Upload 1525 files
f2bee8a verified
/**
* Copyright (C) 2021 Thomas Weber
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Search from './search';
import importedAddons from '../generated/addon-manifests';
import messagesByLocale from '../generated/l10n-settings-entries';
import settingsTranslationsEnglish from './en.json';
import settingsTranslationsOther from './translations.json';
import upstreamMeta from '../generated/upstream-meta.json';
import { detectLocale } from '../../lib/detect-locale';
import { getInitialDarkMode } from '../../lib/tw-theme-hoc.jsx';
import SettingsStore from '../settings-store-singleton';
import Channels from '../channels';
import extensionImage from './icons/extension.svg';
import brushImage from './icons/brush.svg';
import undoImage from './icons/undo.svg';
import expandImageBlack from './icons/expand.svg';
import infoImage from './icons/info.svg';
import styles from './settings.css';
import '../polyfill';
import '../../lib/normalize.css';
/* eslint-disable no-alert */
/* eslint-disable no-console */
/* eslint-disable react/no-multi-comp */
/* eslint-disable react/jsx-no-bind */
const locale = detectLocale(Object.keys(messagesByLocale));
document.documentElement.lang = locale;
const addonTranslations = messagesByLocale[locale] ? messagesByLocale[locale]() : {};
const settingsTranslations = settingsTranslationsEnglish;
if (locale !== 'en') {
const messages = settingsTranslationsOther[locale] || settingsTranslationsOther[locale.split('-')[0]];
if (messages) {
Object.assign(settingsTranslations, messages);
}
}
document.title = `${settingsTranslations.title} - PenguinMod`;
const theme = getInitialDarkMode() ? 'dark' : 'light';
document.body.setAttribute('theme', theme);
let _throttleTimeout;
const postThrottledSettingsChange = store => {
if (_throttleTimeout) {
clearTimeout(_throttleTimeout);
}
_throttleTimeout = setTimeout(() => {
Channels.changeChannel.postMessage({
version: upstreamMeta.commit,
store
});
}, 100);
};
const filterAddonsBySupport = () => {
const supported = {};
const unsupported = {};
for (const [id, manifest] of Object.entries(importedAddons)) {
if (manifest.unsupported) {
unsupported[id] = manifest;
} else {
supported[id] = manifest;
}
}
return {
supported,
unsupported
};
};
const { supported: supportedAddons, unsupported: unsupportedAddons } = filterAddonsBySupport();
const groupAddons = () => {
const groups = {
new: {
label: settingsTranslations.groupNew,
open: true,
addons: []
},
others: {
label: settingsTranslations.groupOthers,
open: true,
addons: []
},
danger: {
label: settingsTranslations.groupDanger,
open: false,
addons: []
}
};
const manifests = Object.values(supportedAddons);
for (let index = 0; index < manifests.length; index++) {
const manifest = manifests[index];
if (manifest.tags.includes('new')) {
groups.new.addons.push(index);
} else if (manifest.tags.includes('danger') || manifest.noCompiler) {
groups.danger.addons.push(index);
} else {
groups.others.addons.push(index);
}
}
return groups;
};
const groupedAddons = groupAddons();
const CreditList = ({ credits }) => (
credits.map((author, index) => {
const isLast = index === credits.length - 1;
return (
<span
className={styles.credit}
key={index}
>
{author.link ? (
<a
href={author.link}
target="_blank"
rel="noreferrer"
>
{author.name}
</a>
) : (
<span>
{author.name}
</span>
)}
{isLast ? null : ', '}
</span>
);
})
);
CreditList.propTypes = {
credits: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
link: PropTypes.string
}))
};
const Switch = ({ onChange, value, ...props }) => (
<button
className={styles.switch}
state={value ? 'on' : 'off'}
role="checkbox"
aria-checked={value ? 'true' : 'false'}
tabIndex="0"
onClick={() => onChange(!value)}
{...props}
/>
);
Switch.propTypes = {
onChange: PropTypes.func,
value: PropTypes.bool
};
const Select = ({
onChange,
value,
values
}) => (
<div className={styles.select}>
{values.map(potentialValue => {
const id = potentialValue.id;
const selected = id === value;
return (
<button
key={id}
onClick={() => onChange(id)}
className={classNames(styles.selectOption, { [styles.selected]: selected })}
>
{potentialValue.name}
</button>
);
})}
</div>
);
Select.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string
}))
};
const Tags = ({ manifest }) => (
<span className={styles.tagContainer}>
{manifest.tags.includes('recommended') && (
<span className={classNames(styles.tag, styles.tagRecommended)}>
{settingsTranslations.tagRecommended}
</span>
)}
{manifest.tags.includes('theme') && (
<span className={classNames(styles.tag, styles.tagTheme)}>
{settingsTranslations.tagTheme}
</span>
)}
{manifest.tags.includes('beta') && (
<span className={classNames(styles.tag, styles.tagBeta)}>
{settingsTranslations.tagBeta}
</span>
)}
{manifest.tags.includes('new') && (
<span className={classNames(styles.tag, styles.tagNew)}>
{settingsTranslations.tagNew}
</span>
)}
{manifest.tags.includes('danger') && (
<span className={classNames(styles.tag, styles.tagDanger)}>
{settingsTranslations.tagDanger}
</span>
)}
</span>
);
Tags.propTypes = {
manifest: PropTypes.shape({
tags: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired
}).isRequired
};
class TextInput extends React.Component {
constructor(props) {
super(props);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleFlush = this.handleFlush.bind(this);
this.handleChange = this.handleChange.bind(this);
this.state = {
value: null,
focused: false
};
}
handleKeyPress(e) {
if (e.key === 'Enter') {
this.handleFlush(e);
e.target.blur();
}
}
handleFocus() {
this.setState({
focused: true
});
}
handleFlush(e) {
this.setState({
focused: false
});
if (this.state.value === null) {
return;
}
if (this.props.type === 'number') {
let value = +this.state.value;
const min = e.target.min;
const max = e.target.max;
const step = e.target.step;
if (min !== '') value = Math.max(min, value);
if (max !== '') value = Math.min(max, value);
if (step === '1') value = Math.round(value);
this.props.onChange(value);
} else {
this.props.onChange(this.state.value);
}
this.setState({ value: null });
}
handleChange(e) {
e.persist();
this.setState({ value: e.target.value }, () => {
// A change event can be fired when not focused by using the browser's number spinners
if (!this.state.focused) {
this.handleFlush(e);
}
});
}
render() {
return (
<input
{...this.props}
value={this.state.value === null ? this.props.value : this.state.value}
onFocus={this.handleFocus}
onBlur={this.handleFlush}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
);
}
}
TextInput.propTypes = {
onChange: PropTypes.func.isRequired,
type: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
const ColorInput = props => (
<input
type="color"
id={props.id}
value={props.value}
onChange={props.onChange}
/>
);
ColorInput.propTypes = {
id: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired
};
const ResetButton = ({
addonId,
settingId,
forTextInput
}) => (
<button
className={classNames(styles.button, styles.resetSettingButton)}
onClick={() => SettingsStore.setAddonSetting(addonId, settingId, null)}
title={settingsTranslations.reset}
data-for-text-input={forTextInput}
>
<img
src={undoImage}
alt={settingsTranslations.reset}
/>
</button>
);
ResetButton.propTypes = {
addonId: PropTypes.string,
settingId: PropTypes.string,
forTextInput: PropTypes.bool
};
const Setting = ({
addonId,
setting,
value
}) => {
if (!SettingsStore.evaluateCondition(addonId, setting.if)) {
return null;
}
const settingId = setting.id;
const settingName = addonTranslations[`${addonId}/@settings-name-${settingId}`] || setting.name;
const uniqueId = `setting/${addonId}/${settingId}`;
const label = (
<label
htmlFor={uniqueId}
className={styles.settingLabel}
>
{settingName}
</label>
);
return (
<div
className={styles.setting}
>
{setting.type === 'boolean' && (
<React.Fragment>
{label}
<input
id={uniqueId}
type="checkbox"
checked={value}
onChange={e => SettingsStore.setAddonSetting(addonId, settingId, e.target.checked)}
/>
</React.Fragment>
)}
{setting.type === 'integer' && (
<React.Fragment>
{label}
<TextInput
id={uniqueId}
type="number"
min={setting.min}
max={setting.max}
step="1"
value={value}
onChange={newValue => SettingsStore.setAddonSetting(addonId, settingId, newValue)}
/>
<ResetButton
addonId={addonId}
settingId={settingId}
forTextInput
/>
</React.Fragment>
)}
{setting.type === 'color' && (
<React.Fragment>
{label}
<ColorInput
id={uniqueId}
value={value}
onChange={e => SettingsStore.setAddonSetting(addonId, settingId, e.target.value)}
/>
<ResetButton
addonId={addonId}
settingId={settingId}
/>
</React.Fragment>
)}
{setting.type === 'select' && (
<React.Fragment>
{label}
<Select
value={value}
values={setting.potentialValues.map(({ id, name }) => ({
id,
name: addonTranslations[`${addonId}/@settings-select-${settingId}-${id}`] || name
}))}
onChange={v => SettingsStore.setAddonSetting(addonId, settingId, v)}
setting={setting}
/>
</React.Fragment>
)}
</div>
);
};
Setting.propTypes = {
addonId: PropTypes.string,
setting: PropTypes.shape({
type: PropTypes.string,
id: PropTypes.string,
name: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number,
default: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
potentialValues: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string
})),
if: PropTypes.shape({
addonEnabled: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
// eslint-disable-next-line react/forbid-prop-types
settings: PropTypes.object
})
}),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number])
};
const Notice = ({
type,
text
}) => (
<div
className={styles.notice}
type={type}
>
<div>
<img
className={styles.noticeIcon}
src={infoImage}
alt=""
draggable={false}
/>
</div>
<div>
{text}
</div>
</div>
);
Notice.propTypes = {
type: PropTypes.string,
text: PropTypes.string
};
const Presets = ({
addonId,
presets
}) => (
<div className={classNames(styles.setting, styles.presets)}>
<div className={styles.settingLabel}>
{settingsTranslations.presets}
</div>
{presets.map(preset => {
const presetId = preset.id;
const name = addonTranslations[`${addonId}/@preset-name-${presetId}`] || preset.name;
const description = addonTranslations[`${addonId}/@preset-description-${presetId}`] || preset.description;
return (
<button
key={presetId}
title={description}
className={classNames(styles.button, styles.presetButton)}
onClick={() => SettingsStore.applyAddonPreset(addonId, presetId)}
>
{name}
</button>
);
})}
</div>
);
Presets.propTypes = {
addonId: PropTypes.string,
presets: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
id: PropTypes.string,
description: PropTypes.string,
values: PropTypes.shape({})
}))
};
const Addon = ({
id,
settings,
manifest,
extended
}) => (
<div className={classNames(styles.addon, { [styles.addonDirty]: settings.dirty })}>
<div className={styles.addonHeader}>
<label className={styles.addonTitle}>
<div className={styles.addonSwitch}>
<Switch
value={settings.enabled}
onChange={value => {
if (
!value ||
!manifest.tags.includes('danger') ||
confirm(settingsTranslations.enableDangerous)
) {
SettingsStore.setAddonEnabled(id, value);
}
}}
/>
</div>
{manifest.tags.includes('theme') ? (
<img
className={styles.extensionImage}
src={brushImage}
draggable={false}
alt=""
/>
) : (
<img
className={styles.extensionImage}
src={extensionImage}
draggable={false}
alt=""
/>
)}
<div className={styles.addonTitleText}>
{addonTranslations[`${id}/@name`] || manifest.name}
</div>
{extended && (
<div className={styles.addonId}>
{`(${id})`}
</div>
)}
</label>
<Tags manifest={manifest} />
{!settings.enabled && (
<div className={styles.inlineDescription}>
{addonTranslations[`${id}/@description`] || manifest.description}
</div>
)}
<div className={styles.addonOperations}>
{settings.enabled && manifest.settings && (
<button
className={styles.resetButton}
onClick={() => SettingsStore.resetAddon(id)}
title={settingsTranslations.reset}
>
<img
src={undoImage}
className={styles.resetButtonImage}
alt={settingsTranslations.reset}
draggable={false}
/>
</button>
)}
</div>
</div>
{settings.enabled && (
<div className={styles.addonDetails}>
<div className={styles.description}>
{addonTranslations[`${id}/@description`] || manifest.description}
</div>
{manifest.credits && (
<div className={styles.creditContainer}>
<span className={styles.creditTitle}>
{settingsTranslations.credits}
</span>
<CreditList credits={manifest.credits} />
</div>
)}
{manifest.info && (
manifest.info.map(info => (
<Notice
key={info.id}
type={info.type}
text={addonTranslations[`${id}/@info-${info.id}`] || info.text}
/>
))
)}
{manifest.noCompiler && (
<Notice
type="warning"
text={settingsTranslations.noCompiler}
/>
)}
{manifest.settings && (
<div className={styles.settingContainer}>
{manifest.settings.map(setting => (
<Setting
key={setting.id}
addonId={id}
setting={setting}
value={settings[setting.id]}
/>
))}
{manifest.presets && (
<Presets
addonId={id}
presets={manifest.presets}
/>
)}
</div>
)}
</div>
)}
</div>
);
Addon.propTypes = {
id: PropTypes.string,
settings: PropTypes.shape({
enabled: PropTypes.bool,
dirty: PropTypes.bool
}),
manifest: PropTypes.shape({
name: PropTypes.string,
description: PropTypes.string,
credits: PropTypes.arrayOf(PropTypes.shape({})),
info: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string
})),
settings: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string
})),
presets: PropTypes.arrayOf(PropTypes.shape({})),
tags: PropTypes.arrayOf(PropTypes.string),
noCompiler: PropTypes.bool
}),
extended: PropTypes.bool
};
const Dirty = props => (
<div className={styles.dirtyOuter}>
<div className={styles.dirtyInner}>
{settingsTranslations.dirty}
{props.onReloadNow && (
<button
className={classNames(styles.button, styles.dirtyButton)}
onClick={props.onReloadNow}
>
{settingsTranslations.dirtyButton}
</button>
)}
</div>
</div>
);
Dirty.propTypes = {
onReloadNow: PropTypes.func
};
const UnsupportedAddons = ({ addons: addonList }) => (
<div className={styles.unsupportedContainer}>
<span className={styles.unsupportedText}>
{settingsTranslations.unsupported}
</span>
{addonList.map(({ id, manifest }, index) => (
<span
key={id}
className={styles.unsupportedAddon}
>
{addonTranslations[`${id}/@name`] || manifest.name}
{index !== addonList.length - 1 && (
', '
)}
</span>
))}
</div>
);
UnsupportedAddons.propTypes = {
addons: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
manifest: PropTypes.shape({
name: PropTypes.string
})
}))
};
const InternalAddonList = ({ addons, extended }) => (
addons.map(({ id, manifest, state }) => (
<Addon
key={id}
id={id}
settings={state}
manifest={manifest}
extended={extended}
/>
))
);
class AddonGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
open: props.open
};
}
render() {
if (this.props.addons.length === 0) {
return null;
}
return (
<div className={styles.addonGroup}>
<button
className={styles.addonGroupName}
onClick={() => {
this.setState({
open: !this.state.open
});
}}
>
<img
className={styles.addonGroupExpand}
src={expandImageBlack}
data-open={this.state.open}
alt=""
/>
{this.props.label.replace('{number}', this.props.addons.length)}
</button>
{this.state.open && (
<InternalAddonList
addons={this.props.addons}
extended={this.props.extended}
/>
)}
</div>
);
}
}
AddonGroup.propTypes = {
label: PropTypes.string,
open: PropTypes.bool,
addons: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
state: PropTypes.shape({}).isRequired,
manifest: PropTypes.shape({}).isRequired
})).isRequired,
extended: PropTypes.bool.isRequired
};
const addonToSearchItem = ({ id, manifest }) => {
const texts = new Set();
const addText = (score, text) => {
if (text) {
texts.add({
score,
text
});
}
};
addText(1, id);
addText(1, manifest.name);
addText(1, addonTranslations[`${id}/@name`]);
addText(0.5, manifest.description);
addText(0.5, addonTranslations[`${id}/@description`]);
if (manifest.settings) {
for (const setting of manifest.settings) {
addText(0.25, setting.name);
addText(0.25, addonTranslations[`${id}/@settings-name-${setting.id}`]);
}
}
if (manifest.presets) {
for (const preset of manifest.presets) {
addText(0.1, preset.name);
addText(0.1, addonTranslations[`${id}/@preset-name-${preset.id}`]);
addText(0.1, preset.description);
addText(0.1, addonTranslations[`${id}/@preset-description-${preset.id}`]);
}
}
for (const tag of manifest.tags) {
const key = `tags.${tag}`;
if (settingsTranslations[key]) {
addText(0.25, settingsTranslations[key]);
}
}
if (manifest.info) {
for (const info of manifest.info) {
addText(0.25, info.text);
addText(0.25, addonTranslations[`${id}/@info-${info.id}`]);
}
}
return texts;
};
class AddonList extends React.Component {
constructor(props) {
super(props);
this.search = new Search(this.props.addons.map(addonToSearchItem));
this.groups = [];
}
render() {
if (this.props.search) {
const addons = this.search.search(this.props.search)
.slice(0, 20)
.map(({ index }) => this.props.addons[index]);
if (addons.length === 0) {
return (
<div className={styles.noResults}>
{settingsTranslations.noResults}
</div>
);
}
return (
<div>
<InternalAddonList
addons={addons}
extended={this.props.extended}
/>
</div>
);
}
return (
<div>
{Object.entries(groupedAddons).map(([id, { label, addons, open }]) => (
<AddonGroup
key={id}
label={label}
open={open}
addons={addons.map(index => this.props.addons[index])}
extended={this.props.extended}
/>
))}
</div>
);
}
}
AddonList.propTypes = {
addons: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
state: PropTypes.shape({}).isRequired,
manifest: PropTypes.shape({}).isRequired
})).isRequired,
search: PropTypes.string.isRequired,
extended: PropTypes.bool.isRequired
};
class AddonSettingsComponent extends React.Component {
constructor(props) {
super(props);
this.handleSettingStoreChanged = this.handleSettingStoreChanged.bind(this);
this.handleReloadNow = this.handleReloadNow.bind(this);
this.handleResetAll = this.handleResetAll.bind(this);
this.handleExport = this.handleExport.bind(this);
this.handleImport = this.handleImport.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleClickSearchButton = this.handleClickSearchButton.bind(this);
this.handleClickVersion = this.handleClickVersion.bind(this);
this.searchRef = this.searchRef.bind(this);
this.searchBar = null;
this.state = {
loading: false,
dirty: false,
search: location.hash ? location.hash.substr(1) : '',
extended: false,
...this.readFullAddonState()
};
if (Channels.changeChannel) {
Channels.changeChannel.addEventListener('message', () => {
SettingsStore.readLocalStorage();
this.setState(this.readFullAddonState());
});
}
}
componentDidMount() {
SettingsStore.addEventListener('setting-changed', this.handleSettingStoreChanged);
document.body.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
SettingsStore.removeEventListener('setting-changed', this.handleSettingStoreChanged);
document.body.removeEventListener('keydown', this.handleKeyDown);
}
readFullAddonState() {
const result = {};
for (const [id, manifest] of Object.entries(supportedAddons)) {
const enabled = SettingsStore.getAddonEnabled(id);
const addonState = {
enabled: enabled,
dirty: false
};
if (manifest.settings) {
for (const setting of manifest.settings) {
addonState[setting.id] = SettingsStore.getAddonSetting(id, setting.id);
}
}
result[id] = addonState;
}
return result;
}
handleSettingStoreChanged(e) {
const { addonId, settingId, value } = e.detail;
// If channels are unavailable, every change requires reload.
const reloadRequired = e.detail.reloadRequired || !Channels.changeChannel;
this.setState(state => {
const newState = {
[addonId]: {
...state[addonId],
[settingId]: value,
dirty: true
}
};
if (reloadRequired) {
newState.dirty = true;
}
return newState;
});
if (!reloadRequired) {
postThrottledSettingsChange(SettingsStore.store);
}
}
handleReloadNow() {
// Value posted does not matter
Channels.reloadChannel.postMessage(0);
this.setState({
dirty: false
});
for (const addonId of Object.keys(supportedAddons)) {
if (this.state[addonId].dirty) {
this.setState(state => ({
[addonId]: {
...state[addonId],
dirty: false
}
}));
}
}
}
handleResetAll() {
if (confirm(settingsTranslations.confirmResetAll)) {
SettingsStore.resetAllAddons();
this.setState({
search: ''
});
}
}
handleExport() {
const exportedData = SettingsStore.export({
theme
});
this.props.onExportSettings(exportedData);
}
handleImport() {
const fileSelector = document.createElement('input');
fileSelector.type = 'file';
fileSelector.accept = '.json';
document.body.appendChild(fileSelector);
fileSelector.click();
document.body.removeChild(fileSelector);
fileSelector.addEventListener('change', async () => {
const file = fileSelector.files[0];
if (!file) {
return;
}
try {
const text = await file.text();
const data = JSON.parse(text);
SettingsStore.import(data);
this.setState({
search: ''
});
} catch (e) {
console.error(e);
alert(e);
}
});
}
handleSearch(e) {
const value = e.target.value;
this.setState({
search: value
});
}
handleClickSearchButton() {
this.setState({
search: ''
});
this.searchBar.focus();
}
handleClickVersion() {
this.setState({
extended: !this.state.extended
});
}
searchRef(searchBar) {
this.searchBar = searchBar;
}
handleKeyDown(e) {
const key = e.key;
if (key.length === 1 && key !== ' ' && e.target === document.body && !(e.ctrlKey || e.metaKey || e.altKey)) {
this.searchBar.focus();
}
// Only preventDefault() if the search bar isn't already focused so
// that we don't break the browser's builtin ctrl+f
if (key === 'f' && (e.ctrlKey || e.metaKey) && document.activeElement !== this.searchBar) {
this.searchBar.focus();
e.preventDefault();
}
}
render() {
const addonState = Object.entries(supportedAddons).map(([id, manifest]) => ({
id,
manifest,
state: this.state[id]
}));
const unsupported = Object.entries(unsupportedAddons).map(([id, manifest]) => ({
id,
manifest
}));
return (
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.section}>
<div className={styles.searchContainer}>
<input
className={styles.searchInput}
value={this.state.search}
onChange={this.handleSearch}
placeholder={settingsTranslations.search}
aria-label={settingsTranslations.search}
ref={this.searchRef}
spellCheck="false"
autoFocus
/>
<div
className={styles.searchButton}
onClick={this.handleClickSearchButton}
/>
</div>
<a
href="https://discord.gg/NZ9MBMYTZh"
target="_blank"
rel="noreferrer"
className={styles.feedbackButtonOuter}
>
<span className={styles.feedbackButtonInner}>
{settingsTranslations.addonFeedback}
</span>
</a>
</div>
{this.state.dirty && (
<Dirty
onReloadNow={Channels.reloadChannel ? this.handleReloadNow : null}
/>
)}
</div>
<div className={styles.addons}>
{!this.state.loading && (
<div className={styles.section}>
<AddonList
addons={addonState}
search={this.state.search}
extended={this.state.extended}
/>
<div className={styles.footerButtons}>
<button
className={classNames(styles.button, styles.resetAllButton)}
onClick={this.handleResetAll}
>
{settingsTranslations.resetAll}
</button>
<button
className={classNames(styles.button, styles.exportButton)}
onClick={this.handleExport}
>
{settingsTranslations.export}
</button>
<button
className={classNames(styles.button, styles.importButton)}
onClick={this.handleImport}
>
{settingsTranslations.import}
</button>
</div>
<footer className={styles.footer}>
{unsupported.length ? (
<UnsupportedAddons
addons={unsupported}
/>
) : null}
<span
className={styles.version}
onClick={this.handleClickVersion}
>
{this.state.extended ?
// Don't bother translating, pretty much no one will ever see this.
// eslint-disable-next-line max-len
`You have enabled debug mode. (Addons version ${upstreamMeta.commit})` :
`Addons version ${upstreamMeta.commit}`}
</span>
</footer>
</div>
)}
</div>
</div>
);
}
}
AddonSettingsComponent.propTypes = {
onExportSettings: PropTypes.func
};
export default AddonSettingsComponent;