import Slider from '@react-native-community/slider'; const React = require('react'); const { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid } = require('react-native'); import Setting, { AppType } from '@joplin/lib/models/Setting'; import NavService from '@joplin/lib/services/NavService'; import ReportService from '@joplin/lib/services/ReportService'; import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; import checkPermissions from '../../utils/checkPermissions'; import time from '@joplin/lib/time'; import shim from '@joplin/lib/shim'; import setIgnoreTlsErrors from '../../utils/TlsUtils'; import { reg } from '@joplin/lib/registry'; import { State } from '@joplin/lib/reducer'; const VersionInfo = require('react-native-version-info').default; const { connect } = require('react-redux'); const { ScreenHeader } = require('../screen-header.js'); const { _ } = require('@joplin/lib/locale'); const { BaseScreenComponent } = require('../base-screen.js'); const { Dropdown } = require('../Dropdown.js'); const { themeStyle } = require('../global-style.js'); const shared = require('@joplin/lib/components/shared/config-shared.js'); import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; const RNFS = require('react-native-fs'); class ConfigScreenComponent extends BaseScreenComponent { static navigationOptions(): any { return { header: null }; } constructor() { super(); this.styles_ = {}; this.state = { creatingReport: false, profileExportStatus: 'idle', profileExportPath: '', }; shared.init(this); this.checkSyncConfig_ = async () => { // to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state // this call sets the new value and returns the previous one which we can use later to revert the change const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']); const result = await shared.checkSyncConfig(this, this.state.settings); if (!result || !result.ok) { await setIgnoreTlsErrors(prevIgnoreTlsErrors); } }; this.e2eeConfig_ = () => { void 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 } // changedSettingKeys is cleared in shared.saveSettings so reading it now const setIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors'); await shared.saveSettings(this); if (setIgnoreTlsErrors) { await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors')); } }; this.syncStatusButtonPress_ = () => { void 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 response = await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE); if (response !== PermissionsAndroid.RESULTS.GRANTED) { throw new Error('Permission denied'); } const copyFiles = async (source: string, dest: string) => { 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_ = () => { void 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.themeId; const theme = themeStyle(themeId); if (this.styles_[themeId]) return this.styles_[themeId]; this.styles_ = {}; const styles: any = { 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: string, title: string) { const theme = themeStyle(this.props.themeId); return ( {title} ); } renderButton(key: string, title: string, clickHandler: Function, options: any = null) { if (!options) options = {}; let descriptionComp = null; if (options.description) { descriptionComp = ( {options.description} ); } return ( ); settingComps.push(profileExportPrompt); } } const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile); if (featureFlagKeys.length) { settingComps.push(this.renderHeader('featureFlags', _('Feature flags'))); settingComps.push({this.renderFeatureFlags(settings, featureFlagKeys)}); } 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( {_('To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions')} {_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')} {_('- Camera: to allow taking a picture and attaching it to a note.')} {_('- Location: to allow attaching geo-location information to a note.')} ); } settingComps.push( { Linking.openURL('https://joplinapp.org/donate/'); }} > {_('Make a donation')} ); settingComps.push( { Linking.openURL('https://joplinapp.org/'); }} > {_('Joplin website')} ); settingComps.push( { Linking.openURL('https://joplinapp.org/privacy/'); }} > {_('Privacy Policy')} ); settingComps.push( {`Joplin ${VersionInfo.appVersion}`} ); settingComps.push( {_('Database v%s', reg.db().version())} ); settingComps.push( {_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])} ); return ( {settingComps} ); } } const ConfigScreen = connect((state: State) => { return { settings: state.settings, themeId: state.settings.theme, }; })(ConfigScreenComponent); export default ConfigScreen;