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) => (
-
-)
diff --git a/app/main/plugins/core/cerebro/settings/Settings/Hotkey/index.js b/app/main/plugins/core/cerebro/settings/Settings/Hotkey.js
similarity index 97%
rename from app/main/plugins/core/cerebro/settings/Settings/Hotkey/index.js
rename to app/main/plugins/core/cerebro/settings/Settings/Hotkey.js
index e5bff564..1a8a2d61 100644
--- a/app/main/plugins/core/cerebro/settings/Settings/Hotkey/index.js
+++ b/app/main/plugins/core/cerebro/settings/Settings/Hotkey.js
@@ -124,9 +124,9 @@ class Hotkey extends Component {
const { hotkey } = this.props
const keys = hotkey.split('+').map(keyToSign).join(osKeyDelimiter)
return (
-
+
-
-
-
-
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('hotkey', key)}
+ />
+
+
)
}
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', () => {