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'); 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').default; 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').default; const SearchEngine = require('lib/services/searchengine/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.themeId; 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.themeId); return ( {title} ); } renderButton(key, title, clickHandler, options = null) { if (!options) options = {}; let descriptionComp = null; if (options.description) { descriptionComp = ( {options.description} ); } return ( ); 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( {_('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 => { return { settings: state.settings, themeId: state.settings.theme, }; })(ConfigScreenComponent); module.exports = { ConfigScreen };