/** * Copyright (C) 2021-2023 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 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 TWFancyCheckbox from '../../components/tw-fancy-checkbox/checkbox.jsx'; import styles from './settings.css'; import {detectTheme} from '../../lib/themes/themePersistance.js'; import {applyGuiColors} from '../../lib/themes/guiHelpers.js'; import {APP_NAME, FEEDBACK_URL} from '../../lib/brand.js'; 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} - ${APP_NAME}`; const theme = detectTheme(); applyGuiColors(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 getAllTags = () => { const tags = new Set(); for (const manifest of Object.values(supportedAddons)) { for (const tag of manifest.tags) { tags.add(tag); } } return Array.from(tags).sort(); }; const allTags = getAllTags(); const getInitialSearch = () => { const hash = location.hash.substring(1); // If the query is an addon ID, it's a better user experience to show the name of the addon // in the search bar instead of a ID they won't understand. if (Object.prototype.hasOwnProperty.call(importedAddons, hash)) { const manifest = importedAddons[hash]; return addonTranslations[`${hash}/@name`] || manifest.name; } return hash; }; const clearHash = () => { // Don't want to insert unnecssary history entry // location.hash = ''; leaves a # in the URL if (location.hash !== '') { history.replaceState(null, null, `${location.pathname}${location.search}`); } }; 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 TagFilter = ({tags, selectedTags, onTagToggle, onClearAll}) => { if (tags.length === 0) return null; return (
{settingsTranslations.filterByTags || 'Filter by tags:'}
{tags.map(tag => ( ))} {selectedTags.size > 0 && ( )}
); }; TagFilter.propTypes = { tags: PropTypes.arrayOf(PropTypes.string).isRequired, selectedTags: PropTypes.instanceOf(Set).isRequired, onTagToggle: PropTypes.func.isRequired, onClearAll: PropTypes.func.isRequired }; 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' || setting.type === 'positive_integer') && ( {label} SettingsStore.setAddonSetting(addonId, settingId, newValue)} /> )} {(setting.type === 'string' || setting.type === 'untranslated') && ( {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;