mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Mobile: Settings screen: Create separate pages for each screen (#8567)
This commit is contained in:
parent
0340c7f65c
commit
672d028d29
@ -427,6 +427,7 @@ packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
@ -467,17 +468,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@ -600,6 +613,7 @@ packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -409,6 +409,7 @@ packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
@ -449,17 +450,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@ -582,6 +595,7 @@ packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
|
@ -12,7 +12,7 @@ const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const pathUtils = require('@joplin/lib/path-utils');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
|
||||
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
import restart from '../../services/restart';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
@ -35,9 +35,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
shared.init(this, reg);
|
||||
shared.init(reg);
|
||||
|
||||
this.state = {
|
||||
...shared.defaultScreenState,
|
||||
selectedSectionName: 'general',
|
||||
screenName: '',
|
||||
changedSettingKeys: [],
|
||||
@ -98,7 +99,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
|
||||
public sectionByName(name: string) {
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
|
||||
const sections = shared.settingsSections({ device: AppType.Desktop, settings: this.state.settings });
|
||||
for (const section of sections) {
|
||||
if (section.name === name) return section;
|
||||
}
|
||||
@ -699,7 +700,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
const hasChanges = this.hasChanges();
|
||||
|
||||
const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName);
|
||||
const settingComps = shared.settingsToComponents2(this, AppType.Desktop, settings, this.state.selectedSectionName);
|
||||
|
||||
// screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
|
||||
// These screens handle their own loading/saving of settings and have bespoke rendering.
|
||||
@ -708,7 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
if (screenComp) containerStyle.display = 'none';
|
||||
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings });
|
||||
const sections = shared.settingsSections({ device: AppType.Desktop, settings });
|
||||
|
||||
const needRestartComp: any = this.state.needRestart ? (
|
||||
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@ -93,7 +93,9 @@ export default function Sidebar(props: Props) {
|
||||
const selected = props.selection === section.name;
|
||||
return (
|
||||
<StyledListItem key={section.name} isSubSection={Setting.isSubSection(section.name)} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}>
|
||||
<StyledListItemIcon className={Setting.sectionNameToIcon(section.name)} />
|
||||
<StyledListItemIcon
|
||||
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
|
||||
/>
|
||||
<StyledListItemLabel>
|
||||
{Setting.sectionNameToLabel(section.name)}
|
||||
</StyledListItemLabel>
|
||||
|
44
packages/app-mobile/components/Icon.tsx
Normal file
44
packages/app-mobile/components/Icon.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { TextStyle } from 'react-native';
|
||||
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
style: TextStyle;
|
||||
|
||||
// If `null` is given, the content must be labeled elsewhere.
|
||||
accessibilityLabel: string|null;
|
||||
}
|
||||
|
||||
const Icon: React.FC<Props> = props => {
|
||||
// Matches:
|
||||
// 1. A prefix of word characters (\w+)
|
||||
// 2. A suffix of non-spaces (\S+)
|
||||
// An "fa-" at the beginning of the suffix is ignored.
|
||||
const nameMatch = props.name.match(/^(\w+)\s+(?:fa-)?(\S+)$/);
|
||||
|
||||
const namePrefix = nameMatch ? nameMatch[1] : '';
|
||||
const nameSuffix = nameMatch ? nameMatch[2] : props.name;
|
||||
|
||||
// If there's no label, make sure that the screen reader doesn't try
|
||||
// to read the characters from the icon font (they don't make sense
|
||||
// without the icon font applied).
|
||||
const accessibilityHidden = props.accessibilityLabel === null;
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
brand={namePrefix.startsWith('fab')}
|
||||
solid={namePrefix.startsWith('fas')}
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
aria-hidden={accessibilityHidden}
|
||||
importantForAccessibility={
|
||||
accessibilityHidden ? 'no-hide-descendants' : 'yes'
|
||||
}
|
||||
name={nameSuffix}
|
||||
style={props.style}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
@ -1,12 +1,12 @@
|
||||
const React = require('react');
|
||||
const { StyleSheet } = require('react-native');
|
||||
import * as React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
|
||||
const rootStyles_ = {};
|
||||
const rootStyles_: Record<number, any> = {};
|
||||
|
||||
class BaseScreenComponent extends React.Component {
|
||||
class BaseScreenComponent<Props, State> extends React.Component<Props, State> {
|
||||
|
||||
rootStyle(themeId) {
|
||||
protected rootStyle(themeId: number) {
|
||||
const theme = themeStyle(themeId);
|
||||
if (rootStyles_[themeId]) return rootStyles_[themeId];
|
||||
rootStyles_[themeId] = StyleSheet.create({
|
||||
@ -19,4 +19,5 @@ class BaseScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { BaseScreenComponent };
|
||||
export { BaseScreenComponent };
|
||||
export default BaseScreenComponent;
|
@ -1,214 +1,168 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
|
||||
import Slider from '@react-native-community/slider';
|
||||
const React = require('react');
|
||||
import { Platform, Linking, View, Switch, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from 'react-native';
|
||||
import * as React from 'react';
|
||||
import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } 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 { connect } from 'react-redux';
|
||||
import ScreenHeader from '../../ScreenHeader';
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../../base-screen.js');
|
||||
const { Dropdown } = require('../../Dropdown');
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import BaseScreenComponent from '../../base-screen';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
|
||||
import * as shared from '@joplin/lib/components/shared/config/config-shared';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
|
||||
import configScreenStyles from './configScreenStyles';
|
||||
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
|
||||
import NoteExportButton from './NoteExportSection/NoteExportButton';
|
||||
import ConfigScreenButton from './ConfigScreenButton';
|
||||
import SettingsButton from './SettingsButton';
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import { ReactNode } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import SectionHeader from './SectionHeader';
|
||||
import ExportProfileButton from './NoteExportSection/ExportProfileButton';
|
||||
import SettingComponent from './SettingComponent';
|
||||
import ExportDebugReportButton from './NoteExportSection/ExportDebugReportButton';
|
||||
import SectionSelector from './SectionSelector';
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
interface ConfigScreenState {
|
||||
settings: any;
|
||||
changedSettingKeys: string[];
|
||||
|
||||
fixingSearchIndex: boolean;
|
||||
checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null;
|
||||
showAdvancedSettings: boolean;
|
||||
|
||||
selectedSectionName: string|null;
|
||||
sidebarWidth: number;
|
||||
}
|
||||
|
||||
interface ConfigScreenProps {
|
||||
settings: any;
|
||||
themeId: number;
|
||||
navigation: any;
|
||||
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, ConfigScreenState> {
|
||||
public static navigationOptions(): any {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
private componentsY_: Record<string, number> = {};
|
||||
private styles_: Record<number, ConfigScreenStyles> = {};
|
||||
private scrollViewRef_: React.RefObject<ScrollView>;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
public constructor(props: ConfigScreenProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
creatingReport: false,
|
||||
profileExportStatus: 'idle',
|
||||
profileExportPath: '',
|
||||
fileSystemSyncPath: Setting.value('sync.2.path'),
|
||||
...shared.defaultScreenState,
|
||||
selectedSectionName: null,
|
||||
fixingSearchIndex: false,
|
||||
sidebarWidth: 100,
|
||||
};
|
||||
|
||||
this.scrollViewRef_ = React.createRef();
|
||||
this.scrollViewRef_ = React.createRef<ScrollView>();
|
||||
|
||||
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');
|
||||
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');
|
||||
};
|
||||
|
||||
this.handleSetting = this.handleSetting.bind(this);
|
||||
shared.init(reg);
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
};
|
||||
|
||||
private e2eeConfig_ = () => {
|
||||
void NavService.go('EncryptionConfig');
|
||||
};
|
||||
|
||||
private 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 shouldSetIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors');
|
||||
|
||||
await shared.saveSettings(this);
|
||||
|
||||
if (shouldSetIgnoreTlsErrors) {
|
||||
await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors'));
|
||||
}
|
||||
};
|
||||
|
||||
private syncStatusButtonPress_ = () => {
|
||||
void NavService.go('Status');
|
||||
};
|
||||
|
||||
private manageProfilesButtonPress_ = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'ProfileSwitcher',
|
||||
});
|
||||
};
|
||||
|
||||
private fixSearchEngineIndexButtonPress_ = async () => {
|
||||
this.setState({ fixingSearchIndex: true });
|
||||
await SearchEngine.instance().rebuildIndex();
|
||||
this.setState({ fixingSearchIndex: false });
|
||||
};
|
||||
|
||||
private logButtonPress_ = () => {
|
||||
void NavService.go('Log');
|
||||
};
|
||||
|
||||
private updateSidebarWidth = () => {
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
|
||||
let sidebarNewWidth = windowWidth;
|
||||
|
||||
const sidebarValidWidths = [280, 230];
|
||||
const maxFractionOfWindowSize = 1 / 3;
|
||||
for (const width of sidebarValidWidths) {
|
||||
if (width < windowWidth * maxFractionOfWindowSize) {
|
||||
sidebarNewWidth = width;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ sidebarWidth: sidebarNewWidth });
|
||||
};
|
||||
|
||||
private navigationFillsScreen() {
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
return this.state.sidebarWidth > windowWidth / 2;
|
||||
}
|
||||
|
||||
private switchSectionPress_ = (section: string) => {
|
||||
const label = Setting.sectionNameToLabel(section);
|
||||
AccessibilityInfo.announceForAccessibility(_('Opening section %s', label));
|
||||
this.setState({ selectedSectionName: section });
|
||||
};
|
||||
|
||||
private showSectionNavigation_ = () => {
|
||||
this.setState({ selectedSectionName: null });
|
||||
};
|
||||
|
||||
public async checkFilesystemPermission() {
|
||||
if (Platform.OS !== 'android') {
|
||||
// Not implemented yet
|
||||
@ -225,7 +179,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
public styles() {
|
||||
public styles(): ConfigScreenStyles {
|
||||
const themeId = this.props.themeId;
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
@ -258,6 +212,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
await BackButtonService.back();
|
||||
};
|
||||
|
||||
// Show navigation when pressing "back" (unless always visible).
|
||||
if (this.state.selectedSectionName && this.navigationFillsScreen()) {
|
||||
this.showSectionNavigation_();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.state.changedSettingKeys.length > 0) {
|
||||
const dialogTitle: string|null = null;
|
||||
Alert.alert(
|
||||
@ -284,6 +244,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.navigation.state.sectionName) {
|
||||
this.setState({ selectedSectionName: this.props.navigation.state.sectionName });
|
||||
setTimeout(() => {
|
||||
this.scrollViewRef_.current.scrollTo({
|
||||
x: 0,
|
||||
@ -294,24 +255,17 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
BackButtonService.addHandler(this.handleBackButtonPress);
|
||||
Dimensions.addEventListener('change', this.updateSidebarWidth);
|
||||
this.updateSidebarWidth();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
BackButtonService.removeHandler(this.handleBackButtonPress);
|
||||
}
|
||||
|
||||
public renderHeader(key: string, title: string) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
|
||||
return (
|
||||
<ConfigScreenButton
|
||||
<SettingsButton
|
||||
key={key}
|
||||
title={title}
|
||||
clickHandler={clickHandler}
|
||||
@ -322,9 +276,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
public sectionToComponent(key: string, section: any, settings: any) {
|
||||
public sectionToComponent(key: string, section: any, settings: any, isSelected: boolean) {
|
||||
const settingComps = [];
|
||||
|
||||
const styleSheet = this.styles().styleSheet;
|
||||
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
|
||||
@ -335,10 +291,10 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusComp = !messages.length ? null : (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={this.styles().descriptionText}>{messages[0]}</Text>
|
||||
<Text style={this.styles().styleSheet.descriptionText}>{messages[0]}</Text>
|
||||
{messages.length >= 1 ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Text style={this.styles().descriptionText}>{messages[1]}</Text>
|
||||
<Text style={this.styles().styleSheet.descriptionText}>{messages[1]}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
@ -360,8 +316,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
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>
|
||||
<View style={this.styles().styleSheet.settingContainerNoBottomBorder}>
|
||||
<Text style={this.styles().styleSheet.settingText}>{_('Email to note')}</Text>
|
||||
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text>
|
||||
</View>
|
||||
{
|
||||
@ -376,11 +332,129 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (section.name === '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_));
|
||||
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 (section.name === 'export') {
|
||||
settingComps.push(<NoteExportButton key='export_as_jex_button' styles={this.styles()} />);
|
||||
settingComps.push(<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>);
|
||||
settingComps.push(<ExportProfileButton key='export_data' styles={this.styles()}/>);
|
||||
}
|
||||
|
||||
if (section.name === 'moreInfo') {
|
||||
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={styleSheet.settingContainer}>
|
||||
<View key="permission_info_wrapper">
|
||||
<Text key="perm1a" style={styleSheet.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={styleSheet.permissionText}>
|
||||
{_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')}
|
||||
</Text>
|
||||
<Text key="perm3" style={styleSheet.permissionText}>
|
||||
{_('- Camera: to allow taking a picture and attaching it to a note.')}
|
||||
</Text>
|
||||
<Text key="perm4" style={styleSheet.permissionText}>
|
||||
{_('- Location: to allow attaching geo-location information to a note.')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
}
|
||||
|
||||
settingComps.push(
|
||||
<View key="donate_link" style={styleSheet.settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
void Linking.openURL('https://joplinapp.org/donate/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={styleSheet.linkText}>
|
||||
{_('Make a donation')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>,
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="website_link" style={styleSheet.settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
void Linking.openURL('https://joplinapp.org/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={styleSheet.linkText}>
|
||||
{_('Joplin website')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>,
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="privacy_link" style={styleSheet.settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
void Linking.openURL('https://joplinapp.org/privacy/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={styleSheet.linkText}>
|
||||
{_('Privacy Policy')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>,
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_app" style={styleSheet.settingContainer}>
|
||||
<Text style={styleSheet.settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_db" style={styleSheet.settingContainer}>
|
||||
<Text style={styleSheet.settingText}>{_('Database v%s', reg.db().version())}</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_fts" style={styleSheet.settingContainer}>
|
||||
<Text style={styleSheet.settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_hermes" style={styleSheet.settingContainer}>
|
||||
<Text style={styleSheet.settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
|
||||
</View>,
|
||||
);
|
||||
|
||||
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
|
||||
if (featureFlagKeys.length) {
|
||||
const headerKey = 'featureFlags';
|
||||
settingComps.push(<SectionHeader
|
||||
key={headerKey}
|
||||
styles={this.styles().styleSheet}
|
||||
title={_('Feature flags')}
|
||||
onLayout={event => this.onHeaderLayout(headerKey, event)}
|
||||
/>);
|
||||
|
||||
settingComps.push(<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>);
|
||||
}
|
||||
}
|
||||
|
||||
if (!settingComps.length) return null;
|
||||
if (!isSelected) 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>
|
||||
);
|
||||
@ -392,22 +466,18 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
return (
|
||||
<View key={key}>
|
||||
<View style={this.containerStyle(false)}>
|
||||
<Text key="label" style={this.styles().switchSettingText}>
|
||||
<View style={this.styles().getContainerStyle(false)}>
|
||||
<Text key="label" style={this.styles().styleSheet.switchSettingText}>
|
||||
{label}
|
||||
</Text>
|
||||
<Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => void updateSettingValue(key, value)} />
|
||||
<Switch key="control" style={this.styles().styleSheet.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> {
|
||||
private handleSetting = async (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.
|
||||
@ -428,123 +498,24 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
public settingToComponent(key: string, value: any) {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
const output: any = null;
|
||||
|
||||
const updateSettingValue = async (key: string, value: any) => {
|
||||
const handled = await this.handleSetting(key, value);
|
||||
if (!handled) 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 = this.containerStyle(!!settingDescription);
|
||||
|
||||
if (md.isEnum) {
|
||||
value = value.toString();
|
||||
|
||||
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
|
||||
|
||||
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}
|
||||
</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;
|
||||
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
|
||||
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={minimum} maximumValue={maximum} value={value} onValueChange={value => void updateSettingValue(key, value)} />
|
||||
</View>
|
||||
</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 {
|
||||
// throw new Error('Unsupported setting type: ' + md.type);
|
||||
}
|
||||
|
||||
return output;
|
||||
return (
|
||||
<SettingComponent
|
||||
key={key}
|
||||
settingId={key}
|
||||
value={value}
|
||||
themeId={this.props.themeId}
|
||||
updateSettingValue={updateSettingValue}
|
||||
styles={this.styles()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderFeatureFlags(settings: any, featureFlagKeys: string[]): any[] {
|
||||
@ -562,139 +533,77 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
public render() {
|
||||
const settings = this.state.settings;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const showAsSidebar = !this.navigationFillsScreen();
|
||||
|
||||
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_));
|
||||
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.') }));
|
||||
|
||||
settingComps.push(this.renderHeader('export', _('Export')));
|
||||
settingComps.push(<NoteExportButton key={'export_as_jex_button'} styles={this.styles()} />);
|
||||
|
||||
if (shim.mobilePlatform() === '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('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, 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);
|
||||
}
|
||||
// If the navigation is a sidebar, always show a section.
|
||||
let currentSectionName = this.state.selectedSectionName;
|
||||
if (showAsSidebar && !currentSectionName) {
|
||||
currentSectionName = 'general';
|
||||
}
|
||||
|
||||
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>);
|
||||
}
|
||||
const sectionSelector = (
|
||||
<SectionSelector
|
||||
selectedSectionName={currentSectionName}
|
||||
styles={this.styles()}
|
||||
settings={settings}
|
||||
openSection={this.switchSectionPress_}
|
||||
width={this.state.sidebarWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
settingComps.push(this.renderHeader('moreInfo', _('More information')));
|
||||
let currentSection: ReactNode;
|
||||
if (currentSectionName) {
|
||||
const settingComps = shared.settingsToComponents2(
|
||||
this, AppType.Mobile, settings, currentSectionName,
|
||||
|
||||
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
|
||||
// TODO: Remove this cast. Currently necessary because of different versions
|
||||
// of React in lib/ and app-mobile/
|
||||
) as ReactNode[];
|
||||
|
||||
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>,
|
||||
currentSection = (
|
||||
<ScrollView
|
||||
ref={this.scrollViewRef_}
|
||||
style={{ flexGrow: 1 }}
|
||||
>
|
||||
{settingComps}
|
||||
</ScrollView>
|
||||
);
|
||||
} else {
|
||||
currentSection = sectionSelector;
|
||||
}
|
||||
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
</View>,
|
||||
);
|
||||
let mainComponent;
|
||||
if (showAsSidebar && currentSectionName) {
|
||||
mainComponent = (
|
||||
<View style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
{sectionSelector}
|
||||
<View style={{ width: 10 }}/>
|
||||
{currentSection}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
mainComponent = currentSection;
|
||||
}
|
||||
|
||||
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>,
|
||||
);
|
||||
|
||||
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>,
|
||||
);
|
||||
|
||||
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>,
|
||||
);
|
||||
let screenHeadingText = _('Configuration');
|
||||
if (currentSectionName) {
|
||||
screenHeadingText = Setting.sectionNameToLabel(currentSectionName);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<ScreenHeader
|
||||
title={screenHeadingText}
|
||||
showSaveButton={true}
|
||||
showSearchButton={false}
|
||||
showSideMenuButton={false}
|
||||
saveButtonDisabled={!this.state.changedSettingKeys.length}
|
||||
onSaveButtonPress={this.saveButton_press}
|
||||
/>
|
||||
{mainComponent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import { TouchableNativeFeedback, View, Text } from 'react-native';
|
||||
import Setting, { SettingItem } from '@joplin/lib/models/Setting';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
settingMetadata: SettingItem;
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
}
|
||||
|
||||
const FileSystemPathSelector: FunctionComponent<Props> = props => {
|
||||
const [fileSystemPath, setFileSystemPath] = useState<string>('');
|
||||
|
||||
const settingId = props.settingMetadata.key;
|
||||
|
||||
useEffect(() => {
|
||||
setFileSystemPath(Setting.value(settingId));
|
||||
}, [settingId]);
|
||||
|
||||
const selectDirectoryButtonPress = useCallback(async () => {
|
||||
try {
|
||||
const doc = await openDocumentTree(true);
|
||||
if (doc?.uri) {
|
||||
setFileSystemPath(doc.uri);
|
||||
await props.updateSettingValue(settingId, doc.uri);
|
||||
} else {
|
||||
throw new Error('User cancelled operation');
|
||||
}
|
||||
} catch (e) {
|
||||
reg.logger().info('Didn\'t pick sync dir: ', e);
|
||||
}
|
||||
}, [props.updateSettingValue, settingId]);
|
||||
|
||||
// Unsupported on non-Android platforms.
|
||||
if (!shim.fsDriver().isUsingAndroidSAF()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
return (
|
||||
<TouchableNativeFeedback
|
||||
onPress={selectDirectoryButtonPress}
|
||||
style={styleSheet.settingContainer}
|
||||
>
|
||||
<View style={styleSheet.settingContainer}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{props.settingMetadata.label()}
|
||||
</Text>
|
||||
<Text style={styleSheet.settingControl}>
|
||||
{fileSystemPath}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemPathSelector;
|
@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import exportDebugReport from './utils/exportDebugReport';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import SettingsButton from '../SettingsButton';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
}
|
||||
|
||||
const ExportDebugReportButton = (props: Props) => {
|
||||
const [creatingReport, setCreatingReport] = useState(false);
|
||||
|
||||
const exportDebugButtonPress = useCallback(async () => {
|
||||
setCreatingReport(true);
|
||||
|
||||
await exportDebugReport();
|
||||
|
||||
setCreatingReport(false);
|
||||
}, [setCreatingReport]);
|
||||
|
||||
const exportDebugReportButton = (
|
||||
<SettingsButton
|
||||
title={creatingReport ? _('Creating report...') : _('Export Debug Report')}
|
||||
clickHandler={exportDebugButtonPress}
|
||||
styles={props.styles}
|
||||
disabled={creatingReport}
|
||||
/>
|
||||
);
|
||||
|
||||
// The debug functionality is only supported on Android.
|
||||
if (shim.mobilePlatform() !== 'android') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exportDebugReportButton;
|
||||
};
|
||||
|
||||
export default ExportDebugReportButton;
|
@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { View, Button } from 'react-native';
|
||||
import { TextInput } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import exportProfile from './utils/exportProfile';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import SettingsButton from '../SettingsButton';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
}
|
||||
|
||||
const ExportProfileButton = (props: Props) => {
|
||||
const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle');
|
||||
const [profileExportPath, setProfileExportPath] = useState<string>('');
|
||||
|
||||
const exportProfileButtonPress = useCallback(async () => {
|
||||
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
|
||||
if (!externalDir) {
|
||||
return;
|
||||
}
|
||||
const p = profileExportPath ? profileExportPath : `${externalDir}/JoplinProfileExport`;
|
||||
|
||||
setProfileExportStatus('prompt');
|
||||
setProfileExportPath(p);
|
||||
}, [profileExportPath]);
|
||||
|
||||
const exportProfileButton = (
|
||||
<SettingsButton
|
||||
styles={props.styles}
|
||||
title={profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile')}
|
||||
clickHandler={exportProfileButtonPress}
|
||||
description={_('For debugging purpose only: export your profile to an external SD card.')}
|
||||
disabled={profileExportStatus === 'exporting'}
|
||||
/>
|
||||
);
|
||||
|
||||
const exportProfileButtonPress2 = useCallback(async () => {
|
||||
setProfileExportStatus('exporting');
|
||||
|
||||
await exportProfile(profileExportPath);
|
||||
|
||||
setProfileExportStatus('idle');
|
||||
}, [profileExportPath]);
|
||||
|
||||
const profileExportPrompt = (
|
||||
<View>
|
||||
<TextInput
|
||||
label={_('Path:')}
|
||||
onChangeText={text => setProfileExportPath(text)}
|
||||
value={profileExportPath}
|
||||
placeholder="/path/to/sdcard"
|
||||
keyboardAppearance={props.styles.keyboardAppearance} />
|
||||
<Button
|
||||
onPress={exportProfileButtonPress2}
|
||||
title={_('OK')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const mainContent = (
|
||||
<>
|
||||
{exportProfileButton}
|
||||
{profileExportStatus === 'prompt' ? profileExportPrompt : null}
|
||||
</>
|
||||
);
|
||||
|
||||
// The debug functionality is only supported on Android.
|
||||
if (shim.mobilePlatform() !== 'android') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mainContent;
|
||||
};
|
||||
|
||||
export default ExportProfileButton;
|
@ -7,10 +7,10 @@ import { FunctionComponent, useCallback, useState } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { join } from 'path';
|
||||
import Share from 'react-native-share';
|
||||
import exportAllFolders, { makeExportCacheDirectory } from './exportAllFolders';
|
||||
import exportAllFolders, { makeExportCacheDirectory } from './utils/exportAllFolders';
|
||||
import { ExportProgressState } from '@joplin/lib/services/interop/types';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import ConfigScreenButton from '../ConfigScreenButton';
|
||||
import SettingsButton from '../SettingsButton';
|
||||
|
||||
const logger = Logger.create('NoteExportButton');
|
||||
|
||||
@ -83,7 +83,7 @@ const NoteExportButton: FunctionComponent<Props> = props => {
|
||||
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
|
||||
|
||||
const startOrCancelExportButton = (
|
||||
<ConfigScreenButton
|
||||
<SettingsButton
|
||||
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')}
|
||||
disabled={exportStatus === ExportStatus.Exporting}
|
||||
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null}
|
||||
@ -96,14 +96,14 @@ const NoteExportButton: FunctionComponent<Props> = props => {
|
||||
return startOrCancelExportButton;
|
||||
} else {
|
||||
const warningComponent = (
|
||||
<Text style={props.styles.warningText}>
|
||||
<Text style={props.styles.styleSheet.warningText}>
|
||||
{_('Warnings:\n%s', warnings)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const exportSummary = (
|
||||
<View style={props.styles.settingContainer}>
|
||||
<Text style={props.styles.descriptionText}>{_('Exported successfully!')}</Text>
|
||||
<View style={props.styles.styleSheet.settingContainer}>
|
||||
<Text style={props.styles.styleSheet.descriptionText}>{_('Exported successfully!')}</Text>
|
||||
{warnings.length > 0 ? warningComponent : null}
|
||||
</View>
|
||||
);
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ReportService from '@joplin/lib/services/ReportService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import time from '@joplin/lib/time';
|
||||
|
||||
const exportDebugReport = async () => {
|
||||
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) {
|
||||
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}`);
|
||||
};
|
||||
|
||||
export default exportDebugReport;
|
@ -0,0 +1,35 @@
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const exportProfile = async (profileExportPath: string) => {
|
||||
const dbPath = '/data/data/net.cozic.joplin/databases';
|
||||
const exportPath = profileExportPath;
|
||||
const resourcePath = `${exportPath}/resources`;
|
||||
try {
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default exportProfile;
|
@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ConfigScreenStyleSheet } from './configScreenStyles';
|
||||
import { View, Text, LayoutChangeEvent } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyleSheet;
|
||||
title: string;
|
||||
onLayout?: (event: LayoutChangeEvent)=> void;
|
||||
}
|
||||
|
||||
const SectionHeader: React.FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<View
|
||||
style={props.styles.headerWrapperStyle}
|
||||
onLayout={props.onLayout}
|
||||
>
|
||||
<Text style={props.styles.headerTextStyle}>
|
||||
{props.title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeader;
|
@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import { FlatList, Text, Pressable, View } from 'react-native';
|
||||
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
|
||||
import Icon from '../../Icon';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
|
||||
width: number|undefined;
|
||||
|
||||
settings: any;
|
||||
selectedSectionName: string|null;
|
||||
openSection: (sectionName: string)=> void;
|
||||
}
|
||||
|
||||
const SectionSelector: FunctionComponent<Props> = props => {
|
||||
const sections = useMemo(() => {
|
||||
return settingsSections({ device: AppType.Mobile, settings: props.settings });
|
||||
}, [props.settings]);
|
||||
const styles = props.styles.styleSheet;
|
||||
|
||||
const itemHeight = styles.sidebarButton.height;
|
||||
|
||||
const onRenderButton = ({ item }: { item: SettingMetadataSection }) => {
|
||||
const section = item;
|
||||
const selected = props.selectedSectionName === section.name;
|
||||
const icon = Setting.sectionNameToIcon(section.name, AppType.Mobile);
|
||||
const label = Setting.sectionNameToLabel(section.name);
|
||||
const shortDescription = Setting.sectionMetadataToSummary(section);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={section.name}
|
||||
role='tab'
|
||||
aria-selected={selected}
|
||||
onPress={() => props.openSection(section.name)}
|
||||
style={selected ? styles.selectedSidebarButton : styles.sidebarButton}
|
||||
>
|
||||
<Icon
|
||||
name={icon}
|
||||
accessibilityLabel={null}
|
||||
style={styles.sidebarIcon}
|
||||
/>
|
||||
<View style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<Text
|
||||
style={selected ? styles.sidebarSelectedButtonText : styles.sidebarButtonMainText}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.sidebarButtonDescriptionText}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{shortDescription ?? ''}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const [flatListRef, setFlatListRef] = useState<FlatList|null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (flatListRef && props.selectedSectionName) {
|
||||
let selectedIndex = 0;
|
||||
for (const section of sections) {
|
||||
if (section.name === props.selectedSectionName) {
|
||||
break;
|
||||
}
|
||||
selectedIndex ++;
|
||||
}
|
||||
|
||||
flatListRef.scrollToIndex({
|
||||
index: selectedIndex,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
}
|
||||
}, [props.selectedSectionName, flatListRef, sections]);
|
||||
|
||||
return (
|
||||
<View style={{ width: props.width, flexDirection: 'column' }}>
|
||||
<FlatList
|
||||
role='tablist'
|
||||
ref={setFlatListRef}
|
||||
data={sections}
|
||||
renderItem={onRenderButton}
|
||||
keyExtractor={item => item.name}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: itemHeight, offset: itemHeight * index, index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionSelector;
|
@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { View, Text, TextInput } from 'react-native';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Dropdown from '../../Dropdown';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import SettingsToggle from './SettingsToggle';
|
||||
import FileSystemPathSelector from './FileSystemPathSelector';
|
||||
import shim from '@joplin/lib/shim';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
|
||||
interface Props {
|
||||
settingId: string;
|
||||
|
||||
// The value associated with the given settings key
|
||||
value: any;
|
||||
|
||||
styles: ConfigScreenStyles;
|
||||
themeId: number;
|
||||
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
}
|
||||
|
||||
|
||||
const SettingComponent: React.FunctionComponent<Props> = props => {
|
||||
const themeId = props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
const output: any = null;
|
||||
|
||||
const md = Setting.settingMetadata(props.settingId);
|
||||
const settingDescription = md.description ? md.description() : '';
|
||||
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
const descriptionComp = !settingDescription ? null : <Text style={styleSheet.settingDescriptionText}>{settingDescription}</Text>;
|
||||
const containerStyle = props.styles.getContainerStyle(!!settingDescription);
|
||||
|
||||
if (md.isEnum) {
|
||||
const value = props.value.toString();
|
||||
|
||||
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
|
||||
|
||||
return (
|
||||
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
||||
<View style={containerStyle}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<Dropdown
|
||||
key="control"
|
||||
items={items as any}
|
||||
selectedValue={value}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue: string) => {
|
||||
void props.updateSettingValue(props.settingId, itemValue);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BOOL) {
|
||||
return (
|
||||
<SettingsToggle
|
||||
settingId={props.settingId}
|
||||
value={props.value}
|
||||
themeId={props.themeId}
|
||||
styles={props.styles}
|
||||
label={md.label()}
|
||||
updateSettingValue={props.updateSettingValue}
|
||||
description={descriptionComp}
|
||||
/>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
const unitLabel = md.unitLabel ? md.unitLabel(props.value) : props.value;
|
||||
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
|
||||
return (
|
||||
<View key={props.settingId} style={styleSheet.settingContainer}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
||||
<Text style={styleSheet.sliderUnits}>{unitLabel}</Text>
|
||||
<Slider
|
||||
key="control"
|
||||
style={{ flex: 1 }}
|
||||
step={md.step}
|
||||
minimumValue={minimum}
|
||||
maximumValue={maximum}
|
||||
value={props.value}
|
||||
onValueChange={newValue => void props.updateSettingValue(props.settingId, newValue)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
|
||||
return (
|
||||
<FileSystemPathSelector
|
||||
styles={props.styles}
|
||||
settingMetadata={md}
|
||||
updateSettingValue={props.updateSettingValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
||||
<View key={props.settingId} style={containerStyle}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<TextInput
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.settingKeyboardAppearance}
|
||||
autoCapitalize="none"
|
||||
key="control"
|
||||
style={styleSheet.settingControl}
|
||||
value={props.value}
|
||||
onChangeText={(newValue: string) => void props.updateSettingValue(props.settingId, newValue)}
|
||||
secureTextEntry={!!md.secure}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
// TODO: Not yet supported
|
||||
} else if (Setting.value('env') === 'dev') {
|
||||
throw new Error(`Unsupported setting type: ${md.type}`);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export default SettingComponent;
|
@ -6,25 +6,27 @@ import { ConfigScreenStyles } from './configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
clickHandler: ()=> void;
|
||||
styles: ConfigScreenStyles;
|
||||
disabled?: boolean;
|
||||
statusComponent?: ReactNode;
|
||||
}
|
||||
|
||||
const ConfigScreenButton: FunctionComponent<Props> = props => {
|
||||
const SettingsButton: FunctionComponent<Props> = props => {
|
||||
const styles = props.styles.styleSheet;
|
||||
|
||||
let descriptionComp = null;
|
||||
if (props.description) {
|
||||
descriptionComp = (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={props.styles.descriptionText}>{props.description}</Text>
|
||||
<Text style={styles.descriptionText}>{props.description}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={props.styles.settingContainer}>
|
||||
<View style={styles.settingContainer}>
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button title={props.title} onPress={props.clickHandler} disabled={!!props.disabled} />
|
||||
@ -35,4 +37,4 @@ const ConfigScreenButton: FunctionComponent<Props> = props => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default ConfigScreenButton;
|
||||
export default SettingsButton;
|
@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent, ReactNode } from 'react';
|
||||
|
||||
import { View, Text, Switch } from 'react-native';
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
settingId: string;
|
||||
value: any;
|
||||
|
||||
themeId: number;
|
||||
styles: ConfigScreenStyles;
|
||||
|
||||
label: string;
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
const SettingsToggle: FunctionComponent<Props> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={props.styles.getContainerStyle(false)}>
|
||||
<Text key="label" style={styleSheet.switchSettingText}>
|
||||
{props.label}
|
||||
</Text>
|
||||
<Switch
|
||||
key="control"
|
||||
style={styleSheet.switchSettingControl}
|
||||
trackColor={{ false: theme.dividerColor }}
|
||||
value={props.value}
|
||||
onValueChange={(value: boolean) => void props.updateSettingValue(props.settingId, value)}
|
||||
/>
|
||||
</View>
|
||||
{props.description}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsToggle;
|
@ -1,13 +1,16 @@
|
||||
import { TextStyle, ViewStyle, StyleSheet } from 'react-native';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
|
||||
export interface ConfigScreenStyles {
|
||||
type SidebarButtonStyle = ViewStyle & { height: number };
|
||||
|
||||
export interface ConfigScreenStyleSheet {
|
||||
body: ViewStyle;
|
||||
|
||||
settingContainer: ViewStyle;
|
||||
settingContainerNoBottomBorder: ViewStyle;
|
||||
headerWrapperStyle: ViewStyle;
|
||||
|
||||
headerTextStyle: TextStyle;
|
||||
settingText: TextStyle;
|
||||
linkText: TextStyle;
|
||||
descriptionText: TextStyle;
|
||||
@ -22,9 +25,24 @@ export interface ConfigScreenStyles {
|
||||
switchSettingContainer: ViewStyle;
|
||||
switchSettingControl: TextStyle;
|
||||
|
||||
sidebarButton: SidebarButtonStyle;
|
||||
sidebarIcon: TextStyle;
|
||||
selectedSidebarButton: SidebarButtonStyle;
|
||||
sidebarButtonMainText: TextStyle;
|
||||
sidebarSelectedButtonText: TextStyle;
|
||||
sidebarButtonDescriptionText: TextStyle;
|
||||
|
||||
settingControl: TextStyle;
|
||||
}
|
||||
|
||||
export interface ConfigScreenStyles {
|
||||
styleSheet: ConfigScreenStyleSheet;
|
||||
|
||||
selectedSectionButtonColor: string;
|
||||
keyboardAppearance: 'default'|'light'|'dark';
|
||||
getContainerStyle(hasDescription: boolean): ViewStyle;
|
||||
}
|
||||
|
||||
const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
@ -54,7 +72,29 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
borderBottomColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const styles: ConfigScreenStyles = {
|
||||
const sidebarButtonHeight = theme.fontSize * 4 + 5;
|
||||
const sidebarButton: SidebarButtonStyle = {
|
||||
height: sidebarButtonHeight,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingEnd: theme.marginRight,
|
||||
};
|
||||
|
||||
const sidebarButtonMainText: TextStyle = {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
const sidebarButtonDescriptionText: TextStyle = {
|
||||
...sidebarButtonMainText,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
color: theme.color,
|
||||
opacity: 0.75,
|
||||
};
|
||||
|
||||
|
||||
const styles: ConfigScreenStyleSheet = {
|
||||
body: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
@ -119,6 +159,8 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
|
||||
headerTextStyle: theme.headerStyle,
|
||||
|
||||
headerWrapperStyle: {
|
||||
...settingContainerStyle,
|
||||
...theme.headerWrapperStyle,
|
||||
@ -129,9 +171,39 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
color: undefined,
|
||||
flex: 0,
|
||||
},
|
||||
|
||||
|
||||
sidebarButton,
|
||||
selectedSidebarButton: {
|
||||
...sidebarButton,
|
||||
backgroundColor: theme.selectedColor,
|
||||
},
|
||||
|
||||
sidebarButtonMainText: sidebarButtonMainText,
|
||||
sidebarIcon: {
|
||||
...sidebarButtonMainText,
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
width: sidebarButtonHeight * 0.8,
|
||||
},
|
||||
sidebarSelectedButtonText: {
|
||||
...sidebarButtonMainText,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
sidebarButtonDescriptionText,
|
||||
};
|
||||
|
||||
return StyleSheet.create(styles);
|
||||
const styleSheet = StyleSheet.create(styles);
|
||||
|
||||
return {
|
||||
styleSheet,
|
||||
|
||||
selectedSectionButtonColor: theme.selectedColor,
|
||||
keyboardAppearance: theme.keyboardAppearance,
|
||||
getContainerStyle: (hasDescription) => {
|
||||
return !hasDescription ? styleSheet.settingContainer : styleSheet.settingContainerNoBottomBorder;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default configScreenStyles;
|
||||
|
10
packages/app-mobile/components/screens/ConfigScreen/types.ts
Normal file
10
packages/app-mobile/components/screens/ConfigScreen/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export interface CustomSettingSection {
|
||||
component: ReactElement;
|
||||
icon: string;
|
||||
title: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
export type UpdateSettingValueCallback = (key: string, value: any)=> Promise<void>;
|
@ -32,7 +32,7 @@ const { Checkbox } = require('../checkbox.js');
|
||||
import { _, currentLocale } from '@joplin/lib/locale';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { themeStyle, editorFont } = require('../global-style.js');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
@ -13,7 +13,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import ActionButton from '../ActionButton';
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { BackButtonService } = require('../../services/back-button.js');
|
||||
import { AppState } from '../../utils/types';
|
||||
|
||||
|
@ -4,7 +4,7 @@ const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const Shared = require('@joplin/lib/components/shared/dropbox-login-shared');
|
||||
|
@ -5,7 +5,7 @@ const { connect } = require('react-redux');
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { default: FolderPicker } = require('../FolderPicker');
|
||||
|
@ -7,7 +7,7 @@ const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const parseUri = require('@joplin/lib/parseUri');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
@ -7,7 +7,7 @@ const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
const { NoteItem } = require('../note-item.js');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
import SearchEngineUtils from '@joplin/lib/services/searchengine/SearchEngineUtils';
|
||||
|
@ -6,7 +6,7 @@ const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const ReportService = require('@joplin/lib/services/ReportService').default;
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
|
||||
class StatusScreenComponent extends BaseScreenComponent {
|
||||
|
@ -6,7 +6,7 @@ const Tag = require('@joplin/lib/models/Tag').default;
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
|
||||
class TagsScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
|
@ -1,24 +1,48 @@
|
||||
const Setting = require('../../../models/Setting').default;
|
||||
const SyncTargetRegistry = require('../../../SyncTargetRegistry').default;
|
||||
import Setting, { AppType } from '../../../models/Setting';
|
||||
import SyncTargetRegistry from '../../../SyncTargetRegistry';
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
const { _ } = require('../../../locale');
|
||||
const { createSelector } = require('reselect');
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
import { createSelector } from 'reselect';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
import { type Registry } from '../../../registry';
|
||||
|
||||
const logger = Logger.create('config-shared');
|
||||
|
||||
const shared = {};
|
||||
interface ConfigScreenState {
|
||||
checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null;
|
||||
settings: any;
|
||||
changedSettingKeys: string[];
|
||||
showAdvancedSettings: boolean;
|
||||
}
|
||||
|
||||
shared.onSettingsSaved = () => {};
|
||||
export const defaultScreenState: ConfigScreenState = {
|
||||
checkSyncConfigResult: null,
|
||||
settings: {},
|
||||
changedSettingKeys: [],
|
||||
showAdvancedSettings: false,
|
||||
};
|
||||
|
||||
shared.init = function(comp, reg) {
|
||||
if (!comp.state) comp.state = {};
|
||||
comp.state.checkSyncConfigResult = null;
|
||||
comp.state.settings = {};
|
||||
comp.state.changedSettingKeys = [];
|
||||
comp.state.showAdvancedSettings = false;
|
||||
interface ConfigScreenComponent {
|
||||
settingToComponent(settingId: string, setting: any): ReactNode;
|
||||
sectionToComponent(sectionName: string, section: any, settings: any, isSelected: boolean): ReactNode;
|
||||
|
||||
shared.onSettingsSaved = (event) => {
|
||||
state: Partial<ConfigScreenState>;
|
||||
|
||||
setState(callbackOrNew: any, callback?: ()=> void): void;
|
||||
}
|
||||
|
||||
interface SettingsSavedEvent {
|
||||
savedSettingKeys: string[];
|
||||
}
|
||||
|
||||
type OnSettingsSavedCallback = (event: SettingsSavedEvent)=> void;
|
||||
|
||||
let onSettingsSaved: OnSettingsSavedCallback = () => {};
|
||||
|
||||
export const init = (reg: Registry) => {
|
||||
onSettingsSaved = (event) => {
|
||||
const savedSettingKeys = event.savedSettingKeys;
|
||||
|
||||
// After changing the sync settings we immediately trigger a sync
|
||||
@ -34,13 +58,13 @@ shared.init = function(comp, reg) {
|
||||
};
|
||||
};
|
||||
|
||||
shared.advancedSettingsButton_click = (comp) => {
|
||||
comp.setState(state => {
|
||||
export const advancedSettingsButton_click = (comp: ConfigScreenComponent) => {
|
||||
comp.setState((state: ConfigScreenState) => {
|
||||
return { showAdvancedSettings: !state.showAdvancedSettings };
|
||||
});
|
||||
};
|
||||
|
||||
shared.checkSyncConfig = async function(comp, settings) {
|
||||
export const checkSyncConfig = async (comp: ConfigScreenComponent, settings: any) => {
|
||||
const syncTargetId = settings['sync.target'];
|
||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
|
||||
|
||||
@ -54,12 +78,12 @@ shared.checkSyncConfig = async function(comp, settings) {
|
||||
|
||||
if (result.ok) {
|
||||
// Users often expect config to be auto-saved at this point, if the config check was successful
|
||||
shared.saveSettings(comp);
|
||||
saveSettings(comp);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
shared.checkSyncConfigMessages = function(comp) {
|
||||
export const checkSyncConfigMessages = (comp: ConfigScreenComponent) => {
|
||||
const result = comp.state.checkSyncConfigResult;
|
||||
const output = [];
|
||||
|
||||
@ -75,10 +99,10 @@ shared.checkSyncConfigMessages = function(comp) {
|
||||
return output;
|
||||
};
|
||||
|
||||
shared.updateSettingValue = function(comp, key, value, callback = null) {
|
||||
export const updateSettingValue = (comp: ConfigScreenComponent, key: string, value: any, callback?: ()=> void) => {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
comp.setState(state => {
|
||||
comp.setState((state: ConfigScreenState) => {
|
||||
// @react-native-community/slider (4.4.0) will emit a valueChanged event
|
||||
// when the component is mounted, even though the value hasn't changed.
|
||||
// We should ignore this, otherwise it will mark the settings as
|
||||
@ -104,16 +128,17 @@ shared.updateSettingValue = function(comp, key, value, callback = null) {
|
||||
}, callback);
|
||||
};
|
||||
|
||||
shared.scheduleSaveSettings = function(comp) {
|
||||
if (shared.scheduleSaveSettingsIID) clearTimeout(shared.scheduleSaveSettingsIID);
|
||||
let scheduleSaveSettingsIID: ReturnType<typeof setTimeout>|null = null;
|
||||
export const scheduleSaveSettings = (comp: ConfigScreenComponent) => {
|
||||
if (scheduleSaveSettingsIID) clearTimeout(scheduleSaveSettingsIID);
|
||||
|
||||
shared.scheduleSaveSettingsIID = setTimeout(() => {
|
||||
shared.scheduleSaveSettingsIID = null;
|
||||
shared.saveSettings(comp);
|
||||
scheduleSaveSettingsIID = setTimeout(() => {
|
||||
scheduleSaveSettingsIID = null;
|
||||
saveSettings(comp);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
shared.saveSettings = function(comp) {
|
||||
export const saveSettings = (comp: ConfigScreenComponent) => {
|
||||
const savedSettingKeys = comp.state.changedSettingKeys.slice();
|
||||
|
||||
for (const key in comp.state.settings) {
|
||||
@ -124,10 +149,10 @@ shared.saveSettings = function(comp) {
|
||||
|
||||
comp.setState({ changedSettingKeys: [] });
|
||||
|
||||
shared.onSettingsSaved({ savedSettingKeys });
|
||||
onSettingsSaved({ savedSettingKeys });
|
||||
};
|
||||
|
||||
shared.settingsToComponents = function(comp, device, settings) {
|
||||
export const settingsToComponents = (comp: ConfigScreenComponent, device: AppType, settings: any) => {
|
||||
const keys = Setting.keys(true, device);
|
||||
const settingComps = [];
|
||||
|
||||
@ -146,10 +171,11 @@ shared.settingsToComponents = function(comp, device, settings) {
|
||||
return settingComps;
|
||||
};
|
||||
|
||||
const deviceSelector = (state) => state.device;
|
||||
const settingsSelector = (state) => state.settings;
|
||||
type SettingsSelectorState = { device: AppType; settings: any };
|
||||
const deviceSelector = (state: SettingsSelectorState) => state.device;
|
||||
const settingsSelector = (state: SettingsSelectorState) => state.settings;
|
||||
|
||||
shared.settingsSections = createSelector(
|
||||
export const settingsSections = createSelector(
|
||||
deviceSelector,
|
||||
settingsSelector,
|
||||
(device, settings) => {
|
||||
@ -168,23 +194,34 @@ shared.settingsSections = createSelector(
|
||||
|
||||
const output = Setting.groupMetadatasBySections(metadatas);
|
||||
|
||||
output.push({
|
||||
name: 'encryption',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
if (device === AppType.Desktop || device === AppType.Cli) {
|
||||
output.push({
|
||||
name: 'encryption',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
output.push({
|
||||
name: 'server',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
output.push({
|
||||
name: 'server',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
output.push({
|
||||
name: 'keymap',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
output.push({
|
||||
name: 'keymap',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
} else {
|
||||
output.push(...([
|
||||
'tools', 'export', 'moreInfo',
|
||||
].map(name => {
|
||||
return {
|
||||
name,
|
||||
metadatas: [],
|
||||
};
|
||||
})));
|
||||
}
|
||||
|
||||
// Ideallly we would also check if the user was able to synchronize
|
||||
// but we don't have a way of doing that besides making a request to Joplin Cloud
|
||||
@ -209,9 +246,11 @@ shared.settingsSections = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
shared.settingsToComponents2 = function(comp, device, settings, selectedSectionName = '') {
|
||||
const sectionComps = [];
|
||||
const sections = shared.settingsSections({ device, settings });
|
||||
export const settingsToComponents2 = (
|
||||
comp: ConfigScreenComponent, device: AppType, settings: any, selectedSectionName = '',
|
||||
) => {
|
||||
const sectionComps: ReactNode[] = [];
|
||||
const sections = settingsSections({ device, settings });
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
@ -222,5 +261,3 @@ shared.settingsToComponents2 = function(comp, device, settings, selectedSectionN
|
||||
|
||||
return sectionComps;
|
||||
};
|
||||
|
||||
module.exports = shared;
|
@ -90,7 +90,7 @@ export interface SettingItem {
|
||||
isGlobal?: boolean;
|
||||
}
|
||||
|
||||
interface SettingItems {
|
||||
export interface SettingItems {
|
||||
[key: string]: SettingItem;
|
||||
}
|
||||
|
||||
@ -221,6 +221,13 @@ const userSettingMigration: UserSettingMigration[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export type SettingMetadataSection = {
|
||||
name: string;
|
||||
isScreen?: boolean;
|
||||
metadatas: SettingItem[];
|
||||
};
|
||||
export type MetadataBySection = SettingMetadataSection[];
|
||||
|
||||
class Setting extends BaseModel {
|
||||
|
||||
public static schemaUrl = 'https://joplinapp.org/schema/settings.json';
|
||||
@ -2545,6 +2552,9 @@ class Setting extends BaseModel {
|
||||
'revisionService',
|
||||
'server',
|
||||
'keymap',
|
||||
'tools',
|
||||
'export',
|
||||
'moreInfo',
|
||||
];
|
||||
}
|
||||
|
||||
@ -2557,7 +2567,7 @@ class Setting extends BaseModel {
|
||||
return ['encryption', 'application', 'appearance', 'joplinCloud'].includes(sectionName);
|
||||
}
|
||||
|
||||
public static groupMetadatasBySections(metadatas: SettingItem[]) {
|
||||
public static groupMetadatasBySections(metadatas: SettingItem[]): MetadataBySection {
|
||||
const sections = [];
|
||||
const generalSection: any = { name: 'general', metadatas: [] };
|
||||
const nameToSections: any = {};
|
||||
@ -2605,6 +2615,9 @@ class Setting extends BaseModel {
|
||||
if (name === 'server') return _('Web Clipper');
|
||||
if (name === 'keymap') return _('Keyboard Shortcuts');
|
||||
if (name === 'joplinCloud') return _('Joplin Cloud');
|
||||
if (name === 'tools') return _('Tools');
|
||||
if (name === 'export') return _('Export');
|
||||
if (name === 'moreInfo') return _('More information');
|
||||
|
||||
if (this.customSections_[name] && this.customSections_[name].label) return this.customSections_[name].label;
|
||||
|
||||
@ -2620,20 +2633,68 @@ class Setting extends BaseModel {
|
||||
return '';
|
||||
}
|
||||
|
||||
public static sectionNameToIcon(name: string) {
|
||||
if (name === 'general') return 'icon-general';
|
||||
if (name === 'sync') return 'icon-sync';
|
||||
if (name === 'appearance') return 'icon-appearance';
|
||||
if (name === 'note') return 'icon-note';
|
||||
if (name === 'folder') return 'icon-notebooks';
|
||||
if (name === 'plugins') return 'icon-plugins';
|
||||
if (name === 'markdownPlugins') return 'fab fa-markdown';
|
||||
if (name === 'application') return 'icon-application';
|
||||
if (name === 'revisionService') return 'icon-note-history';
|
||||
if (name === 'encryption') return 'icon-encryption';
|
||||
if (name === 'server') return 'far fa-hand-scissors';
|
||||
if (name === 'keymap') return 'fa fa-keyboard';
|
||||
if (name === 'joplinCloud') return 'fa fa-cloud';
|
||||
public static sectionMetadataToSummary(metadata: SettingMetadataSection): string {
|
||||
// TODO: This is currently specific to the mobile app
|
||||
const sectionNameToSummary: Record<string, string> = {
|
||||
'general': _('Language, date format'),
|
||||
'appearance': _('App theme, editor font'),
|
||||
'sync': _('Sync, encryption, proxy'),
|
||||
'joplinCloud': _('Email To Note, login information'),
|
||||
'markdownPlugins': _('Media player, math, diagrams, table of contents'),
|
||||
'note': _('Geolocation, spellcheck, editor toolbar, image resize'),
|
||||
'revisionService': _('Toggle note history, keep notes for'),
|
||||
'tools': _('Application log, profiles, sync status'),
|
||||
'export': _('Export your data'),
|
||||
'moreInfo': _('Privacy policy, donate, website'),
|
||||
};
|
||||
|
||||
return sectionNameToSummary[metadata.name] ?? '';
|
||||
}
|
||||
|
||||
public static sectionNameToIcon(name: string, appType: AppType) {
|
||||
const nameToIconMap: Record<string, string> = {
|
||||
'general': 'icon-general',
|
||||
'sync': 'icon-sync',
|
||||
'appearance': 'icon-appearance',
|
||||
'note': 'icon-note',
|
||||
'folder': 'icon-notebooks',
|
||||
'plugins': 'icon-plugins',
|
||||
'markdownPlugins': 'fab fa-markdown',
|
||||
'application': 'icon-application',
|
||||
'revisionService': 'icon-note-history',
|
||||
'encryption': 'icon-encryption',
|
||||
'server': 'far fa-hand-scissors',
|
||||
'keymap': 'fa fa-keyboard',
|
||||
'joplinCloud': 'fa fa-cloud',
|
||||
'tools': 'fa fa-toolbox',
|
||||
'export': 'fa fa-file-export',
|
||||
'moreInfo': 'fa fa-info-circle',
|
||||
};
|
||||
|
||||
// Icomoon icons are currently not present in the mobile app -- we override these
|
||||
// below.
|
||||
//
|
||||
// These icons come from react-native-vector-icons.
|
||||
// See https://oblador.github.io/react-native-vector-icons/
|
||||
const mobileNameToIconMap: Record<string, string> = {
|
||||
'general': 'fa fa-sliders-h',
|
||||
'sync': 'fa fa-sync',
|
||||
'appearance': 'fa fa-ruler',
|
||||
'note': 'fa fa-sticky-note',
|
||||
'revisionService': 'far fa-history',
|
||||
'plugins': 'fa fa-puzzle-piece',
|
||||
'application': 'fa fa-cog',
|
||||
'encryption': 'fa fa-key',
|
||||
};
|
||||
|
||||
// Overridden?
|
||||
if (appType === AppType.Mobile && name in mobileNameToIconMap) {
|
||||
return mobileNameToIconMap[name];
|
||||
}
|
||||
|
||||
if (name in nameToIconMap) {
|
||||
return nameToIconMap[name];
|
||||
}
|
||||
|
||||
if (this.customSections_[name] && this.customSections_[name].iconName) return this.customSections_[name].iconName;
|
||||
|
||||
|
@ -77,7 +77,7 @@ export interface State {
|
||||
syncStarted: boolean;
|
||||
syncReport: any;
|
||||
searchQuery: string;
|
||||
settings: any;
|
||||
settings: Record<string, any>;
|
||||
sharedData: any;
|
||||
appState: string;
|
||||
biometricsDone: boolean;
|
||||
|
@ -260,6 +260,8 @@ class Registry {
|
||||
|
||||
}
|
||||
|
||||
export type { Registry };
|
||||
|
||||
const reg = new Registry();
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
|
Loading…
Reference in New Issue
Block a user