import * as React from 'react'; import SideBar from './SideBar'; import ButtonBar from './ButtonBar'; import Button, { ButtonLevel } from '../Button/Button'; import { _ } from 'lib/locale'; const { connect } = require('react-redux'); const Setting = require('lib/models/Setting').default; const { themeStyle } = require('lib/theme'); const pathUtils = require('lib/path-utils.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry'); const shared = require('lib/components/shared/config-shared.js'); const bridge = require('electron').remote.require('./bridge').default; const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min'); const { ClipperConfigScreen } = require('../ClipperConfigScreen.min'); const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); class ConfigScreenComponent extends React.Component { rowStyle_:any = null; constructor(props:any) { super(props); shared.init(this); this.state = { selectedSectionName: 'general', screenName: '', changedSettingKeys: [], }; this.rowStyle_ = { marginBottom: 10, }; this.sideBar_selectionChange = this.sideBar_selectionChange.bind(this); this.checkSyncConfig_ = this.checkSyncConfig_.bind(this); this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this); this.showLogButton_click = this.showLogButton_click.bind(this); this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this); this.onCancelClick = this.onCancelClick.bind(this); this.onSaveClick = this.onSaveClick.bind(this); this.onApplyClick = this.onApplyClick.bind(this); } async checkSyncConfig_() { await shared.checkSyncConfig(this, this.state.settings); } async checkNextcloudAppButton_click() { this.setState({ showNextcloudAppLog: true }); await shared.checkNextcloudApp(this, this.state.settings); } showLogButton_click() { this.setState({ showNextcloudAppLog: true }); } nextcloudAppHelpLink_click() { bridge().openExternal('https://joplinapp.org/nextcloud_app'); } 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:string) { 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:string) { if (screenName === 'encryption') return ; if (screenName === 'server') return ; if (screenName === 'keymap') return ; throw new Error(`Invalid screen name: ${screenName}`); } switchSection(name:string) { 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 }); } sideBar_selectionChange(event:any) { this.switchSection(event.section.name); } keyValueToArray(kv:any) { const output = []; for (const k in kv) { if (!kv.hasOwnProperty(k)) continue; output.push({ key: k, label: kv[k], }); } return output; } renderSectionDescription(section:any) { const description = Setting.sectionDescription(section.name); if (!description) return null; const theme = themeStyle(this.props.themeId); return (
{description}
); } sectionToComponent(key:string, section:any, settings:any, selected:boolean) { const theme = themeStyle(this.props.themeId); const createSettingComponents = (advanced:boolean) => { 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:any = { marginTop: 20, marginBottom: 20, maxWidth: 640, }; 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(
); } 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}   
); } } 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:string, value:any) { const theme = themeStyle(this.props.themeId); const output:any = null; const rowStyle = { marginBottom: theme.mainPadding, }; const labelStyle = Object.assign({}, theme.textStyle, { display: 'block', color: theme.color, fontSize: theme.fontSize * 1.083333, fontWeight: 500, marginBottom: theme.mainPadding / 4, }); const subLabel = Object.assign({}, labelStyle, { display: 'block', opacity: 0.7, marginBottom: labelStyle.marginBottom, }); const checkboxLabelStyle = Object.assign({}, labelStyle, { marginLeft: 8, display: 'inline', backgroundColor: 'transparent', }); const controlStyle = { display: 'inline-block', color: theme.color, fontFamily: theme.fontFamily, backgroundColor: theme.backgroundColor, }; const descriptionStyle = Object.assign({}, theme.textStyle, { color: theme.colorFaded, marginTop: 5, fontStyle: 'italic', maxWidth: '70em', }); const textInputBaseStyle = Object.assign({}, controlStyle, { fontFamily: theme.fontFamily, border: '1px solid', padding: '4px 6px', boxSizing: 'border-box', borderColor: theme.borderColor4, borderRadius: 3, paddingLeft: 6, paddingRight: 6, paddingTop: 4, paddingBottom: 4, }); const updateSettingValue = (key:string, value:any) => { // 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, { paddingLeft: 6, paddingRight: 6, paddingTop: 4, paddingBottom: 4, borderColor: theme.borderColor4, borderRadius: 3, }); return (
{descriptionComp}
); } else if (md.type === Setting.TYPE_BOOL) { const onCheckboxClick = () => { updateSettingValue(key, !value); }; const checkboxSize = theme.fontSize * 1.1666666666666; // 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(); }} style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }} />
{descriptionComp}
); } else if (md.type === Setting.TYPE_STRING) { const inputStyle:any = 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:string) => { const path = pathUtils.extractExecutablePath(cmdString); const args = cmdString.substr(path.length + 1); return [pathUtils.unquotePath(path), args]; }; const joinCmd = (cmdArray:string[]) => { 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:any) => { const cmd = splitCmd(this.state.settings[key]); cmd[0] = event.target.value; updateSettingValue(key, joinCmd(cmd)); }; const onArgsChange = (event:any) => { 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:
{ onPathChange(event); }} value={cmd[0]} />
Arguments:
{ onArgsChange(event); }} value={cmd[1]} />
{descriptionComp}
); } else { const onTextChange = (event:any) => { updateSettingValue(key, event.target.value); }; return (
{ onTextChange(event); }} />
{descriptionComp}
); } } else if (md.type === Setting.TYPE_INT) { const onNumChange = (event:any) => { updateSettingValue(key, event.target.value); }; const label = [md.label()]; if (md.unitLabel) label.push(`(${md.unitLabel()})`); const inputStyle:any = Object.assign({}, textInputBaseStyle); return (
{ onNumChange(event); }} min={md.minimum} max={md.maximum} step={md.step} /> {descriptionComp}
); } else if (md.type === Setting.TYPE_BUTTON) { return (
); } 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.themeId); const style = Object.assign({}, this.props.style, { overflow: 'hidden', display: 'flex', flexDirection: 'column', backgroundColor: theme.backgroundColor3, } ); const settings = this.state.settings; const containerStyle = { overflow: 'auto', padding: theme.configScreenPadding, paddingTop: 0, display: 'flex', flex: 1, }; const hasChanges = this.hasChanges(); const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName); // screenComp is a custom config screen, such as the encryption config screen or keymap config screen. // These screens handle their own loading/saving of settings and have bespoke rendering. // When screenComp is null, it means we are viewing the regular settings. 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}
); } } const mapStateToProps = (state:any) => { return { themeId: state.settings.theme, settings: state.settings, locale: state.settings.locale, }; }; export default connect(mapStateToProps)(ConfigScreenComponent);