import * as React from 'react'; import Sidebar from './Sidebar'; import ButtonBar from './ButtonBar'; import Button, { ButtonLevel } from '../Button/Button'; import { _ } from '@joplin/lib/locale'; import bridge from '../../services/bridge'; import Setting, { AppType, SettingValueType, SyncStartupOperation } from '@joplin/lib/models/Setting'; import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen'; import { reg } from '@joplin/lib/registry'; const { connect } = require('react-redux'); import { themeStyle } from '@joplin/lib/theme'; import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; import * as shared from '@joplin/lib/components/shared/config/config-shared.js'; import ClipperConfigScreen from '../ClipperConfigScreen'; import restart from '../../services/restart'; import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen'; import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton'; import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning'; import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink'; const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent'; interface Font { family: string; } declare global { interface Window { queryLocalFonts(): Promise; openChangelogLink: ()=> void; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied class ConfigScreenComponent extends React.Component { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private rowStyle_: any = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public constructor(props: any) { super(props); shared.init(reg); this.state = { ...shared.defaultScreenState, selectedSectionName: 'general', screenName: '', changedSettingKeys: [], needRestart: false, fonts: [], }; this.rowStyle_ = { marginBottom: 10, }; this.sidebar_selectionChange = this.sidebar_selectionChange.bind(this); this.checkSyncConfig_ = this.checkSyncConfig_.bind(this); this.onCancelClick = this.onCancelClick.bind(this); this.onSaveClick = this.onSaveClick.bind(this); this.onApplyClick = this.onApplyClick.bind(this); this.handleSettingButton = this.handleSettingButton.bind(this); } private async checkSyncConfig_() { if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) { const isAuthenticated = await reg.syncTarget().isAuthenticated(); if (!isAuthenticated) { return this.props.dispatch({ type: 'NAV_GO', routeName: 'JoplinCloudLogin', }); } } await shared.checkSyncConfig(this, this.state.settings); } public UNSAFE_componentWillMount() { this.setState({ settings: this.props.settings }); } public async componentDidMount() { if (this.props.defaultSection) { this.setState({ selectedSectionName: this.props.defaultSection }, () => { void this.switchSection(this.props.defaultSection); }); } const fonts = (await window.queryLocalFonts()).map((font: Font) => font.family); const uniqueFonts = [...new Set(fonts)]; this.setState({ fonts: uniqueFonts }); } private async handleSettingButton(key: string) { if (key === 'sync.clearLocalSyncStateButton') { if (!confirm('This cannot be undone. Do you want to continue?')) return; Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalSyncState); await Setting.saveAll(); await restart(); } else if (key === 'sync.clearLocalDataButton') { if (!confirm('This cannot be undone. Do you want to continue?')) return; Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData); await Setting.saveAll(); await restart(); } else if (key === 'ocr.clearLanguageDataCacheButton') { if (!confirm(this.restartMessage())) return; Setting.setValue('ocr.clearLanguageDataCache', true); await restart(); } else if (key === 'sync.openSyncWizard') { this.props.dispatch({ type: 'DIALOG_OPEN', name: 'syncWizard', }); } else { throw new Error(`Unhandled key: ${key}`); } } public sectionByName(name: string) { const sections = shared.settingsSections({ device: AppType.Desktop, settings: this.state.settings }); for (const section of sections) { if (section.name === name) return section; } throw new Error(`Invalid section name: ${name}`); } public screenFromName(screenName: string) { if (screenName === 'encryption') return ; if (screenName === 'server') return ; if (screenName === 'keymap') return ; if (screenName === 'joplinCloud') return ; throw new Error(`Invalid screen name: ${screenName}`); } public async 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) { await shared.saveSettings(this); } } } this.setState({ selectedSectionName: section.name, screenName: screenName }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private sidebar_selectionChange(event: any) { void this.switchSection(event.section.name); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public renderSectionDescription(section: any) { const description = Setting.sectionDescription(section.name, AppType.Desktop); if (!description) return null; const theme = themeStyle(this.props.themeId); return (
{description}
); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public 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); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const sectionWidths: Record = { plugins: '100%', }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const sectionStyle: any = { marginTop: 20, marginBottom: 20, maxWidth: sectionWidths[section.name] ? sectionWidths[section.name] : 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 = { ...theme.textStyle, marginTop: 10 }; const warningStyle = { ...theme.textStyle, color: theme.colorWarn }; // Don't show the missing password warning if the user just changed the sync target (but hasn't // saved yet). const matchesSavedTarget = settings['sync.target'] === this.props.settings['sync.target']; if (matchesSavedTarget && shouldShowMissingPasswordWarning(settings['sync.target'], settings)) { settingComps.push(

{_('%s: Missing password.', _('Warning'))} {' '}

, ); } if (syncTargetMd.supportsConfigCheck) { const messages = shared.checkSyncConfigMessages(this); const statusComp = !messages.length ? null : (
{messages[0]} {messages.length >= 1 ?

{messages[1]}

: null}
); if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) { const goToJoplinCloudLogin = () => { this.props.dispatch({ type: 'NAV_GO', routeName: 'JoplinCloudLogin', }); }; settingComps.push(
, ); } settingComps.push(
, ); } } let advancedSettingsButton = null; const advancedSettingsSectionStyle = { display: 'none' }; const advancedSettingsGroupId = `advanced_settings_${key}`; if (advancedSettingComps.length) { advancedSettingsButton = ( shared.advancedSettingsButton_click(this)} advancedSettingsVisible={this.state.showAdvancedSettings} aria-controls={advancedSettingsGroupId} /> ); advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none'; } return (
{this.renderSectionDescription(section)}
{settingComps}
{advancedSettingsButton}
{advancedSettingComps}
); } private onUpdateSettingValue = ({ key, value }: UpdateSettingValueEvent) => { const md = Setting.settingMetadata(key); if (md.needRestart) { this.setState({ needRestart: true }); } shared.updateSettingValue(this, key, value); }; public settingToComponent(key: T, value: SettingValueType) { return ( ); } private restartMessage() { return _('The application must be restarted for these changes to take effect.'); } private async restartApp() { await Setting.saveAll(); await restart(); } private async checkNeedRestart() { if (this.state.needRestart) { const doItNow = await bridge().showConfirmMessageBox(this.restartMessage(), { buttons: [_('Do it now'), _('Later')], }); if (doItNow) await this.restartApp(); } } public async onApplyClick() { const done = await shared.saveSettings(this); if (!done) return; await this.checkNeedRestart(); } public async onSaveClick() { const done = await shared.saveSettings(this); if (!done) return; await this.checkNeedRestart(); this.props.dispatch({ type: 'NAV_BACK' }); } public onCancelClick() { this.props.dispatch({ type: 'NAV_BACK' }); } public hasChanges() { return !!this.state.changedSettingKeys.length; } public render() { const theme = themeStyle(this.props.themeId); const style = { ...this.props.style, overflow: 'hidden', display: 'flex', flexDirection: 'column', backgroundColor: theme.backgroundColor3, }; const settings = this.state.settings; const containerStyle: React.CSSProperties = { overflow: 'auto', padding: theme.configScreenPadding, paddingTop: 0, display: 'flex', flex: 1, }; const hasChanges = this.hasChanges(); const settingComps = shared.settingsToComponents2(this, AppType.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: AppType.Desktop, settings }); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const needRestartComp: any = this.state.needRestart ? ( ) : null; const rightStyle = { ...style, flex: 1 }; delete style.width; const tabComponents: React.ReactNode[] = []; for (const section of sections) { const sectionId = `setting-section-${section.name}`; let content = null; const visible = section.name === this.state.selectedSectionName; if (visible) { content = ( <> {screenComp}
{settingComps}
); } tabComponents.push( , ); } return (
{needRestartComp} {tabComponents}
); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const mapStateToProps = (state: any) => { return { themeId: state.settings.theme, settings: state.settings, locale: state.settings.locale, }; }; export default connect(mapStateToProps)(ConfigScreenComponent);