mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-30 10:36:35 +02:00
b7f5f848f2
Will also allow using them when exporting HTML or PDF from CLI.
564 lines
19 KiB
JavaScript
564 lines
19 KiB
JavaScript
import Slider from '@react-native-community/slider';
|
|
const React = require('react');
|
|
const { Platform, TouchableOpacity, Linking, View, Switch, StyleSheet, Text, Button, ScrollView, TextInput, Alert, PermissionsAndroid } = require('react-native');
|
|
const { connect } = require('react-redux');
|
|
const { ScreenHeader } = require('lib/components/screen-header.js');
|
|
const { _ } = require('lib/locale.js');
|
|
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
|
const { Dropdown } = require('lib/components/Dropdown.js');
|
|
const { themeStyle } = require('lib/components/global-style.js');
|
|
const Setting = require('lib/models/Setting.js');
|
|
const shared = require('lib/components/shared/config-shared.js');
|
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
|
const { reg } = require('lib/registry.js');
|
|
const NavService = require('lib/services/NavService.js');
|
|
const VersionInfo = require('react-native-version-info').default;
|
|
const { ReportService } = require('lib/services/report.js');
|
|
const { time } = require('lib/time-utils');
|
|
const { shim } = require('lib/shim');
|
|
const SearchEngine = require('lib/services/SearchEngine');
|
|
const RNFS = require('react-native-fs');
|
|
const checkPermissions = require('lib/checkPermissions.js').default;
|
|
|
|
class ConfigScreenComponent extends BaseScreenComponent {
|
|
static navigationOptions() {
|
|
return { header: null };
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.styles_ = {};
|
|
|
|
this.state = {
|
|
creatingReport: false,
|
|
profileExportStatus: 'idle',
|
|
profileExportPath: '',
|
|
};
|
|
|
|
shared.init(this);
|
|
|
|
this.checkSyncConfig_ = async () => {
|
|
await shared.checkSyncConfig(this, this.state.settings);
|
|
};
|
|
|
|
this.e2eeConfig_ = () => {
|
|
NavService.go('EncryptionConfig');
|
|
};
|
|
|
|
this.saveButton_press = async () => {
|
|
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem') && !(await this.checkFilesystemPermission())) {
|
|
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
|
|
// Save settings anyway, even if permission has not been granted
|
|
}
|
|
return shared.saveSettings(this);
|
|
};
|
|
|
|
this.syncStatusButtonPress_ = () => {
|
|
NavService.go('Status');
|
|
};
|
|
|
|
this.exportDebugButtonPress_ = async () => {
|
|
this.setState({ creatingReport: true });
|
|
const service = new ReportService();
|
|
|
|
const logItems = await reg.logger().lastEntries(null);
|
|
const logItemRows = [['Date', 'Level', 'Message']];
|
|
for (let i = 0; i < logItems.length; i++) {
|
|
const item = logItems[i];
|
|
logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]);
|
|
}
|
|
const logItemCsv = service.csvCreate(logItemRows);
|
|
|
|
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
|
const filePath = `${RNFS.ExternalDirectoryPath}/syncReport-${new Date().getTime()}.txt`;
|
|
|
|
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
|
|
|
|
await RNFS.writeFile(filePath, finalText);
|
|
alert(`Debug report exported to ${filePath}`);
|
|
this.setState({ creatingReport: false });
|
|
};
|
|
|
|
this.fixSearchEngineIndexButtonPress_ = async () => {
|
|
this.setState({ fixingSearchIndex: true });
|
|
await SearchEngine.instance().rebuildIndex();
|
|
this.setState({ fixingSearchIndex: false });
|
|
};
|
|
|
|
this.exportProfileButtonPress_ = async () => {
|
|
const p = this.state.profileExportPath ? this.state.profileExportPath : `${RNFS.ExternalStorageDirectoryPath}/JoplinProfileExport`;
|
|
this.setState({
|
|
profileExportStatus: 'prompt',
|
|
profileExportPath: p,
|
|
});
|
|
};
|
|
|
|
this.exportProfileButtonPress2_ = async () => {
|
|
this.setState({ profileExportStatus: 'exporting' });
|
|
|
|
const dbPath = '/data/data/net.cozic.joplin/databases';
|
|
const exportPath = this.state.profileExportPath;
|
|
const resourcePath = `${exportPath}/resources`;
|
|
try {
|
|
const hasPermissions = await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE);
|
|
if (!hasPermissions) {
|
|
throw new Error('Permission denied');
|
|
}
|
|
|
|
const copyFiles = async (source, dest) => {
|
|
await shim.fsDriver().mkdir(dest);
|
|
|
|
const files = await shim.fsDriver().readDirStats(source);
|
|
|
|
for (const file of files) {
|
|
const source_ = `${source}/${file.path}`;
|
|
const dest_ = `${dest}/${file.path}`;
|
|
if (!file.isDirectory()) {
|
|
reg.logger().info(`Copying profile: ${source_} => ${dest_}`);
|
|
await shim.fsDriver().copy(source_, dest_);
|
|
} else {
|
|
await copyFiles(source_, dest_);
|
|
}
|
|
}
|
|
};
|
|
await copyFiles(dbPath, exportPath);
|
|
await copyFiles(Setting.value('resourceDir'), resourcePath);
|
|
|
|
alert('Profile has been exported!');
|
|
} catch (error) {
|
|
alert(`Could not export files: ${error.message}`);
|
|
} finally {
|
|
this.setState({ profileExportStatus: 'idle' });
|
|
}
|
|
};
|
|
|
|
this.logButtonPress_ = () => {
|
|
NavService.go('Log');
|
|
};
|
|
}
|
|
|
|
async checkFilesystemPermission() {
|
|
if (Platform.OS !== 'android') {
|
|
// Not implemented yet
|
|
return true;
|
|
}
|
|
return await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
|
title: _('Information'),
|
|
message: _('In order to use file system synchronisation your permission to write to external storage is required.'),
|
|
buttonPositive: _('OK'),
|
|
});
|
|
}
|
|
|
|
UNSAFE_componentWillMount() {
|
|
this.setState({ settings: this.props.settings });
|
|
}
|
|
|
|
styles() {
|
|
const themeId = this.props.theme;
|
|
const theme = themeStyle(themeId);
|
|
|
|
if (this.styles_[themeId]) return this.styles_[themeId];
|
|
this.styles_ = {};
|
|
|
|
const styles = {
|
|
body: {
|
|
flex: 1,
|
|
justifyContent: 'flex-start',
|
|
flexDirection: 'column',
|
|
},
|
|
settingContainer: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.dividerColor,
|
|
paddingTop: theme.marginTop,
|
|
paddingBottom: theme.marginBottom,
|
|
paddingLeft: theme.marginLeft,
|
|
paddingRight: theme.marginRight,
|
|
},
|
|
settingText: {
|
|
color: theme.color,
|
|
fontSize: theme.fontSize,
|
|
flex: 1,
|
|
paddingRight: 5,
|
|
},
|
|
descriptionText: {
|
|
color: theme.colorFaded,
|
|
fontSize: theme.fontSizeSmaller,
|
|
flex: 1,
|
|
},
|
|
sliderUnits: {
|
|
color: theme.color,
|
|
fontSize: theme.fontSize,
|
|
marginRight: 10,
|
|
},
|
|
settingDescriptionText: {
|
|
color: theme.colorFaded,
|
|
fontSize: theme.fontSizeSmaller,
|
|
flex: 1,
|
|
paddingLeft: theme.marginLeft,
|
|
paddingRight: theme.marginRight,
|
|
paddingBottom: theme.marginBottom,
|
|
},
|
|
permissionText: {
|
|
color: theme.color,
|
|
fontSize: theme.fontSize,
|
|
flex: 1,
|
|
marginTop: 10,
|
|
},
|
|
settingControl: {
|
|
color: theme.color,
|
|
flex: 1,
|
|
},
|
|
textInput: {
|
|
color: theme.color,
|
|
},
|
|
};
|
|
|
|
styles.settingContainerNoBottomBorder = Object.assign({}, styles.settingContainer, {
|
|
borderBottomWidth: 0,
|
|
paddingBottom: theme.marginBottom / 2,
|
|
});
|
|
|
|
styles.settingControl.borderBottomWidth = 1;
|
|
styles.settingControl.borderBottomColor = theme.dividerColor;
|
|
|
|
styles.switchSettingText = Object.assign({}, styles.settingText);
|
|
styles.switchSettingText.width = '80%';
|
|
|
|
styles.switchSettingContainer = Object.assign({}, styles.settingContainer);
|
|
styles.switchSettingContainer.flexDirection = 'row';
|
|
styles.switchSettingContainer.justifyContent = 'space-between';
|
|
|
|
styles.linkText = Object.assign({}, styles.settingText);
|
|
styles.linkText.borderBottomWidth = 1;
|
|
styles.linkText.borderBottomColor = theme.color;
|
|
styles.linkText.flex = 0;
|
|
styles.linkText.fontWeight = 'normal';
|
|
|
|
styles.headerWrapperStyle = Object.assign({}, styles.settingContainer, theme.headerWrapperStyle);
|
|
|
|
styles.switchSettingControl = Object.assign({}, styles.settingControl);
|
|
delete styles.switchSettingControl.color;
|
|
// styles.switchSettingControl.width = '20%';
|
|
styles.switchSettingControl.flex = 0;
|
|
|
|
this.styles_[themeId] = StyleSheet.create(styles);
|
|
return this.styles_[themeId];
|
|
}
|
|
|
|
renderHeader(key, title) {
|
|
const theme = themeStyle(this.props.theme);
|
|
return (
|
|
<View key={key} style={this.styles().headerWrapperStyle}>
|
|
<Text style={theme.headerStyle}>{title}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
renderButton(key, title, clickHandler, options = null) {
|
|
if (!options) options = {};
|
|
|
|
let descriptionComp = null;
|
|
if (options.description) {
|
|
descriptionComp = (
|
|
<View style={{ flex: 1, marginTop: 10 }}>
|
|
<Text style={this.styles().descriptionText}>{options.description}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View key={key} style={this.styles().settingContainer}>
|
|
<View style={{ flex: 1, flexDirection: 'column' }}>
|
|
<View style={{ flex: 1 }}>
|
|
<Button title={title} onPress={clickHandler} disabled={!!options.disabled} />
|
|
</View>
|
|
{options.statusComp}
|
|
{descriptionComp}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
sectionToComponent(key, section, settings) {
|
|
const settingComps = [];
|
|
|
|
for (let i = 0; i < section.metadatas.length; i++) {
|
|
const md = section.metadatas[i];
|
|
|
|
if (section.name === 'sync' && md.key === 'sync.resourceDownloadMode') {
|
|
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
|
|
|
if (syncTargetMd.supportsConfigCheck) {
|
|
const messages = shared.checkSyncConfigMessages(this);
|
|
const statusComp = !messages.length ? null : (
|
|
<View style={{ flex: 1, marginTop: 10 }}>
|
|
<Text style={this.styles().descriptionText}>{messages[0]}</Text>
|
|
{messages.length >= 1 ? (
|
|
<View style={{ marginTop: 10 }}>
|
|
<Text style={this.styles().descriptionText}>{messages[1]}</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(this.renderButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp }));
|
|
}
|
|
}
|
|
|
|
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
|
settingComps.push(settingComp);
|
|
}
|
|
|
|
if (section.name === 'sync') {
|
|
settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_));
|
|
}
|
|
|
|
if (!settingComps.length) return null;
|
|
|
|
return (
|
|
<View key={key}>
|
|
{this.renderHeader(section.name, Setting.sectionNameToLabel(section.name))}
|
|
<View>{settingComps}</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
settingToComponent(key, value) {
|
|
const themeId = this.props.theme;
|
|
const theme = themeStyle(themeId);
|
|
const output = null;
|
|
|
|
const updateSettingValue = (key, value) => {
|
|
return shared.updateSettingValue(this, key, value);
|
|
};
|
|
|
|
const md = Setting.settingMetadata(key);
|
|
const settingDescription = md.description ? md.description() : '';
|
|
|
|
const descriptionComp = !settingDescription ? null : <Text style={this.styles().settingDescriptionText}>{settingDescription}</Text>;
|
|
const containerStyle = !settingDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
|
|
|
|
if (md.isEnum) {
|
|
value = value.toString();
|
|
|
|
const items = [];
|
|
const settingOptions = md.options();
|
|
for (const k in settingOptions) {
|
|
if (!settingOptions.hasOwnProperty(k)) continue;
|
|
items.push({ label: settingOptions[k], value: k.toString() });
|
|
}
|
|
|
|
return (
|
|
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
|
<View style={containerStyle}>
|
|
<Text key="label" style={this.styles().settingText}>
|
|
{md.label()}
|
|
</Text>
|
|
<Dropdown
|
|
key="control"
|
|
style={this.styles().settingControl}
|
|
items={items}
|
|
selectedValue={value}
|
|
itemListStyle={{
|
|
backgroundColor: theme.backgroundColor,
|
|
}}
|
|
headerStyle={{
|
|
color: theme.color,
|
|
fontSize: theme.fontSize,
|
|
}}
|
|
itemStyle={{
|
|
color: theme.color,
|
|
fontSize: theme.fontSize,
|
|
}}
|
|
onValueChange={(itemValue) => {
|
|
updateSettingValue(key, itemValue);
|
|
}}
|
|
/>
|
|
</View>
|
|
{descriptionComp}
|
|
</View>
|
|
);
|
|
} else if (md.type == Setting.TYPE_BOOL) {
|
|
return (
|
|
<View key={key}>
|
|
<View style={containerStyle}>
|
|
<Text key="label" style={this.styles().switchSettingText}>
|
|
{md.label()}
|
|
</Text>
|
|
<Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
|
</View>
|
|
{descriptionComp}
|
|
</View>
|
|
);
|
|
} else if (md.type == Setting.TYPE_INT) {
|
|
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
|
|
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
|
|
// on the Slider as they are buggy and can crash the app on certain devices.
|
|
// https://github.com/laurent22/joplin/issues/2733
|
|
// https://github.com/react-native-community/react-native-slider/issues/161
|
|
return (
|
|
<View key={key} style={this.styles().settingContainer}>
|
|
<Text key="label" style={this.styles().settingText}>
|
|
{md.label()}
|
|
</Text>
|
|
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
|
<Text style={this.styles().sliderUnits}>{unitLabel}</Text>
|
|
<Slider key="control" style={{ flex: 1 }} step={md.step} minimumValue={md.minimum} maximumValue={md.maximum} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
|
</View>
|
|
</View>
|
|
);
|
|
} else if (md.type == Setting.TYPE_STRING) {
|
|
return (
|
|
<View key={key} style={this.styles().settingContainer}>
|
|
<Text key="label" style={this.styles().settingText}>
|
|
{md.label()}
|
|
</Text>
|
|
<TextInput autoCorrect={false} autoCompleteType="off" selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={value => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
|
|
</View>
|
|
);
|
|
} else {
|
|
// throw new Error('Unsupported setting type: ' + md.type);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
render() {
|
|
const settings = this.state.settings;
|
|
|
|
const theme = themeStyle(this.props.theme);
|
|
|
|
const settingComps = shared.settingsToComponents2(this, 'mobile', settings);
|
|
|
|
settingComps.push(this.renderHeader('tools', _('Tools')));
|
|
|
|
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
|
|
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
|
|
if (Platform.OS === 'android') {
|
|
settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport }));
|
|
}
|
|
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.') }));
|
|
|
|
if (shim.mobilePlatform() === 'android') {
|
|
settingComps.push(this.renderButton('export_data', this.state.profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile'), this.exportProfileButtonPress_, { disabled: this.state.profileExportStatus === 'exporting', description: _('For debugging purpose only: export your profile to an external SD card.') }));
|
|
|
|
if (this.state.profileExportStatus === 'prompt') {
|
|
const profileExportPrompt = (
|
|
<View style={this.styles().settingContainer} key="profileExport">
|
|
<Text style={this.styles().settingText}>Path:</Text>
|
|
<TextInput style={{ ...this.styles().textInput, paddingRight: 20 }} onChange={(event) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance}></TextInput>
|
|
<Button title="OK" onPress={this.exportProfileButtonPress2_}></Button>
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(profileExportPrompt);
|
|
}
|
|
}
|
|
|
|
settingComps.push(this.renderHeader('moreInfo', _('More information')));
|
|
|
|
if (Platform.OS === 'android' && Platform.Version >= 23) {
|
|
// 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
|
|
|
|
settingComps.push(
|
|
<View key="permission_info" style={this.styles().settingContainer}>
|
|
<View key="permission_info_wrapper">
|
|
<Text key="perm1a" style={this.styles().settingText}>
|
|
{_('To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions')}
|
|
</Text>
|
|
<Text key="perm2" style={this.styles().permissionText}>
|
|
{_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')}
|
|
</Text>
|
|
<Text key="perm3" style={this.styles().permissionText}>
|
|
{_('- Camera: to allow taking a picture and attaching it to a note.')}
|
|
</Text>
|
|
<Text key="perm4" style={this.styles().permissionText}>
|
|
{_('- Location: to allow attaching geo-location information to a note.')}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
settingComps.push(
|
|
<View key="donate_link" style={this.styles().settingContainer}>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Linking.openURL('https://joplinapp.org/donate/');
|
|
}}
|
|
>
|
|
<Text key="label" style={this.styles().linkText}>
|
|
{_('Make a donation')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(
|
|
<View key="website_link" style={this.styles().settingContainer}>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Linking.openURL('https://joplinapp.org/');
|
|
}}
|
|
>
|
|
<Text key="label" style={this.styles().linkText}>
|
|
{_('Joplin website')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(
|
|
<View key="privacy_link" style={this.styles().settingContainer}>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Linking.openURL('https://joplinapp.org/privacy/');
|
|
}}
|
|
>
|
|
<Text key="label" style={this.styles().linkText}>
|
|
Privacy Policy
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(
|
|
<View key="version_info_app" style={this.styles().settingContainer}>
|
|
<Text style={this.styles().settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(
|
|
<View key="version_info_db" style={this.styles().settingContainer}>
|
|
<Text style={this.styles().settingText}>{_('Database v%s', reg.db().version())}</Text>
|
|
</View>
|
|
);
|
|
|
|
settingComps.push(
|
|
<View key="version_info_fts" style={this.styles().settingContainer}>
|
|
<Text style={this.styles().settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<View style={this.rootStyle(this.props.theme).root}>
|
|
<ScreenHeader title={_('Configuration')} showSaveButton={true} showSearchButton={false} showSideMenuButton={false} saveButtonDisabled={!this.state.changedSettingKeys.length} onSaveButtonPress={this.saveButton_press} />
|
|
<ScrollView>{settingComps}</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
const ConfigScreen = connect(state => {
|
|
return {
|
|
settings: state.settings,
|
|
theme: state.settings.theme,
|
|
};
|
|
})(ConfigScreenComponent);
|
|
|
|
module.exports = { ConfigScreen };
|