diff --git a/app/lib/config.js b/app/lib/config.js index 1261bea0..0933ae44 100644 --- a/app/lib/config.js +++ b/app/lib/config.js @@ -23,7 +23,8 @@ const defaultSettings = memoize(() => { developerMode: false, cleanOnHide: true, skipDonateDialog: false, - lastShownDonateDialog: null + lastShownDonateDialog: null, + plugins: {}, } }) diff --git a/app/lib/plugins/index.js b/app/lib/plugins/index.js index 4cc42f2c..38b1a2cd 100644 --- a/app/lib/plugins/index.js +++ b/app/lib/plugins/index.js @@ -32,3 +32,4 @@ export const ensureFiles = () => { } export const client = npm(pluginsPath) +export { default as settings } from './settings' diff --git a/app/lib/plugins/settings/get.js b/app/lib/plugins/settings/get.js new file mode 100644 index 00000000..d1210774 --- /dev/null +++ b/app/lib/plugins/settings/get.js @@ -0,0 +1,3 @@ +import config from 'lib/config' + +export default (pluginName) => config.get('plugins')[pluginName] diff --git a/app/lib/plugins/settings/index.js b/app/lib/plugins/settings/index.js new file mode 100644 index 00000000..e89c363d --- /dev/null +++ b/app/lib/plugins/settings/index.js @@ -0,0 +1,4 @@ +import get from './get' +import validate from './validate' + +export default { get, validate } diff --git a/app/lib/plugins/settings/validate.js b/app/lib/plugins/settings/validate.js new file mode 100644 index 00000000..c58eaadd --- /dev/null +++ b/app/lib/plugins/settings/validate.js @@ -0,0 +1,24 @@ +import { every } from 'lodash/fp' + +const VALID_TYPES = new Set([ + 'array', + 'string', + 'number', + 'bool', + 'option', +]) + +const validSetting = ({ type, options }) => { + // General validation of settings + if (!type || !VALID_TYPES.has(type)) return false + + // Type-specific validations + if (type === 'option') return Array.isArray(options) && options.length + + return true +} + +export default ({ settings }) => { + if (!settings) return true + return every(validSetting)(settings) +} diff --git a/app/main/actions/search.js b/app/main/actions/search.js index e9b685c7..31c21e47 100644 --- a/app/main/actions/search.js +++ b/app/main/actions/search.js @@ -2,6 +2,7 @@ import plugins from '../plugins/' import config from 'lib/config' import { shell, clipboard, remote } from 'electron' import store from '../store' +import { settings as pluginSettings } from 'lib/plugins' import { UPDATE_TERM, @@ -44,7 +45,8 @@ const eachPlugin = (term, display) => { term, hide: (id) => store.dispatch(hideElement(`${name}-${id}`)), update: (id, result) => store.dispatch(updateElement(`${name}-${id}`, result)), - display: (payload) => display(name, payload) + display: (payload) => display(name, payload), + settings: pluginSettings.get(name), }) } catch (error) { // Do not fail on plugin errors, just log them to console diff --git a/app/main/components/Form/Checkbox.js b/app/main/components/Form/Checkbox.js new file mode 100644 index 00000000..44b264c4 --- /dev/null +++ b/app/main/components/Form/Checkbox.js @@ -0,0 +1,28 @@ +import React, { PropTypes } from 'react' +import styles from './styles.css' + +const Checkbox = ({ label, value, onChange, description }) => ( +
+
+ +
{description}
+
+
+) + +Checkbox.propTypes = { + label: PropTypes.string, + value: PropTypes.bool, + onChange: PropTypes.func.isRequired, + description: PropTypes.string, +} + +export default Checkbox diff --git a/app/main/components/Form/Select.js b/app/main/components/Form/Select.js new file mode 100644 index 00000000..050367b9 --- /dev/null +++ b/app/main/components/Form/Select.js @@ -0,0 +1,41 @@ +import React, { PropTypes } from 'react' +import ReactSelect, { Creatable } from 'react-select' +import Wrapper from './Wrapper' + +const Select = ({ label, value, onChange, description, options, multi, clearable, creatable }) => { + const Component = creatable ? Creatable : ReactSelect + return ( + + { + if (!newValue) { + return newValue + } + const changedValue = multi ? newValue.map(val => val.value) : newValue.value + onChange(changedValue) + }} + /> + + ) +} + +Select.propTypes = { + label: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.array, + ]), + onChange: PropTypes.func.isRequired, + description: PropTypes.string, + options: PropTypes.array.isRequired, + multi: PropTypes.bool, + clearable: PropTypes.bool, + creatable: PropTypes.bool +} + +export default Select diff --git a/app/main/components/Form/Text.js b/app/main/components/Form/Text.js new file mode 100644 index 00000000..c16b4e12 --- /dev/null +++ b/app/main/components/Form/Text.js @@ -0,0 +1,27 @@ +import React, { PropTypes } from 'react' +import Wrapper from './Wrapper' +import styles from './styles.css' + +const Input = ({ label, value, onChange, description, type }) => ( + + onChange(target.value)} + /> + +) + +Input.propTypes = { + label: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + onChange: PropTypes.func.isRequired, + description: PropTypes.string, + type: PropTypes.string.isRequired, +} + +export default Input diff --git a/app/main/components/Form/Wrapper.js b/app/main/components/Form/Wrapper.js new file mode 100644 index 00000000..5bd7adca --- /dev/null +++ b/app/main/components/Form/Wrapper.js @@ -0,0 +1,20 @@ +import React, { PropTypes } from 'react' +import styles from './styles.css' + +const Wrapper = ({ label, description, children }) => ( +
+ {label && } +
+ {children} +
{description}
+
+
+) + +Wrapper.propTypes = { + label: PropTypes.string, + description: PropTypes.string, + children: PropTypes.any +} + +export default Wrapper diff --git a/app/main/components/Form/index.js b/app/main/components/Form/index.js new file mode 100644 index 00000000..8863cb96 --- /dev/null +++ b/app/main/components/Form/index.js @@ -0,0 +1,7 @@ +import Checkbox from './Checkbox' +import Text from './Text' +import Select from './Select' + +export { + Checkbox, Text, Select +} diff --git a/app/main/components/Form/styles.css b/app/main/components/Form/styles.css new file mode 100644 index 00000000..bfae693d --- /dev/null +++ b/app/main/components/Form/styles.css @@ -0,0 +1,48 @@ +.item { + display: flex; + flex-direction: row; + width: 250px; + margin-top: 20px; + &:first-child { + margin-top: 0; + } +} + +.itemValue { + display: flex; + flex-direction: column; + flex-grow: 2; +} + +.itemValueWithoutLabel { + composes: itemValue; + margin-left: 75px; +} + +.itemNotice{ + color: var(--secondary-font-color); + font-size: .8em; + margin-top: 5px; +} + +.label { + margin-right: 15px; + margin-top: 8px; + min-width: 60px; + max-width: 60px; +} + +.input { + font-size: 16px; + line-height: 34px; + padding: 0 10px; + box-sizing: border-box; + width: 100%; + border-color: #d9d9d9 #ccc #b3b3b3; + border-radius: 4px; + border: 1px solid #ccc; +} + +.checkbox { + margin-right: 5px; +} diff --git a/app/main/plugins/core/cerebro/plugins/Preview/FormItem.js b/app/main/plugins/core/cerebro/plugins/Preview/FormItem.js new file mode 100644 index 00000000..de9197db --- /dev/null +++ b/app/main/plugins/core/cerebro/plugins/Preview/FormItem.js @@ -0,0 +1,23 @@ +import React, { PropTypes } from 'react' +import { Select, Text, Checkbox } from 'main/components/Form' + +const components = { + bool: Checkbox, + option: Select, + array: Select, +} + +const FormItem = ({ type, ...props }) => { + const Component = components[type] || Text + + return ( + + ) +} + +FormItem.propTypes = { + value: PropTypes.any, + type: PropTypes.string.isRequired, +} + +export default FormItem diff --git a/app/main/plugins/core/cerebro/plugins/Preview/Settings.js b/app/main/plugins/core/cerebro/plugins/Preview/Settings.js new file mode 100644 index 00000000..a3dd3e51 --- /dev/null +++ b/app/main/plugins/core/cerebro/plugins/Preview/Settings.js @@ -0,0 +1,59 @@ +import React, { PropTypes, Component } from 'react' +import config from 'lib/config' +import FormItem from './FormItem' +import styles from './styles.css' + +export default class Settings extends Component { + constructor(props) { + super(props) + this.state = { + values: config.get('plugins')[props.name], + } + this.renderSetting = this.renderSetting.bind(this) + this.changeSetting = this.changeSetting.bind(this) + } + + changeSetting(plugin, label, value) { + const values = { + ...this.state.values, + [label]: value, + } + + this.setState({ values }) + config.set('plugins', { + ...config.get('plugins'), + [this.props.name]: values, + }) + } + + renderSetting(key) { + const setting = this.props.settings[key] + const { defaultValue, label, ...restProps } = setting + const value = key in this.state.values ? this.state.values[key] : defaultValue + + return ( + this.changeSetting(this.props.name, key, newValue)} + {...restProps} + /> + ) + } + + render() { + return ( +
+ { + Object.keys(this.props.settings).map(this.renderSetting) + } +
+ ) + } +} + +Settings.propTypes = { + name: PropTypes.string.isRequired, + settings: PropTypes.object.isRequired, +} diff --git a/app/main/plugins/core/cerebro/plugins/Preview/index.js b/app/main/plugins/core/cerebro/plugins/Preview/index.js index 10a85cdb..bb2a27f1 100644 --- a/app/main/plugins/core/cerebro/plugins/Preview/index.js +++ b/app/main/plugins/core/cerebro/plugins/Preview/index.js @@ -3,12 +3,14 @@ import Preload from 'main/components/Preload' import KeyboardNav from 'main/components/KeyboardNav' import KeyboardNavItem from 'main/components/KeyboardNavItem' import ActionButton from './ActionButton.js' +import Settings from './Settings' import getReadme from '../getReadme' import ReactMarkdown from 'react-markdown' import styles from './styles.css' import trackEvent from 'lib/trackEvent' import * as format from '../format' import { client } from 'lib/plugins' +import plugins from 'main/plugins' const isRelative = (src) => !src.match(/^(https?:|data:)/) const urlTransform = (repo, src) => { @@ -24,6 +26,7 @@ class Preview extends Component { this.onComplete = this.onComplete.bind(this) this.state = { showDescription: false, + showSettings: false, } } @@ -71,13 +74,23 @@ class Preview extends Component { isUpdateAvailable } = this.props const githubRepo = repo && repo.match(/^.+github.com\/([^\/]+\/[^\/]+).*?/) - const runningAction = this.state.runningAction + const { runningAction, showSettings } = this.state + const settings = plugins[name] ? plugins[name].settings : null return (

{format.name(name)} ({version})

{format.description(description)}

+ { + settings && + this.setState({ showSettings: !this.state.showSettings })} + > + Settings + + } + {showSettings && } { !isInstalled && this.setState({ showDescription: true })} + onSelect={() => this.setState({ showDescription: !this.state.showDescription })} > Details @@ -124,6 +137,7 @@ class Preview extends Component { Preview.propTypes = { name: PropTypes.string.isRequired, + settings: PropTypes.object, version: PropTypes.string.isRequired, description: PropTypes.string, repo: PropTypes.string, diff --git a/app/main/plugins/core/cerebro/plugins/Preview/styles.css b/app/main/plugins/core/cerebro/plugins/Preview/styles.css index e7facb14..da997e14 100644 --- a/app/main/plugins/core/cerebro/plugins/Preview/styles.css +++ b/app/main/plugins/core/cerebro/plugins/Preview/styles.css @@ -94,3 +94,7 @@ margin-top: 0.25em; } } + +.settingsWrapper { + margin: 15px 0; +} diff --git a/app/main/plugins/core/cerebro/settings/Settings/CountrySelect/index.js b/app/main/plugins/core/cerebro/settings/Settings/CountrySelect/index.js deleted file mode 100644 index ff2bb713..00000000 --- a/app/main/plugins/core/cerebro/settings/Settings/CountrySelect/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import countries from './countries' -import Select from 'react-select' - -export default (props) => ( - -
- -
- this.changeConfig('hotkey', key)} - /> -
- Type your global shortcut for Cerebro in this input. -
-
-
- -
- -
- this.changeConfig('country', value)} - /> -
- Choose your country so Cerebro can better choose currency, language, etc. -
-
-
- -
- -
- this.changeConfig('showInTray', target.checked)} - className={styles.checkbox} - /> - Show in menu bar - -
-
- -
-
- -
-
- -
-
- -
-
+ + this.changeConfig('hotkey', key)} + /> + + this.changeConfig('theme', value)} + /> + this.changeConfig('showInTray', value)} + /> + this.changeConfig('developerMode', value)} + /> + this.changeConfig('cleanOnHide', value)} + />
) } diff --git a/app/main/plugins/core/cerebro/settings/Settings/styles.css b/app/main/plugins/core/cerebro/settings/Settings/styles.css index 04b9741b..30ad0f95 100644 --- a/app/main/plugins/core/cerebro/settings/Settings/styles.css +++ b/app/main/plugins/core/cerebro/settings/Settings/styles.css @@ -5,32 +5,6 @@ align-items: center; } -.item { - display: flex; - flex-direction: row; - width: 250px; - margin-top: 20px; - &:first-child { - margin-top: 0; - } -} - -.itemValue { - display: flex; - flex-direction: column; - flex-grow: 2; -} -.itemValueWithoutLabel { - composes: itemValue; - margin-left: 75px; -} - -.itemNotice{ - color: var(--secondary-font-color); - font-size: .8em; - margin-top: 5px; -} - .label { margin-right: 15px; margin-top: 8px; @@ -41,3 +15,27 @@ .checkbox { margin-right: 5px; } + +.settingItem { + padding: 20px; + box-sizing: border-box; + width: 100%; + border-color: #d9d9d9 #ccc #b3b3b3; + border-top: 1px solid #ccc; + margin-top: 16px; +} + +.header { + font-weight: bold; +} + +.input { + font-size: 16px; + line-height: 34px; + padding: 0 10px; + box-sizing: border-box; + width: 100%; + border-color: #d9d9d9 #ccc #b3b3b3; + border-radius: 4px; + border: 1px solid #ccc; +} diff --git a/app/main/plugins/externalPlugins.js b/app/main/plugins/externalPlugins.js index 4beaf87f..94513301 100644 --- a/app/main/plugins/externalPlugins.js +++ b/app/main/plugins/externalPlugins.js @@ -1,7 +1,7 @@ import debounce from 'lodash/debounce' import chokidar from 'chokidar' import path from 'path' -import { modulesDirectory, ensureFiles } from 'lib/plugins' +import { modulesDirectory, ensureFiles, settings } from 'lib/plugins' const requirePlugin = (pluginPath) => { try { @@ -55,6 +55,12 @@ pluginsWatcher.on('addDir', (pluginPath) => { console.groupEnd() return } + if (!settings.validate(plugin)) { + console.log('Invalid plugins settings') + console.groupEnd() + return + } + console.log('Loaded.') const requirePath = window.require.resolve(pluginPath) const watcher = chokidar.watch(requirePath, { depth: 0 }) diff --git a/test/actions/search.spec.js b/test/actions/search.spec.js index 54213b5d..c02c7f61 100644 --- a/test/actions/search.spec.js +++ b/test/actions/search.spec.js @@ -17,10 +17,14 @@ const pluginsMock = { 'test-plugin': testPlugin } + const actions = searchInjector({ electron: {}, '../plugins/': pluginsMock, - 'lib/config': {} + 'lib/config': {}, + 'lib/plugins': { + get: () => undefined + } }) describe('reset', () => {