/** * 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 . */ 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 ( {author.link ? ( {author.name} ) : ( {author.name} )} {isLast ? null : ', '} ); }) ); CreditList.propTypes = { credits: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, link: PropTypes.string })) }; const Switch = ({ onChange, value, ...props }) => ( ); })} ); Select.propTypes = { onChange: PropTypes.func, value: PropTypes.string, values: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, name: PropTypes.string })) }; const Tags = ({ manifest }) => ( {manifest.tags.includes('recommended') && ( {settingsTranslations.tagRecommended} )} {manifest.tags.includes('theme') && ( {settingsTranslations.tagTheme} )} {manifest.tags.includes('beta') && ( {settingsTranslations.tagBeta} )} {manifest.tags.includes('new') && ( {settingsTranslations.tagNew} )} {manifest.tags.includes('danger') && ( {settingsTranslations.tagDanger} )} ); 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 ( ); } } TextInput.propTypes = { onChange: PropTypes.func.isRequired, type: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }; const ColorInput = props => ( ); ColorInput.propTypes = { id: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, value: PropTypes.string.isRequired }; const ResetButton = ({ addonId, settingId, forTextInput }) => ( ); 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 = ( ); return (
{setting.type === 'boolean' && ( {label} SettingsStore.setAddonSetting(addonId, settingId, e.target.checked)} /> )} {setting.type === 'integer' && ( {label} SettingsStore.setAddonSetting(addonId, settingId, newValue)} /> )} {setting.type === 'color' && ( {label} SettingsStore.setAddonSetting(addonId, settingId, e.target.value)} /> )} {setting.type === 'select' && ( {label}
{settingsTranslations.addonFeedback}
{this.state.dirty && ( )}
{!this.state.loading && (
)}
); } } AddonSettingsComponent.propTypes = { onExportSettings: PropTypes.func }; export default AddonSettingsComponent;