1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx

711 lines
25 KiB
TypeScript
Raw Normal View History

/* eslint-disable @typescript-eslint/explicit-member-accessibility */
import Slider from '@react-native-community/slider';
const React = require('react');
2023-07-18 15:58:06 +02:00
import { Platform, Linking, View, Switch, 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';
2023-07-18 15:58:06 +02:00
import checkPermissions from '../../../utils/checkPermissions';
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
2023-07-18 15:58:06 +02:00
import setIgnoreTlsErrors from '../../../utils/TlsUtils';
import { reg } from '@joplin/lib/registry';
import { State } from '@joplin/lib/reducer';
2023-07-18 15:58:06 +02:00
const { BackButtonService } = require('../../../services/back-button.js');
const VersionInfo = require('react-native-version-info').default;
const { connect } = require('react-redux');
2023-07-18 15:58:06 +02:00
import ScreenHeader from '../../ScreenHeader';
const { _ } = require('@joplin/lib/locale');
2023-07-18 15:58:06 +02:00
const { BaseScreenComponent } = require('../../base-screen.js');
const { Dropdown } = require('../../Dropdown');
const { themeStyle } = require('../../global-style.js');
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
2021-08-16 16:20:14 +02:00
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import { openDocumentTree } from '@joplin/react-native-saf-x';
2023-07-18 15:58:06 +02:00
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles from './configScreenStyles';
import NoteExportButton from './NoteExportSection/NoteExportButton';
import ConfigScreenButton from './ConfigScreenButton';
import Clipboard from '@react-native-community/clipboard';
2017-07-23 20:26:50 +02:00
2017-08-01 19:59:01 +02:00
class ConfigScreenComponent extends BaseScreenComponent {
public static navigationOptions(): any {
2017-08-01 19:59:01 +02:00
return { header: null };
2017-07-30 23:04:26 +02:00
}
2017-07-23 20:26:50 +02:00
private componentsY_: Record<string, number> = {};
public constructor() {
2017-08-01 19:59:01 +02:00
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.saveButton_press = this.saveButton_press.bind(this);
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');
2019-09-19 23:51:18 +02:00
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 {
Mobile: Upgraded React Native to v0.63 commit 2fb6cee90174bfcc02f77ba1606bfd8c4e2c8fc8 Merge: 4e303be85f db509955f6 Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 16:24:07 2020 +0100 Merge branch 'dev' into rn_63 commit 4e303be85f7b3162b7e5b96e18557da13acfc988 Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 16:14:39 2020 +0100 Clean up commit e3a37ec2d6f3e6cc07c018b11b3f80ca8256063e Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 15:57:55 2020 +0100 Use different script for pre-commit and manual start commit bd236648fcd92a812cd16369dfa2238d38c6638f Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 15:56:45 2020 +0100 Removed RN eslint config commit e7feda41c9b473cd18768f2ce7686611fa2b3d08 Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 15:27:08 2020 +0100 Revert "Disable git hook for now" This reverts commit 89263ac7425bae5b03b60742ab186441217b37dc. commit cfd63fe46fbc714c065f13dd9add5a0b8e18bf1f Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 13:02:32 2020 +0100 Ask permission to use geo-location commit 66059939a38460ba05c09eed7f3b19fc4ead924c Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 12:26:20 2020 +0100 Fixed WebView race condition commit 1e0d2b7b86d88629f19ae6574f73c945d422d0b5 Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 11:56:21 2020 +0100 Fixed webview issues commit f537d22d7fc4bcf6ddb54a4faadf72f585ae271c Author: Laurent Cozic <laurent@cozic.net> Date: Fri Oct 16 11:08:29 2020 +0100 Improve resource file watching commit eec32cf70aaf69b04a703ce49c3ede48a6ac1067 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 18:40:13 2020 +0100 Removed cache package dependency and implemented one more suitable for React Native commit efa346fea48414c98c1e577bf0d74a6e90a78044 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 14:57:21 2020 +0100 iOS: Added fonts to Info.plist although it was working without it commit 572b647bc0ff5b12ddd555ad7ca2bb18ccaeb512 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 14:56:49 2020 +0100 Specify content-type header for OneDrive to prevent network error commit bcedf6c7f0c35a428fd1c0800d4f17de662a49ff Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:45:01 2020 +0100 iOS: Disable long press menu since it is already built-in commit 7359dd61d1a609dbfce87b0deb169b8c5e2ace14 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:37:40 2020 +0100 Removed unused react-native-device-info commit 2d63ab36d32775f07236dae62f6cc7792dac435a Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:35:54 2020 +0100 iOS: Fixed taking a picture commit 8e2875a91c87b48ba3e230e9296d032cd05a267c Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 12:11:13 2020 +0100 iOS: Restored camera roll functionality commit 75f5edf2addfe3590d1a37bdac99680cb1a5c84c Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 11:40:13 2020 +0100 iOS: Fixed build settings commit b220c984198e78a2401387f4385bfdd331852d78 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 11:40:03 2020 +0100 iOS: Got images to work with WebKit commit c34b43e841b768104f19e86900133a1986b53af9 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 10:24:52 2020 +0100 iOS: Restore more settings commit 32997611e625f1775df05af966782ae763c1aa17 Author: Laurent Cozic <laurent@cozic.net> Date: Thu Oct 15 10:15:14 2020 +0100 iOS: Added back icons and other properties commit b5811d7f7cff227a30bb10ecad76f52cb212170a Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 23:53:14 2020 +0100 Got iOS build to work commit dc6d7c00e0048088cca653d5214d1cc4679ca005 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 18:40:06 2020 +0100 Imported old settings in gradle build commit dff59f560317d260b8540a9324bcdd5e749a0c0b Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 18:20:00 2020 +0100 Restored sharing commit 0bdb449e72ef1766bd5aac878f44106c36e662c2 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 17:25:40 2020 +0100 Updated NoteBodyViewer commit 0c0d228815251cfaf66ba25a276852d32192f106 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 16:54:42 2020 +0100 Fixed networking commit 6ff45ce485d59e3e0fe66a9658a678499c887058 Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 13:11:00 2020 +0100 Fixed document picker commit cc889182b66052b8dfad03b46121e6a14763a51a Author: Laurent Cozic <laurent@cozic.net> Date: Wed Oct 14 12:56:27 2020 +0100 Added back support for alarms commit 040261abfad89e5a58617d4c2d4f811d324ea488 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 22:04:49 2020 +0100 Fixed Clipboard and remove image-picker package commit 1077ad8f16481afcc63d92020e91cd37f077f207 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 21:54:52 2020 +0100 Fixed Select Alarm dialog and PoorManIntervals class commit 8296676fd52878b2f1cc2028a099f31909e6f286 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 21:32:52 2020 +0100 Fixed icons and warnings commit 3b0e3f6f43c83bb103132e8296d3887fccd386b5 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 17:02:59 2020 +0100 Got app to build again commit 89263ac7425bae5b03b60742ab186441217b37dc Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:41:17 2020 +0100 Disable git hook for now commit d6da162f674f94ba2c462268e39c161fa6126220 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:39:12 2020 +0100 Restored back all RN packages commit 7f8ce3732cf4c8ff6dcbcc0a8918c680adadd3f4 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:13:12 2020 +0100 Restored base packages commit ea59726eb3e0414afcdbe8af30a7765875239225 Author: Laurent Cozic <laurent@cozic.net> Date: Tue Oct 13 15:05:17 2020 +0100 Started over from scratch
2020-10-16 17:26:19 +02:00
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');
};
this.handleSetting = this.handleSetting.bind(this);
}
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 });
2017-08-01 19:59:01 +02:00
}
2017-07-30 23:04:26 +02:00
public styles() {
2020-09-15 15:01:07 +02:00
const themeId = this.props.themeId;
2017-08-01 19:59:01 +02:00
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
2023-07-18 15:58:06 +02:00
this.styles_[themeId] = configScreenStyles(themeId);
2017-08-01 19:59:01 +02:00
return this.styles_[themeId];
2017-07-23 20:26:50 +02:00
}
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) {
2020-09-15 15:01:07 +02:00
const theme = themeStyle(this.props.themeId);
return (
<View key={key} style={this.styles().headerWrapperStyle} onLayout={(event: any) => this.onHeaderLayout(key, event)}>
<Text style={theme.headerStyle}>{title}</Text>
</View>
);
}
2023-07-18 15:58:06 +02:00
private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
return (
2023-07-18 15:58:06 +02:00
<ConfigScreenButton
key={key}
title={title}
clickHandler={clickHandler}
description={options?.description}
statusComponent={options?.statusComp}
styles={this.styles()}
/>
);
}
public sectionToComponent(key: string, section: any, settings: any) {
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 (section.name === 'joplinCloud') {
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(
<View key="joplinCloud">
<View style={this.styles().settingContainerNoBottomBorder}>
<Text style={this.styles().settingText}>{_('Email to note')}</Text>
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text>
</View>
{
this.renderButton(
'sync.10.inboxEmail',
_('Copy to clipboard'),
() => Clipboard.setString(this.props.settings['sync.10.inboxEmail']),
{ description },
)
}
</View>,
);
}
if (!settingComps.length) return null;
return (
<View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}>
{this.renderHeader(section.name, Setting.sectionNameToLabel(section.name))}
<View>{settingComps}</View>
</View>
);
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
private renderToggle(key: string, label: string, value: any, updateSettingValue: Function, descriptionComp: any = null) {
const theme = themeStyle(this.props.themeId);
return (
<View key={key}>
<View style={this.containerStyle(false)}>
<Text key="label" style={this.styles().switchSettingText}>
{label}
</Text>
<Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => void updateSettingValue(key, value)} />
</View>
{descriptionComp}
</View>
);
}
private containerStyle(hasDescription: boolean): any {
return !hasDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
}
private async handleSetting(key: string, value: any): Promise<boolean> {
// When the user tries to enable biometrics unlock, we ask for the
// fingerprint or Face ID, and if it's correct we save immediately. If
// it's not, we don't turn on the setting.
if (key === 'security.biometricsEnabled' && !!value) {
try {
await biometricAuthenticate();
shared.updateSettingValue(this, key, value, async () => await this.saveButton_press());
} catch (error) {
shared.updateSettingValue(this, key, false);
Alert.alert(error.message);
}
return true;
}
if (key === 'security.biometricsEnabled' && !value) {
shared.updateSettingValue(this, key, value, async () => await this.saveButton_press());
return true;
}
return false;
}
public settingToComponent(key: string, value: any) {
2020-09-15 15:01:07 +02:00
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
const output: any = null;
2017-07-23 20:26:50 +02:00
const updateSettingValue = async (key: string, value: any) => {
const handled = await this.handleSetting(key, value);
if (!handled) shared.updateSettingValue(this, key, value);
};
2017-07-23 20:26:50 +02:00
2017-07-31 22:51:24 +02:00
const md = Setting.settingMetadata(key);
const settingDescription = md.description ? md.description() : '';
2017-07-31 22:51:24 +02:00
Mobile: Add toolbar, list continuation and Markdown preview to editor (#2224) * The basic editor is working! No list continuation still though * List continuation is working! Now to delete when entering again and not typing on line + handle ordered lists * Supports checkboxes + attempted at setting font * Editor font works now; now need to fix the delete (look at past state) * Fix deletion problem * Add ordered list handler * Add comments * Extract insertListLine * End lists on enter for empty bullets * Add MarkdownView (renders badly though) * Save edited text from MarkdownEditor * Cleanup * Refactor react-native-markdown-editor/ * Rename react-native-markdown-editor/ => MarkdownEditor/ * Cleanup * Fix preview styles; still need to fix checkbox problem * Fix keyboard padding * Change name back to #body_changeText * Incorporate PR feedback from @laurent22 * wip: Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/ * Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/ * Remove log statement * Focus TextInput in MarkdownEditor from grandparent * Make eslint happy * Extract textInputRefName to shared variable * Remove accidental #setState * Cleanup * Cleanup * Run linter * Cleanup * Update button order * Improve styles for config descriptions * Allow descriptions to be added to BOOL type Setting configs * Add editorBeta Setting * Move FailSafe details to description text * Update descriptionText styles * Put the editor under the beta flag toggle * Incorporate PR feedback from @laurent22 * Refactor Markdown editor focusing * Cleanup * Reorder MarkdownEditor formats * Make applyListFormat behavior more intuitive * Add comment * Show MarkdownEditor with preview by default * Show preview by default, then hide on typing * Fix MarkdownEditor selection bug * Cleanup * Update Markdown button styles * Make Markdown button colors theme-conscious * Fix merge conflict resolution mistake * Fix broken import * Delete package-lock.json * Reset package-lock.json Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2020-03-25 12:50:45 +02:00
const descriptionComp = !settingDescription ? null : <Text style={this.styles().settingDescriptionText}>{settingDescription}</Text>;
const containerStyle = this.containerStyle(!!settingDescription);
Mobile: Add toolbar, list continuation and Markdown preview to editor (#2224) * The basic editor is working! No list continuation still though * List continuation is working! Now to delete when entering again and not typing on line + handle ordered lists * Supports checkboxes + attempted at setting font * Editor font works now; now need to fix the delete (look at past state) * Fix deletion problem * Add ordered list handler * Add comments * Extract insertListLine * End lists on enter for empty bullets * Add MarkdownView (renders badly though) * Save edited text from MarkdownEditor * Cleanup * Refactor react-native-markdown-editor/ * Rename react-native-markdown-editor/ => MarkdownEditor/ * Cleanup * Fix preview styles; still need to fix checkbox problem * Fix keyboard padding * Change name back to #body_changeText * Incorporate PR feedback from @laurent22 * wip: Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/ * Move MarkdownEditor/ from ReactNativeClient/lib/ to ReactNativeClient/ * Remove log statement * Focus TextInput in MarkdownEditor from grandparent * Make eslint happy * Extract textInputRefName to shared variable * Remove accidental #setState * Cleanup * Cleanup * Run linter * Cleanup * Update button order * Improve styles for config descriptions * Allow descriptions to be added to BOOL type Setting configs * Add editorBeta Setting * Move FailSafe details to description text * Update descriptionText styles * Put the editor under the beta flag toggle * Incorporate PR feedback from @laurent22 * Refactor Markdown editor focusing * Cleanup * Reorder MarkdownEditor formats * Make applyListFormat behavior more intuitive * Add comment * Show MarkdownEditor with preview by default * Show preview by default, then hide on typing * Fix MarkdownEditor selection bug * Cleanup * Update Markdown button styles * Make Markdown button colors theme-conscious * Fix merge conflict resolution mistake * Fix broken import * Delete package-lock.json * Reset package-lock.json Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2020-03-25 12:50:45 +02:00
2017-07-31 22:51:24 +02:00
if (md.isEnum) {
value = value.toString();
2017-07-23 20:26:50 +02:00
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
2017-07-23 20:26:50 +02:00
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: string) => {
void updateSettingValue(key, itemValue);
}}
/>
</View>
{descriptionComp}
2017-07-23 20:26:50 +02:00
</View>
);
} else if (md.type === Setting.TYPE_BOOL) {
return this.renderToggle(key, md.label(), value, updateSettingValue, descriptionComp);
// 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:any) => updateSettingValue(key, value)} />
// </View>
// {descriptionComp}
// </View>
// );
} else if (md.type === Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
2023-01-03 22:22:44 +02:00
const minimum = 'minimum' in md ? md.minimum : 0;
const maximum = 'maximum' in md ? md.maximum : 10;
// 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
2017-07-25 23:55:26 +02:00
return (
2017-08-01 19:59:01 +02:00
<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={minimum} maximumValue={maximum} value={value} onValueChange={value => void updateSettingValue(key, value)} />
</View>
2017-07-25 23:55:26 +02:00
</View>
);
} else if (md.type === Setting.TYPE_STRING) {
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
return (
<TouchableNativeFeedback key={key} onPress={this.selectDirectoryButtonPress} style={this.styles().settingContainer}>
<View style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<Text style={this.styles().settingControl}>
{this.state.fileSystemSyncPath}
</Text>
</View>
</TouchableNativeFeedback>
);
}
return (
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View key={key} style={containerStyle}>
<Text key="label" style={this.styles().settingText}>
{md.label()}
</Text>
<TextInput autoCorrect={false} autoComplete="off" selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value: any) => void updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
</View>
{descriptionComp}
</View>
);
} else {
2019-10-09 21:35:13 +02:00
// throw new Error('Unsupported setting type: ' + md.type);
2017-07-23 20:26:50 +02:00
}
return output;
}
private renderFeatureFlags(settings: any, featureFlagKeys: string[]): any[] {
const updateSettingValue = (key: string, value: any) => {
return shared.updateSettingValue(this, key, value);
};
const output: any[] = [];
for (const key of featureFlagKeys) {
output.push(this.renderToggle(key, key, settings[key], updateSettingValue));
}
return output;
}
public render() {
const settings = this.state.settings;
2017-07-23 20:26:50 +02:00
2020-09-15 15:01:07 +02:00
const theme = themeStyle(this.props.themeId);
const settingComps = shared.settingsToComponents2(this, 'mobile', settings);
settingComps.push(this.renderHeader('tools', _('Tools')));
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
2019-07-22 00:27:13 +02:00
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.') }));
2023-07-18 15:58:06 +02:00
settingComps.push(this.renderHeader('export', _('Export')));
settingComps.push(<NoteExportButton key={'export_as_jex_button'} styles={this.styles()} />);
if (shim.mobilePlatform() === 'android') {
2023-07-18 15:58:06 +02:00
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('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 = (
2020-06-08 10:01:33 +02:00
<View style={this.styles().settingContainer} key="profileExport">
<Text style={{ ...this.styles().settingText, flex: 0 }}>Path:</Text>
<TextInput style={{ ...this.styles().textInput, paddingRight: 20, width: '75%', marginRight: 'auto' }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance} />
<Button title="OK" onPress={this.exportProfileButtonPress2_} />
</View>
);
settingComps.push(profileExportPrompt);
}
}
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
if (featureFlagKeys.length) {
settingComps.push(this.renderHeader('featureFlags', _('Feature flags')));
settingComps.push(<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>);
}
settingComps.push(this.renderHeader('moreInfo', _('More information')));
2018-03-09 11:09:13 +02:00
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>,
);
}
2018-03-09 11:09:13 +02:00
settingComps.push(
<View key="donate_link" style={this.styles().settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/donate/');
}}
>
<Text key="label" style={this.styles().linkText}>
{_('Make a donation')}
</Text>
2018-03-09 11:09:13 +02:00
</TouchableOpacity>
</View>,
2018-03-09 11:09:13 +02:00
);
2018-11-02 02:43:42 +02:00
settingComps.push(
<View key="website_link" style={this.styles().settingContainer}>
<TouchableOpacity
onPress={() => {
void 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={() => {
void Linking.openURL('https://joplinapp.org/privacy/');
}}
>
<Text key="label" style={this.styles().linkText}>
{_('Privacy Policy')}
</Text>
</TouchableOpacity>
</View>,
);
2017-07-23 20:26:50 +02:00
2018-11-02 02:43:42 +02:00
settingComps.push(
<View key="version_info_app" style={this.styles().settingContainer}>
2019-09-19 23:51:18 +02:00
<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>,
2018-11-02 02:43:42 +02:00
);
settingComps.push(
<View key="version_info_hermes" style={this.styles().settingContainer}>
<Text style={this.styles().settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
</View>,
);
2017-07-23 20:26:50 +02:00
return (
2020-09-15 15:01:07 +02:00
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Configuration')} showSaveButton={true} showSearchButton={false} showSideMenuButton={false} saveButtonDisabled={!this.state.changedSettingKeys.length} onSaveButtonPress={this.saveButton_press} />
<ScrollView ref={this.scrollViewRef_}>{settingComps}</ScrollView>
2017-07-23 20:26:50 +02:00
</View>
);
}
}
const ConfigScreen = connect((state: State) => {
return {
settings: state.settings,
2020-09-15 15:01:07 +02:00
themeId: state.settings.theme,
};
})(ConfigScreenComponent);
2017-07-23 20:26:50 +02:00
export default ConfigScreen;