const React = require('react'); const { connect } = require('react-redux'); const Setting = require('lib/models/Setting.js'); const { bridge } = require('electron').remote.require('./bridge'); const { themeStyle } = require('lib/theme'); const pathUtils = require('lib/path-utils.js'); const { _ } = require('lib/locale.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry'); const shared = require('lib/components/shared/config-shared.js'); const ConfigMenuBar = require('./ConfigMenuBar.min.js'); const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min'); const { ClipperConfigScreen } = require('./ClipperConfigScreen.min'); class ConfigScreenComponent extends React.Component { constructor() { super(); shared.init(this); this.state.selectedSectionName = 'general'; this.state.screenName = ''; this.checkSyncConfig_ = async () => { await shared.checkSyncConfig(this, this.state.settings); }; this.checkNextcloudAppButton_click = async () => { this.setState({ showNextcloudAppLog: true }); await shared.checkNextcloudApp(this, this.state.settings); }; this.showLogButton_click = () => { this.setState({ showNextcloudAppLog: true }); }; this.nextcloudAppHelpLink_click = () => { bridge().openExternal('https://joplinapp.org/nextcloud_app'); }; this.rowStyle_ = { marginBottom: 10, }; this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this); } UNSAFE_componentWillMount() { this.setState({ settings: this.props.settings }); } componentDidMount() { if (this.props.defaultSection) { this.setState({ selectedSectionName: this.props.defaultSection }, () => { this.switchSection(this.props.defaultSection); }); } } sectionByName(name) { const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings }); for (const section of sections) { if (section.name === name) return section; } throw new Error(`Invalid section name: ${name}`); } screenFromName(screenName) { if (screenName === 'encryption') return ; if (screenName === 'server') return ; throw new Error(`Invalid screen name: ${screenName}`); } switchSection(name) { const section = this.sectionByName(name); let screenName = ''; if (section.isScreen) { screenName = section.name; if (this.hasChanges()) { const ok = confirm(_('This will open a new screen. Save your current changes?')); if (ok) shared.saveSettings(this); } } this.setState({ selectedSectionName: section.name, screenName: screenName }); } configMenuBar_selectionChange(event) { this.switchSection(event.section.name); } keyValueToArray(kv) { const output = []; for (const k in kv) { if (!kv.hasOwnProperty(k)) continue; output.push({ key: k, label: kv[k], }); } return output; } renderSectionDescription(section) { const description = Setting.sectionDescription(section.name); if (!description) return null; const theme = themeStyle(this.props.theme); return (
{description}
); } sectionToComponent(key, section, settings, selected) { const theme = themeStyle(this.props.theme); const createSettingComponents = (advanced) => { const output = []; for (let i = 0; i < section.metadatas.length; i++) { const md = section.metadatas[i]; if (!!md.advanced !== advanced) continue; const settingComp = this.settingToComponent(md.key, settings[md.key]); output.push(settingComp); } return output; }; const settingComps = createSettingComponents(false); const advancedSettingComps = createSettingComponents(true); const sectionStyle = { marginTop: 20, marginBottom: 20, }; if (!selected) sectionStyle.display = 'none'; if (section.name === 'general') { sectionStyle.borderTopWidth = 0; } if (section.name === 'sync') { const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']); const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 }); if (syncTargetMd.supportsConfigCheck) { const messages = shared.checkSyncConfigMessages(this); const statusComp = !messages.length ? null : (
{messages[0]} {messages.length >= 1 ?

{messages[1]}

: null}
); settingComps.push(
{statusComp}
); } if (syncTargetMd.name === 'nextcloud') { const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']]; let status = _('Unknown'); let errorMessage = null; if (this.state.checkNextcloudAppResult === 'checking') { status = _('Checking...'); } else if (syncTarget) { if (syncTarget.uuid) status = _('OK'); if (syncTarget.error) { status = _('Error'); errorMessage = syncTarget.error; } } const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (

{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}

{errorMessage}
); const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : ( [{_('Show Log')}] ); const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' }); settingComps.push(
Beta: {_('Joplin Nextcloud App status:')} {status}    {showLogButton}       [{_('Help')}] {statusComp}
); } } let advancedSettingsButton = null; const advancedSettingsSectionStyle = { display: 'none' }; if (advancedSettingComps.length) { const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right'; const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 }); advancedSettingsButton = ; advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none'; } return (
{this.renderSectionDescription(section)}
{settingComps}
{advancedSettingsButton}
{advancedSettingComps}
); } settingToComponent(key, value) { const theme = themeStyle(this.props.theme); const output = null; const rowStyle = this.rowStyle_; const labelStyle = Object.assign({}, theme.textStyle, { display: 'inline-block', marginRight: 10, color: theme.color, }); const subLabel = Object.assign({}, labelStyle, { opacity: 0.7, marginBottom: Math.round(rowStyle.marginBottom * 0.7), }); const invisibleLabel = Object.assign({}, labelStyle, { opacity: 0, }); const checkboxLabelStyle = Object.assign({}, labelStyle, { marginLeft: 8, }); const controlStyle = { display: 'inline-block', color: theme.color, backgroundColor: theme.backgroundColor, }; const descriptionStyle = Object.assign({}, theme.textStyle, { color: theme.colorFaded, marginTop: 5, fontStyle: 'italic', maxWidth: '70em', }); const textInputBaseStyle = Object.assign({}, controlStyle, { border: '1px solid', padding: '4px 6px', borderColor: theme.dividerColor, borderRadius: 4, }); const updateSettingValue = (key, value) => { // console.info(key + ' = ' + value); return shared.updateSettingValue(this, key, value); }; // Component key needs to be key+value otherwise it doesn't update when the settings change. const md = Setting.settingMetadata(key); const descriptionText = Setting.keyDescription(key, 'desktop'); const descriptionComp = descriptionText ?
{descriptionText}
: null; if (md.isEnum) { const items = []; const settingOptions = md.options(); const array = this.keyValueToArray(settingOptions); for (let i = 0; i < array.length; i++) { const e = array[i]; items.push( ); } const selectStyle = Object.assign({}, controlStyle, { height: 22, borderColor: theme.dividerColor }); return (
{descriptionComp}
); } else if (md.type === Setting.TYPE_BOOL) { const onCheckboxClick = () => { updateSettingValue(key, !value); }; // Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes. // There's probably a better way to do this but can't figure it out. return (
{ onCheckboxClick(event); }} /> {descriptionComp}
); } else if (md.type === Setting.TYPE_STRING) { const inputStyle = Object.assign({}, textInputBaseStyle, { width: '50%', minWidth: '20em', }); const inputType = md.secure === true ? 'password' : 'text'; if (md.subType === 'file_path_and_args') { inputStyle.marginBottom = subLabel.marginBottom; const splitCmd = cmdString => { const path = pathUtils.extractExecutablePath(cmdString); const args = cmdString.substr(path.length + 1); return [pathUtils.unquotePath(path), args]; }; const joinCmd = cmdArray => { if (!cmdArray[0] && !cmdArray[1]) return ''; let cmdString = pathUtils.quotePath(cmdArray[0]); if (!cmdString) cmdString = '""'; if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`; return cmdString; }; const onPathChange = event => { const cmd = splitCmd(this.state.settings[key]); cmd[0] = event.target.value; updateSettingValue(key, joinCmd(cmd)); }; const onArgsChange = event => { const cmd = splitCmd(this.state.settings[key]); cmd[1] = event.target.value; updateSettingValue(key, joinCmd(cmd)); }; const browseButtonClick = () => { const paths = bridge().showOpenDialog(); if (!paths || !paths.length) return; const cmd = splitCmd(this.state.settings[key]); cmd[0] = paths[0]; updateSettingValue(key, joinCmd(cmd)); }; const cmd = splitCmd(this.state.settings[key]); return (
Path:
Arguments:
{ onPathChange(event); }} value={cmd[0]} />
{ onArgsChange(event); }} value={cmd[1]} />
{descriptionComp}
); } else { const onTextChange = event => { updateSettingValue(key, event.target.value); }; return (
{ onTextChange(event); }} /> {descriptionComp}
); } } else if (md.type === Setting.TYPE_INT) { const onNumChange = event => { updateSettingValue(key, event.target.value); }; const label = [md.label()]; if (md.unitLabel) label.push(`(${md.unitLabel()})`); const inputStyle = Object.assign({}, textInputBaseStyle); return (
{ onNumChange(event); }} min={md.minimum} max={md.maximum} step={md.step} /> {descriptionComp}
); } else if (md.type === Setting.TYPE_BUTTON) { const theme = themeStyle(this.props.theme); const buttonStyle = Object.assign({}, theme.buttonStyle, { display: 'inline-block', marginRight: 10, }); return (
{descriptionComp}
); } else { console.warn(`Type not implemented: ${key}`); } return output; } onApplyClick() { shared.saveSettings(this); } onSaveClick() { shared.saveSettings(this); this.props.dispatch({ type: 'NAV_BACK' }); } onCancelClick() { this.props.dispatch({ type: 'NAV_BACK' }); } hasChanges() { return !!this.state.changedSettingKeys.length; } render() { const theme = themeStyle(this.props.theme); const style = Object.assign( { backgroundColor: theme.backgroundColor, }, this.props.style, { overflow: 'hidden', display: 'flex', flexDirection: 'column', } ); const settings = this.state.settings; const containerStyle = Object.assign({}, theme.containerStyle, { padding: 10, paddingTop: 0, display: 'flex', flex: 1 }); const hasChanges = this.hasChanges(); const buttonStyle = Object.assign({}, theme.buttonStyle, { display: 'inline-block', marginRight: 10, }); const buttonStyleApprove = Object.assign({}, buttonStyle, { opacity: hasChanges ? 1 : theme.disabledOpacity, }); const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName); const buttonBarStyle = { display: 'flex', alignItems: 'center', padding: 10, borderTopWidth: 1, borderTopStyle: 'solid', borderTopColor: theme.dividerColor, }; const screenComp = this.state.screenName ?
{this.screenFromName(this.state.screenName)}
: null; if (screenComp) containerStyle.display = 'none'; const sections = shared.settingsSections({ device: 'desktop', settings }); return (
{screenComp}
{settingComps}
{ !screenComp && (
)}
); } } const mapStateToProps = state => { return { theme: state.settings.theme, settings: state.settings, locale: state.settings.locale, }; }; const ConfigScreen = connect(mapStateToProps)(ConfigScreenComponent); module.exports = { ConfigScreen };