You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Mobile: Settings screen: Create separate pages for each screen (#8567)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user