/* eslint-disable @typescript-eslint/explicit-member-accessibility */ import Slider from '@react-native-community/slider'; const React = require('react'); import { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from '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 { BackButtonService } = require('../../services/back-button.js'); const VersionInfo = require('react-native-version-info').default; const { connect } = require('react-redux'); import ScreenHeader from '../ScreenHeader'; 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'; import { openDocumentTree } from '@joplin/react-native-saf-x'; import biometricAuthenticate from '../biometrics/biometricAuthenticate'; class ConfigScreenComponent extends BaseScreenComponent { public static navigationOptions(): any { return { header: null }; } private componentsY_: Record = {}; public constructor() { super(); this.styles_ = {}; this.state = { creatingReport: false, profileExportStatus: 'idle', profileExportPath: '', fileSystemSyncPath: Setting.value('sync.2.path'), }; this.scrollViewRef_ = React.createRef(); shared.init(this, reg); this.selectDirectoryButtonPress = async () => { try { const doc = await openDocumentTree(true); if (doc?.uri) { this.setState({ fileSystemSyncPath: doc.uri }); shared.updateSettingValue(this, 'sync.2.path', doc.uri); } else { throw new Error('User cancelled operation'); } } catch (e) { reg.logger().info('Didn\'t pick sync dir: ', e); } }; 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')) { if (Platform.OS === 'android') { if (Platform.Version < 29) { if (!(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.manageProfilesButtonPress_ = () => { this.props.dispatch({ type: 'NAV_GO', routeName: 'ProfileSwitcher', }); }; 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 externalDir = await shim.fsDriver().getExternalDirectoryPath(); if (!externalDir) { this.setState({ creatingReport: false }); return; } const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`; const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n'); await shim.fsDriver().writeFile(filePath, finalText, 'utf8'); 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 externalDir = await shim.fsDriver().getExternalDirectoryPath(); if (!externalDir) { return; } const p = this.state.profileExportPath ? this.state.profileExportPath : `${externalDir}/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'); }; } public 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'), }); } public UNSAFE_componentWillMount() { this.setState({ settings: this.props.settings }); } public 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]; } private onHeaderLayout(key: string, event: any) { const layout = event.nativeEvent.layout; this.componentsY_[`header_${key}`] = layout.y; } private onSectionLayout(key: string, event: any) { const layout = event.nativeEvent.layout; this.componentsY_[`section_${key}`] = layout.y; } private componentY(key: string): number { if ((`section_${key}`) in this.componentsY_) return this.componentsY_[`section_${key}`]; if ((`header_${key}`) in this.componentsY_) return this.componentsY_[`header_${key}`]; console.error(`ConfigScreen: Could not find key to scroll to: ${key}`); return 0; } private handleBackButtonPress = (): boolean => { const goBack = async () => { BackButtonService.removeHandler(this.handleBackButtonPress); await BackButtonService.back(); }; if (this.state.changedSettingKeys.length > 0) { const dialogTitle: string|null = null; Alert.alert( dialogTitle, _('There are unsaved changes.'), [{ text: _('Save changes'), onPress: async () => { await this.saveButton_press(); await goBack(); }, }, { text: _('Discard changes'), onPress: goBack, }] ); return true; } return false; }; public componentDidMount() { if (this.props.navigation.state.sectionName) { setTimeout(() => { this.scrollViewRef_.current.scrollTo({ x: 0, y: this.componentY(this.props.navigation.state.sectionName), animated: true, }); }, 200); } BackButtonService.addHandler(this.handleBackButtonPress); } public componentWillUnmount() { BackButtonService.removeHandler(this.handleBackButtonPress); } public renderHeader(key: string, title: string) { const theme = themeStyle(this.props.themeId); return ( this.onHeaderLayout(key, event)}> {title} ); } renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) { if (!options) options = {}; let descriptionComp = null; if (options.description) { descriptionComp = ( {options.description} ); } return (