1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Resolves #9294: Implement settings search (#9320)

This commit is contained in:
Henry Heino 2023-11-16 04:17:03 -08:00 committed by GitHub
parent f9a1ab4d40
commit 18e86a7ba3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 202 additions and 104 deletions

View File

@ -70,6 +70,7 @@ interface ScreenHeaderProps {
onRedoButtonPress: OnPressCallback; onRedoButtonPress: OnPressCallback;
onSaveButtonPress: OnPressCallback; onSaveButtonPress: OnPressCallback;
sortButton_press?: OnPressCallback; sortButton_press?: OnPressCallback;
onSearchButtonPress?: OnPressCallback;
showSideMenuButton?: boolean; showSideMenuButton?: boolean;
showSearchButton?: boolean; showSearchButton?: boolean;
@ -242,8 +243,12 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
} }
private searchButton_press() { private searchButton_press() {
if (this.props.onSearchButtonPress) {
this.props.onSearchButtonPress();
} else {
void NavService.go('Search'); void NavService.go('Search');
} }
}
private async duplicateButton_press() { private async duplicateButton_press() {
const noteIds = this.props.selectedNoteIds; const noteIds = this.props.selectedNoteIds;

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native'; import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
import Setting, { AppType } from '@joplin/lib/models/Setting'; import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService'; import NavService from '@joplin/lib/services/NavService';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
import checkPermissions from '../../../utils/checkPermissions'; import checkPermissions from '../../../utils/checkPermissions';
@ -18,21 +18,25 @@ import * as shared from '@joplin/lib/components/shared/config/config-shared';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate'; import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles'; import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
import NoteExportButton from './NoteExportSection/NoteExportButton'; import NoteExportButton, { exportButtonDescription, exportButtonTitle } from './NoteExportSection/NoteExportButton';
import SettingsButton from './SettingsButton'; import SettingsButton from './SettingsButton';
import Clipboard from '@react-native-community/clipboard'; import Clipboard from '@react-native-community/clipboard';
import { ReactNode } from 'react'; import { ReactElement, ReactNode } from 'react';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import SectionHeader from './SectionHeader'; import SectionHeader from './SectionHeader';
import ExportProfileButton from './NoteExportSection/ExportProfileButton'; import ExportProfileButton, { exportProfileButtonTitle } from './NoteExportSection/ExportProfileButton';
import SettingComponent from './SettingComponent'; import SettingComponent from './SettingComponent';
import ExportDebugReportButton from './NoteExportSection/ExportDebugReportButton'; import ExportDebugReportButton, { exportDebugReportTitle } from './NoteExportSection/ExportDebugReportButton';
import SectionSelector from './SectionSelector'; import SectionSelector from './SectionSelector';
import { TextInput } from 'react-native-paper';
interface ConfigScreenState { interface ConfigScreenState {
settings: any; settings: any;
changedSettingKeys: string[]; changedSettingKeys: string[];
searchQuery: string;
searching: boolean;
fixingSearchIndex: boolean; fixingSearchIndex: boolean;
checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null; checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null;
showAdvancedSettings: boolean; showAdvancedSettings: boolean;
@ -66,6 +70,8 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
selectedSectionName: null, selectedSectionName: null,
fixingSearchIndex: false, fixingSearchIndex: false,
sidebarWidth: 100, sidebarWidth: 100,
searchQuery: '',
searching: false,
}; };
this.scrollViewRef_ = React.createRef<ScrollView>(); this.scrollViewRef_ = React.createRef<ScrollView>();
@ -128,6 +134,21 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
void NavService.go('Log'); void NavService.go('Log');
}; };
private setShowSearch_(searching: boolean) {
if (searching !== this.state.searching) {
this.setState({ searching });
AccessibilityInfo.announceForAccessibility(searching ? _('Search shown') : _('Search hidden'));
}
}
private onSearchButtonPress_ = () => {
this.setShowSearch_(!this.state.searching);
};
private onSearchUpdate_ = (newQuery: string) => {
this.setState({ searchQuery: newQuery });
};
private updateSidebarWidth = () => { private updateSidebarWidth = () => {
const windowWidth = Dimensions.get('window').width; const windowWidth = Dimensions.get('window').width;
@ -150,10 +171,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
return this.state.sidebarWidth > windowWidth / 2; return this.state.sidebarWidth > windowWidth / 2;
} }
private switchSectionPress_ = (section: string) => { private onJumpToSection_ = (section: string) => {
const label = Setting.sectionNameToLabel(section); const label = Setting.sectionNameToLabel(section);
AccessibilityInfo.announceForAccessibility(_('Opening section %s', label)); AccessibilityInfo.announceForAccessibility(_('Opening section %s', label));
this.setState({ selectedSectionName: section }); this.setState({
selectedSectionName: section,
searching: false,
});
}; };
private showSectionNavigation_ = () => { private showSectionNavigation_ = () => {
@ -245,6 +269,12 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
await BackButtonService.back(); await BackButtonService.back();
}; };
// Cancel search on back
if (this.state.searching) {
this.setShowSearch_(false);
return true;
}
// Show navigation when pressing "back" (unless always visible). // Show navigation when pressing "back" (unless always visible).
if (this.state.selectedSectionName && this.navigationFillsScreen()) { if (this.state.selectedSectionName && this.navigationFillsScreen()) {
this.showSectionNavigation_(); this.showSectionNavigation_();
@ -298,10 +328,73 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
); );
} }
public sectionToComponent(key: string, section: any, settings: any, isSelected: boolean) { public sectionToComponent(key: string, section: SettingMetadataSection, settings: any, isSelected: boolean) {
const settingComps = []; const settingComps: ReactElement[] = [];
const headerTitle = Setting.sectionNameToLabel(section.name);
const matchesSearchQuery = (relatedText: string|string[]) => {
let searchThrough;
if (Array.isArray(relatedText)) {
searchThrough = relatedText.join('\n');
} else {
searchThrough = relatedText;
}
searchThrough = searchThrough.toLocaleLowerCase();
const searchQuery = this.state.searchQuery.toLocaleLowerCase().trim();
const hasSearchMatches =
headerTitle.toLocaleLowerCase() === searchQuery
|| searchThrough.includes(searchQuery);
// Don't show results when the search input is empty
return this.state.searchQuery.length > 0 && hasSearchMatches;
};
const addSettingComponent = (component: ReactElement, relatedText: string|string[]) => {
const hiddenBySearch = this.state.searching && !matchesSearchQuery(relatedText);
if (component && !hiddenBySearch) {
settingComps.push(component);
}
};
const addSettingButton = (key: string, title: string, clickHandler: ()=> void, options: any = null) => {
const relatedText = [title];
if (typeof options === 'object' && options?.description) {
relatedText.push(options.description);
}
addSettingComponent(this.renderButton(key, title, clickHandler, options), relatedText);
};
const styleSheet = this.styles().styleSheet; const styleSheet = this.styles().styleSheet;
const addSettingLink = (key: string, title: string, target: string) => {
const component = (
<View key={key} style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL(target);
}}
accessibilityRole='link'
>
<Text key="label" style={styleSheet.linkText}>
{title}
</Text>
</TouchableOpacity>
</View>
);
addSettingComponent(component, title);
};
const addSettingText = (key: string, text: string) => {
addSettingComponent(
<View key={key} style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{text}</Text>
</View>,
text,
);
};
for (let i = 0; i < section.metadatas.length; i++) { for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i]; const md = section.metadatas[i];
@ -322,24 +415,29 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
</View> </View>
); );
settingComps.push(this.renderButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp })); addSettingButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp });
} }
} }
const settingComp = this.settingToComponent(md.key, settings[md.key]); const settingComp = this.settingToComponent(md.key, settings[md.key]);
settingComps.push(settingComp); const relatedText = [md.label?.() ?? '', md.description?.() ?? ''];
addSettingComponent(
settingComp,
relatedText,
);
} }
if (section.name === 'sync') { if (section.name === 'sync') {
settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_)); addSettingButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_);
} }
if (section.name === 'joplinCloud') { if (section.name === 'joplinCloud') {
const label = _('Email to note');
const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook'); const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook');
settingComps.push( addSettingComponent(
<View key="joplinCloud"> <View key="joplinCloud">
<View style={this.styles().styleSheet.settingContainerNoBottomBorder}> <View style={this.styles().styleSheet.settingContainerNoBottomBorder}>
<Text style={this.styles().styleSheet.settingText}>{_('Email to note')}</Text> <Text style={this.styles().styleSheet.settingText}>{label}</Text>
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text> <Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text>
</View> </View>
{ {
@ -351,20 +449,30 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
) )
} }
</View>, </View>,
[label, description],
); );
} }
if (section.name === 'tools') { if (section.name === 'tools') {
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_)); addSettingButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_);
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_)); addSettingButton('status_button', _('Sync Status'), this.syncStatusButtonPress_);
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_)); addSettingButton('log_button', _('Log'), this.logButtonPress_);
settingComps.push(this.renderButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') })); addSettingButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') });
} }
if (section.name === 'export') { if (section.name === 'export') {
settingComps.push(<NoteExportButton key='export_as_jex_button' styles={this.styles()} />); addSettingComponent(
settingComps.push(<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>); <NoteExportButton key='export_as_jex_button' styles={this.styles()} />,
settingComps.push(<ExportProfileButton key='export_data' styles={this.styles()}/>); [exportButtonTitle(), exportButtonDescription()],
);
addSettingComponent(
<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>,
exportDebugReportTitle(),
);
addSettingComponent(
<ExportProfileButton key='export_data' styles={this.styles()}/>,
exportProfileButtonTitle(),
);
} }
if (section.name === 'moreInfo') { if (section.name === 'moreInfo') {
@ -372,7 +480,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually // Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually
// set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again // set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again
settingComps.push( addSettingComponent(
<View key="permission_info" style={styleSheet.settingContainer}> <View key="permission_info" style={styleSheet.settingContainer}>
<View key="permission_info_wrapper"> <View key="permission_info_wrapper">
<Text key="perm1a" style={styleSheet.settingText}> <Text key="perm1a" style={styleSheet.settingText}>
@ -389,95 +497,60 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
</Text> </Text>
</View> </View>
</View>, </View>,
'',
); );
} }
settingComps.push( addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
<View key="donate_link" style={styleSheet.settingContainer}> addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
<TouchableOpacity addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
onPress={() => {
void Linking.openURL('https://joplinapp.org/donate/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Make a donation')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push( addSettingText('version_info_app', `Joplin ${VersionInfo.appVersion}`);
<View key="website_link" style={styleSheet.settingContainer}> addSettingText('version_info_db', _('Database v%s', reg.db().version()));
<TouchableOpacity addSettingText('version_info_fts', _('FTS enabled: %d', this.props.settings['db.ftsEnabled']));
onPress={() => { addSettingText('version_info_hermes', _('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0));
void Linking.openURL('https://joplinapp.org/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Joplin website')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="privacy_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/privacy/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Privacy Policy')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="version_info_app" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
</View>,
);
settingComps.push(
<View key="version_info_db" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('Database v%s', reg.db().version())}</Text>
</View>,
);
settingComps.push(
<View key="version_info_fts" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
</View>,
);
settingComps.push(
<View key="version_info_hermes" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
</View>,
);
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile); const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
if (featureFlagKeys.length) { if (featureFlagKeys.length) {
const headerKey = 'featureFlags'; const headerKey = 'featureFlags';
settingComps.push(<SectionHeader const featureFlagsTitle = _('Feature flags');
addSettingComponent(
<SectionHeader
key={headerKey} key={headerKey}
styles={this.styles().styleSheet} styles={this.styles().styleSheet}
title={_('Feature flags')} title={featureFlagsTitle}
onLayout={event => this.onHeaderLayout(headerKey, event)} onLayout={event => this.onHeaderLayout(headerKey, event)}
/>); />,
_('Feature flags'),
);
settingComps.push(<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>); addSettingComponent(
<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>,
featureFlagsTitle,
);
} }
} }
if (!settingComps.length) return null; if (!settingComps.length) return null;
if (!isSelected) return null; if (!isSelected && !this.state.searching) return null;
const headerComponent = (
<TouchableOpacity onPress={() => {
this.onJumpToSection_(section.name);
}}>
<SectionHeader
styles={styleSheet}
title={headerTitle}
/>
</TouchableOpacity>
);
return ( return (
<View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}> <View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}>
<View>{settingComps}</View> <View>
{this.state.searching ? headerComponent : null}
{settingComps}
</View>
</View> </View>
); );
} }
@ -563,18 +636,22 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
currentSectionName = 'general'; currentSectionName = 'general';
} }
if (this.state.searching) {
currentSectionName = null;
}
const sectionSelector = ( const sectionSelector = (
<SectionSelector <SectionSelector
selectedSectionName={currentSectionName} selectedSectionName={currentSectionName}
styles={this.styles()} styles={this.styles()}
settings={settings} settings={settings}
openSection={this.switchSectionPress_} openSection={this.onJumpToSection_}
width={this.state.sidebarWidth} width={this.state.sidebarWidth}
/> />
); );
let currentSection: ReactNode; let currentSection: ReactNode;
if (currentSectionName) { if (currentSectionName || this.state.searching) {
const settingComps = shared.settingsToComponents2( const settingComps = shared.settingsToComponents2(
this, AppType.Mobile, settings, currentSectionName, this, AppType.Mobile, settings, currentSectionName,
@ -582,11 +659,20 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// of React in lib/ and app-mobile/ // of React in lib/ and app-mobile/
) as ReactNode[]; ) as ReactNode[];
const searchInput = <TextInput
value={this.state.searchQuery}
label={_('Search')}
placeholder={_('Search...')}
onChangeText={this.onSearchUpdate_}
autoFocus={true}
/>;
currentSection = ( currentSection = (
<ScrollView <ScrollView
ref={this.scrollViewRef_} ref={this.scrollViewRef_}
style={{ flexGrow: 1 }} style={{ flexGrow: 1 }}
> >
{this.state.searching ? searchInput : null}
{settingComps} {settingComps}
</ScrollView> </ScrollView>
); );
@ -620,10 +706,11 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
<ScreenHeader <ScreenHeader
title={screenHeadingText} title={screenHeadingText}
showSaveButton={true} showSaveButton={true}
showSearchButton={false} showSearchButton={true}
showSideMenuButton={false} showSideMenuButton={false}
saveButtonDisabled={!this.hasUnsavedChanges()} saveButtonDisabled={!this.hasUnsavedChanges()}
onSaveButtonPress={this.saveButton_press} onSaveButtonPress={this.saveButton_press}
onSearchButtonPress={this.onSearchButtonPress_}
/> />
{mainComponent} {mainComponent}
</View> </View>

View File

@ -11,6 +11,8 @@ interface Props {
styles: ConfigScreenStyles; styles: ConfigScreenStyles;
} }
export const exportDebugReportTitle = () => _('Export Debug Report');
const ExportDebugReportButton = (props: Props) => { const ExportDebugReportButton = (props: Props) => {
const [creatingReport, setCreatingReport] = useState(false); const [creatingReport, setCreatingReport] = useState(false);
@ -24,7 +26,7 @@ const ExportDebugReportButton = (props: Props) => {
const exportDebugReportButton = ( const exportDebugReportButton = (
<SettingsButton <SettingsButton
title={creatingReport ? _('Creating report...') : _('Export Debug Report')} title={creatingReport ? _('Creating report...') : exportDebugReportTitle()}
clickHandler={exportDebugButtonPress} clickHandler={exportDebugButtonPress}
styles={props.styles} styles={props.styles}
disabled={creatingReport} disabled={creatingReport}

View File

@ -13,6 +13,8 @@ interface Props {
styles: ConfigScreenStyles; styles: ConfigScreenStyles;
} }
export const exportProfileButtonTitle = () => _('Export profile');
const ExportProfileButton = (props: Props) => { const ExportProfileButton = (props: Props) => {
const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle'); const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle');
const [profileExportPath, setProfileExportPath] = useState<string>(''); const [profileExportPath, setProfileExportPath] = useState<string>('');
@ -31,7 +33,7 @@ const ExportProfileButton = (props: Props) => {
const exportProfileButton = ( const exportProfileButton = (
<SettingsButton <SettingsButton
styles={props.styles} styles={props.styles}
title={profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile')} title={profileExportStatus === 'exporting' ? _('Exporting profile...') : exportProfileButtonTitle()}
clickHandler={exportProfileButtonPress} clickHandler={exportProfileButtonPress}
description={_('For debugging purpose only: export your profile to an external SD card.')} description={_('For debugging purpose only: export your profile to an external SD card.')}
disabled={profileExportStatus === 'exporting'} disabled={profileExportStatus === 'exporting'}

View File

@ -24,6 +24,9 @@ enum ExportStatus {
Exported, Exported,
} }
export const exportButtonTitle = () => _('Export all notes as JEX');
export const exportButtonDescription = () => _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const NoteExportButton: FunctionComponent<Props> = props => { const NoteExportButton: FunctionComponent<Props> = props => {
const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted); const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted);
const [exportProgress, setExportProgress] = useState<number|undefined>(0); const [exportProgress, setExportProgress] = useState<number|undefined>(0);
@ -80,13 +83,12 @@ const NoteExportButton: FunctionComponent<Props> = props => {
indeterminate={exportProgress === undefined} indeterminate={exportProgress === undefined}
progress={exportProgress}/> progress={exportProgress}/>
); );
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const startOrCancelExportButton = ( const startOrCancelExportButton = (
<SettingsButton <SettingsButton
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')} title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()}
disabled={exportStatus === ExportStatus.Exporting} disabled={exportStatus === ExportStatus.Exporting}
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null} description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null}
statusComponent={progressComponent} statusComponent={progressComponent}
clickHandler={startExport} clickHandler={startExport}
styles={props.styles} styles={props.styles}