2022-07-10 16:26:24 +02:00
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
2020-06-02 22:13:15 +02:00
import Slider from '@react-native-community/slider' ;
2019-07-30 09:35:42 +02:00
const React = require ( 'react' ) ;
2023-01-11 20:45:00 +02:00
import { Platform , Linking , View , Switch , StyleSheet , ScrollView , Text , Button , TouchableOpacity , TextInput , Alert , PermissionsAndroid , TouchableNativeFeedback } from 'react-native' ;
2021-06-19 18:32:36 +02:00
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' ;
2023-01-11 20:45:00 +02:00
const { BackButtonService } = require ( '../../services/back-button.js' ) ;
2021-06-21 09:55:02 +02:00
const VersionInfo = require ( 'react-native-version-info' ) . default ;
2018-03-09 22:59:12 +02:00
const { connect } = require ( 'react-redux' ) ;
2022-08-27 14:36:59 +02:00
import ScreenHeader from '../ScreenHeader' ;
2020-11-07 17:59:37 +02:00
const { _ } = require ( '@joplin/lib/locale' ) ;
2020-11-05 18:58:23 +02:00
const { BaseScreenComponent } = require ( '../base-screen.js' ) ;
const { Dropdown } = require ( '../Dropdown.js' ) ;
const { themeStyle } = require ( '../global-style.js' ) ;
2020-11-07 17:59:37 +02:00
const shared = require ( '@joplin/lib/components/shared/config-shared.js' ) ;
2021-08-16 16:20:14 +02:00
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry' ;
2022-07-10 16:26:24 +02:00
import { openDocumentTree } from '@joplin/react-native-saf-x' ;
2023-04-09 11:28:18 +02:00
import biometricAuthenticate from '../biometrics/biometricAuthenticate' ;
2017-07-23 20:26:50 +02:00
2017-08-01 19:59:01 +02:00
class ConfigScreenComponent extends BaseScreenComponent {
2023-03-06 16:22:01 +02:00
public static navigationOptions ( ) : any {
2017-08-01 19:59:01 +02:00
return { header : null } ;
2017-07-30 23:04:26 +02:00
}
2017-07-23 20:26:50 +02:00
2021-08-16 17:18:32 +02:00
private componentsY_ : Record < string , number > = { } ;
public constructor ( ) {
2017-08-01 19:59:01 +02:00
super ( ) ;
this . styles_ = { } ;
2018-01-25 21:01:14 +02:00
2019-06-26 01:13:13 +02:00
this . state = {
creatingReport : false ,
2019-12-28 19:47:37 +02:00
profileExportStatus : 'idle' ,
profileExportPath : '' ,
2022-07-10 16:26:24 +02:00
fileSystemSyncPath : Setting.value ( 'sync.2.path' ) ,
2019-06-26 01:13:13 +02:00
} ;
2021-08-16 17:18:32 +02:00
this . scrollViewRef_ = React . createRef ( ) ;
2022-04-13 13:38:09 +02:00
shared . init ( this , reg ) ;
2018-02-06 20:59:36 +02:00
2022-07-10 16:26:24 +02:00
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 ) ;
}
} ;
2018-02-06 20:59:36 +02:00
this . checkSyncConfig_ = async ( ) = > {
2021-04-25 10:50:52 +02:00
// 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 ) ;
}
2019-07-30 09:35:42 +02:00
} ;
2018-02-06 20:59:36 +02:00
2019-06-26 01:13:13 +02:00
this . e2eeConfig_ = ( ) = > {
2021-06-19 18:32:36 +02:00
void NavService . go ( 'EncryptionConfig' ) ;
2019-07-30 09:35:42 +02:00
} ;
2019-06-26 01:13:13 +02:00
2019-07-13 15:51:54 +02:00
this . saveButton_press = async ( ) = > {
2022-07-10 16:26:24 +02:00
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.' ) ) ;
}
}
}
2019-07-13 15:51:54 +02:00
// Save settings anyway, even if permission has not been granted
}
2021-04-25 10:50:52 +02:00
// 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' ) ) ;
}
2018-01-25 21:01:14 +02:00
} ;
2019-06-26 01:13:13 +02:00
2023-05-29 14:14:09 +02:00
this . saveButton_press = this . saveButton_press . bind ( this ) ;
2019-06-26 01:13:13 +02:00
this . syncStatusButtonPress_ = ( ) = > {
2021-06-19 18:32:36 +02:00
void NavService . go ( 'Status' ) ;
2019-07-30 09:35:42 +02:00
} ;
2019-06-26 01:13:13 +02:00
2023-01-10 14:08:13 +02:00
this . manageProfilesButtonPress_ = ( ) = > {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'ProfileSwitcher' ,
} ) ;
} ;
2019-06-26 01:13:13 +02:00
this . exportDebugButtonPress_ = async ( ) = > {
this . setState ( { creatingReport : true } ) ;
const service = new ReportService ( ) ;
const logItems = await reg . logger ( ) . lastEntries ( null ) ;
2019-07-30 09:35:42 +02:00
const logItemRows = [ [ 'Date' , 'Level' , 'Message' ] ] ;
2019-06-26 01:13:13 +02:00
for ( let i = 0 ; i < logItems . length ; i ++ ) {
const item = logItems [ i ] ;
2019-07-30 09:35:42 +02:00
logItemRows . push ( [ time . formatMsToLocal ( item . timestamp , 'MM-DDTHH:mm:ss' ) , item . level , item . message ] ) ;
2019-06-26 01:13:13 +02:00
}
const logItemCsv = service . csvCreate ( logItemRows ) ;
const itemListCsv = await service . basicItemList ( { format : 'csv' } ) ;
2022-10-13 23:02:06 +02:00
const externalDir = await shim . fsDriver ( ) . getExternalDirectoryPath ( ) ;
if ( ! externalDir ) {
this . setState ( { creatingReport : false } ) ;
return ;
}
const filePath = ` ${ externalDir } /syncReport- ${ new Date ( ) . getTime ( ) } .txt ` ;
2019-06-26 01:13:13 +02:00
2022-10-13 23:02:06 +02:00
const finalText = [ logItemCsv , itemListCsv ] . join ( '\n================================================================================\n' ) ;
await shim . fsDriver ( ) . writeFile ( filePath , finalText , 'utf8' ) ;
2019-09-19 23:51:18 +02:00
alert ( ` Debug report exported to ${ filePath } ` ) ;
2019-06-26 01:13:13 +02:00
this . setState ( { creatingReport : false } ) ;
2019-07-30 09:35:42 +02:00
} ;
2019-06-26 01:13:13 +02:00
2019-06-28 01:48:52 +02:00
this . fixSearchEngineIndexButtonPress_ = async ( ) = > {
this . setState ( { fixingSearchIndex : true } ) ;
await SearchEngine . instance ( ) . rebuildIndex ( ) ;
this . setState ( { fixingSearchIndex : false } ) ;
2019-07-30 09:35:42 +02:00
} ;
2019-06-28 01:48:52 +02:00
2019-12-28 19:47:37 +02:00
this . exportProfileButtonPress_ = async ( ) = > {
2022-10-13 23:02:06 +02:00
const externalDir = await shim . fsDriver ( ) . getExternalDirectoryPath ( ) ;
if ( ! externalDir ) {
return ;
}
const p = this . state . profileExportPath ? this . state . profileExportPath : ` ${ externalDir } /JoplinProfileExport ` ;
2019-12-28 19:47:37 +02:00
this . setState ( {
profileExportStatus : 'prompt' ,
profileExportPath : p ,
} ) ;
} ;
this . exportProfileButtonPress2_ = async ( ) = > {
this . setState ( { profileExportStatus : 'exporting' } ) ;
const dbPath = '/data/data/net.cozic.joplin/databases' ;
2020-05-08 23:46:12 +02:00
const exportPath = this . state . profileExportPath ;
const resourcePath = ` ${ exportPath } /resources ` ;
2019-12-28 19:47:37 +02:00
try {
2020-10-16 17:26:19 +02:00
const response = await checkPermissions ( PermissionsAndroid . PERMISSIONS . WRITE_EXTERNAL_STORAGE ) ;
if ( response !== PermissionsAndroid . RESULTS . GRANTED ) {
2020-06-08 10:01:11 +02:00
throw new Error ( 'Permission denied' ) ;
}
2021-06-19 18:32:36 +02:00
const copyFiles = async ( source : string , dest : string ) = > {
2020-06-08 10:01:11 +02:00
await shim . fsDriver ( ) . mkdir ( dest ) ;
const files = await shim . fsDriver ( ) . readDirStats ( source ) ;
2019-12-28 19:47:37 +02:00
2020-06-08 10:01:11 +02:00
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_ ) ;
2020-05-08 23:46:12 +02:00
}
2020-06-08 10:01:11 +02:00
}
} ;
await copyFiles ( dbPath , exportPath ) ;
await copyFiles ( Setting . value ( 'resourceDir' ) , resourcePath ) ;
2019-12-28 19:47:37 +02:00
alert ( 'Profile has been exported!' ) ;
} catch ( error ) {
alert ( ` Could not export files: ${ error . message } ` ) ;
2020-05-09 16:39:02 +02:00
} finally {
2019-12-28 19:47:37 +02:00
this . setState ( { profileExportStatus : 'idle' } ) ;
}
} ;
2019-06-26 01:13:13 +02:00
this . logButtonPress_ = ( ) = > {
2021-06-19 18:32:36 +02:00
void NavService . go ( 'Log' ) ;
2019-07-30 09:35:42 +02:00
} ;
2023-05-29 14:14:09 +02:00
this . handleSetting = this . handleSetting . bind ( this ) ;
2018-01-25 21:01:14 +02:00
}
2023-03-06 16:22:01 +02:00
public async checkFilesystemPermission() {
2019-07-13 15:51:54 +02:00
if ( Platform . OS !== 'android' ) {
// Not implemented yet
return true ;
}
2020-06-08 10:01:11 +02:00
return await checkPermissions ( PermissionsAndroid . PERMISSIONS . WRITE_EXTERNAL_STORAGE , {
2019-07-30 09:35:42 +02:00
title : _ ( 'Information' ) ,
message : _ ( 'In order to use file system synchronisation your permission to write to external storage is required.' ) ,
buttonPositive : _ ( 'OK' ) ,
} ) ;
2019-07-13 15:51:54 +02:00
}
2023-03-06 16:22:01 +02:00
public UNSAFE_componentWillMount() {
2018-01-25 21:01:14 +02:00
this . setState ( { settings : this.props.settings } ) ;
2017-08-01 19:59:01 +02:00
}
2017-07-30 23:04:26 +02:00
2023-03-06 16:22:01 +02:00
public styles() {
2020-09-15 15:01:07 +02:00
const themeId = this . props . themeId ;
2017-08-01 19:59:01 +02:00
const theme = themeStyle ( themeId ) ;
if ( this . styles_ [ themeId ] ) return this . styles_ [ themeId ] ;
this . styles_ = { } ;
2021-06-19 18:32:36 +02:00
const styles : any = {
2017-11-19 01:59:07 +02:00
body : {
flex : 1 ,
2018-03-09 22:59:12 +02:00
justifyContent : 'flex-start' ,
flexDirection : 'column' ,
2017-11-19 01:59:07 +02:00
} ,
2017-08-01 19:59:01 +02:00
settingContainer : {
2017-11-19 01:59:07 +02:00
flex : 1 ,
2018-03-09 22:59:12 +02:00
flexDirection : 'row' ,
alignItems : 'center' ,
2017-08-01 19:59:01 +02:00
borderBottomWidth : 1 ,
borderBottomColor : theme.dividerColor ,
paddingTop : theme.marginTop ,
paddingBottom : theme.marginBottom ,
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
} ,
settingText : {
color : theme.color ,
fontSize : theme.fontSize ,
2017-11-19 01:59:07 +02:00
flex : 1 ,
2019-05-26 20:39:07 +02:00
paddingRight : 5 ,
2017-08-01 19:59:01 +02:00
} ,
2018-02-14 21:08:07 +02:00
descriptionText : {
2020-03-25 12:50:45 +02:00
color : theme.colorFaded ,
fontSize : theme.fontSizeSmaller ,
2018-02-14 21:08:07 +02:00
flex : 1 ,
} ,
2019-06-08 01:23:17 +02:00
sliderUnits : {
color : theme.color ,
fontSize : theme.fontSize ,
2019-06-27 01:05:17 +02:00
marginRight : 10 ,
2019-06-08 01:23:17 +02:00
} ,
2019-05-26 20:39:07 +02:00
settingDescriptionText : {
2020-03-25 12:50:45 +02:00
color : theme.colorFaded ,
fontSize : theme.fontSizeSmaller ,
2019-05-26 20:39:07 +02:00
flex : 1 ,
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
paddingBottom : theme.marginBottom ,
} ,
2018-05-21 18:24:09 +02:00
permissionText : {
color : theme.color ,
fontSize : theme.fontSize ,
flex : 1 ,
marginTop : 10 ,
} ,
2017-08-01 19:59:01 +02:00
settingControl : {
color : theme.color ,
2017-11-19 01:59:07 +02:00
flex : 1 ,
2017-08-01 19:59:01 +02:00
} ,
2020-04-05 21:47:30 +02:00
textInput : {
color : theme.color ,
} ,
2019-07-30 09:35:42 +02:00
} ;
2017-07-26 19:49:01 +02:00
2023-06-01 13:02:36 +02:00
styles . settingContainerNoBottomBorder = { . . . styles . settingContainer , borderBottomWidth : 0 ,
paddingBottom : theme.marginBottom / 2 } ;
2019-05-26 20:39:07 +02:00
styles . settingControl . borderBottomWidth = 1 ;
2020-06-10 23:08:59 +02:00
styles . settingControl . borderBottomColor = theme . dividerColor ;
2018-02-05 20:32:59 +02:00
2023-06-01 13:02:36 +02:00
styles . switchSettingText = { . . . styles . settingText } ;
2018-03-09 22:59:12 +02:00
styles . switchSettingText . width = '80%' ;
2017-07-26 19:49:01 +02:00
2023-06-01 13:02:36 +02:00
styles . switchSettingContainer = { . . . styles . settingContainer } ;
2018-03-09 22:59:12 +02:00
styles . switchSettingContainer . flexDirection = 'row' ;
styles . switchSettingContainer . justifyContent = 'space-between' ;
2017-07-23 20:26:50 +02:00
2023-06-01 13:02:36 +02:00
styles . linkText = { . . . styles . settingText } ;
2017-11-20 20:25:23 +02:00
styles . linkText . borderBottomWidth = 1 ;
styles . linkText . borderBottomColor = theme . color ;
styles . linkText . flex = 0 ;
2018-03-09 22:59:12 +02:00
styles . linkText . fontWeight = 'normal' ;
2017-11-20 20:25:23 +02:00
2023-06-01 13:02:36 +02:00
styles . headerWrapperStyle = { . . . styles . settingContainer , . . . theme . headerWrapperStyle } ;
2019-06-08 01:23:17 +02:00
2023-06-01 13:02:36 +02:00
styles . switchSettingControl = { . . . styles . settingControl } ;
2017-08-01 19:59:01 +02:00
delete styles . switchSettingControl . color ;
2019-10-09 21:35:13 +02:00
// styles.switchSettingControl.width = '20%';
2017-11-19 02:23:18 +02:00
styles . switchSettingControl . flex = 0 ;
2017-08-01 19:59:01 +02:00
this . styles_ [ themeId ] = StyleSheet . create ( styles ) ;
return this . styles_ [ themeId ] ;
2017-07-23 20:26:50 +02:00
}
2021-08-16 17:18:32 +02:00
private onHeaderLayout ( key : string , event : any ) {
const layout = event . nativeEvent . layout ;
this . componentsY_ [ ` header_ ${ key } ` ] = layout . y ;
}
private onSectionLayout ( key : string , event : any ) {
const layout = event . nativeEvent . layout ;
this . componentsY_ [ ` section_ ${ key } ` ] = layout . y ;
}
private componentY ( key : string ) : number {
if ( ( ` section_ ${ key } ` ) in this . componentsY_ ) return this . componentsY_ [ ` section_ ${ key } ` ] ;
if ( ( ` header_ ${ key } ` ) in this . componentsY_ ) return this . componentsY_ [ ` header_ ${ key } ` ] ;
console . error ( ` ConfigScreen: Could not find key to scroll to: ${ key } ` ) ;
return 0 ;
}
2023-01-11 20:45:00 +02:00
private handleBackButtonPress = ( ) : boolean = > {
const goBack = async ( ) = > {
BackButtonService . removeHandler ( this . handleBackButtonPress ) ;
await BackButtonService . back ( ) ;
} ;
if ( this . state . changedSettingKeys . length > 0 ) {
const dialogTitle : string | null = null ;
Alert . alert (
dialogTitle ,
_ ( 'There are unsaved changes.' ) ,
[ {
text : _ ( 'Save changes' ) ,
onPress : async ( ) = > {
await this . saveButton_press ( ) ;
await goBack ( ) ;
} ,
} ,
{
text : _ ( 'Discard changes' ) ,
onPress : goBack ,
} ]
) ;
return true ;
}
return false ;
} ;
2021-08-16 17:18:32 +02:00
public componentDidMount() {
if ( this . props . navigation . state . sectionName ) {
setTimeout ( ( ) = > {
this . scrollViewRef_ . current . scrollTo ( {
x : 0 ,
y : this.componentY ( this . props . navigation . state . sectionName ) ,
animated : true ,
} ) ;
} , 200 ) ;
}
2023-01-11 20:45:00 +02:00
BackButtonService . addHandler ( this . handleBackButtonPress ) ;
}
public componentWillUnmount() {
BackButtonService . removeHandler ( this . handleBackButtonPress ) ;
2021-08-16 17:18:32 +02:00
}
2023-03-06 16:22:01 +02:00
public renderHeader ( key : string , title : string ) {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2019-06-08 01:23:17 +02:00
return (
2021-08-16 17:18:32 +02:00
< View key = { key } style = { this . styles ( ) . headerWrapperStyle } onLayout = { ( event : any ) = > this . onHeaderLayout ( key , event ) } >
2019-06-08 01:23:17 +02:00
< Text style = { theme . headerStyle } > { title } < / Text >
< / View >
) ;
}
2023-01-11 20:45:00 +02:00
renderButton ( key : string , title : string , clickHandler : ( ) = > void , options : any = null ) {
2019-06-26 01:13:13 +02:00
if ( ! options ) options = { } ;
2019-06-28 01:48:52 +02:00
let descriptionComp = null ;
if ( options . description ) {
descriptionComp = (
2019-07-30 09:35:42 +02:00
< View style = { { flex : 1 , marginTop : 10 } } >
2019-06-28 01:48:52 +02:00
< Text style = { this . styles ( ) . descriptionText } > { options . description } < / Text >
< / View >
) ;
}
2019-06-26 01:13:13 +02:00
return (
< View key = { key } style = { this . styles ( ) . settingContainer } >
2019-07-30 09:35:42 +02:00
< View style = { { flex : 1 , flexDirection : 'column' } } >
< View style = { { flex : 1 } } >
< Button title = { title } onPress = { clickHandler } disabled = { ! ! options . disabled } / >
2019-06-26 01:13:13 +02:00
< / View >
2019-07-30 09:35:42 +02:00
{ options . statusComp }
{ descriptionComp }
2019-06-26 01:13:13 +02:00
< / View >
< / View >
) ;
}
2023-03-06 16:22:01 +02:00
public sectionToComponent ( key : string , section : any , settings : any ) {
2019-06-08 01:23:17 +02:00
const settingComps = [ ] ;
for ( let i = 0 ; i < section . metadatas . length ; i ++ ) {
const md = section . metadatas [ i ] ;
if ( section . name === 'sync' && md . key === 'sync.resourceDownloadMode' ) {
const syncTargetMd = SyncTargetRegistry . idToMetadata ( settings [ 'sync.target' ] ) ;
if ( syncTargetMd . supportsConfigCheck ) {
const messages = shared . checkSyncConfigMessages ( this ) ;
const statusComp = ! messages . length ? null : (
2019-07-30 09:35:42 +02:00
< View style = { { flex : 1 , marginTop : 10 } } >
2019-06-08 01:23:17 +02:00
< Text style = { this . styles ( ) . descriptionText } > { messages [ 0 ] } < / Text >
2019-07-30 09:35:42 +02:00
{ messages . length >= 1 ? (
< View style = { { marginTop : 10 } } >
< Text style = { this . styles ( ) . descriptionText } > { messages [ 1 ] } < / Text >
< / View >
) : null }
< / View >
) ;
2019-06-08 01:23:17 +02:00
2019-06-26 01:13:13 +02:00
settingComps . push ( this . renderButton ( 'check_sync_config_button' , _ ( 'Check synchronisation configuration' ) , this . checkSyncConfig_ , { statusComp : statusComp } ) ) ;
2019-06-08 01:23:17 +02:00
}
}
const settingComp = this . settingToComponent ( md . key , settings [ md . key ] ) ;
settingComps . push ( settingComp ) ;
}
2019-06-26 01:13:13 +02:00
if ( section . name === 'sync' ) {
2019-06-26 19:51:12 +02:00
settingComps . push ( this . renderButton ( 'e2ee_config_button' , _ ( 'Encryption Config' ) , this . e2eeConfig_ ) ) ;
2019-06-26 01:13:13 +02:00
}
2019-09-16 23:59:45 +02:00
if ( ! settingComps . length ) return null ;
2019-06-08 01:23:17 +02:00
return (
2021-08-16 17:18:32 +02:00
< View key = { key } onLayout = { ( event : any ) = > this . onSectionLayout ( key , event ) } >
2019-06-08 01:23:17 +02:00
{ this . renderHeader ( section . name , Setting . sectionNameToLabel ( section . name ) ) }
2019-07-30 09:35:42 +02:00
< View > { settingComps } < / View >
2019-06-08 01:23:17 +02:00
< / View >
) ;
}
2021-06-19 18:32:36 +02:00
private renderToggle ( key : string , label : string , value : any , updateSettingValue : Function , descriptionComp : any = null ) {
const theme = themeStyle ( this . props . themeId ) ;
return (
< View key = { key } >
< View style = { this . containerStyle ( false ) } >
< Text key = "label" style = { this . styles ( ) . switchSettingText } >
{ label }
< / Text >
2023-04-09 11:28:18 +02:00
< Switch key = "control" style = { this . styles ( ) . switchSettingControl } trackColor = { { false : theme . dividerColor } } value = { value } onValueChange = { ( value : any ) = > void updateSettingValue ( key , value ) } / >
2021-06-19 18:32:36 +02:00
< / View >
{ descriptionComp }
< / View >
) ;
}
private containerStyle ( hasDescription : boolean ) : any {
return ! hasDescription ? this . styles ( ) . settingContainer : this.styles ( ) . settingContainerNoBottomBorder ;
}
2023-04-09 11:28:18 +02:00
private async handleSetting ( key : string , value : any ) : Promise < boolean > {
// When the user tries to enable biometrics unlock, we ask for the
// fingerprint or Face ID, and if it's correct we save immediately. If
// it's not, we don't turn on the setting.
if ( key === 'security.biometricsEnabled' && ! ! value ) {
try {
await biometricAuthenticate ( ) ;
2023-05-29 14:14:09 +02:00
shared . updateSettingValue ( this , key , value , async ( ) = > await this . saveButton_press ( ) ) ;
2023-04-09 11:28:18 +02:00
} catch ( error ) {
shared . updateSettingValue ( this , key , false ) ;
Alert . alert ( error . message ) ;
}
return true ;
}
if ( key === 'security.biometricsEnabled' && ! value ) {
2023-05-29 14:14:09 +02:00
shared . updateSettingValue ( this , key , value , async ( ) = > await this . saveButton_press ( ) ) ;
2023-04-09 11:28:18 +02:00
return true ;
}
return false ;
}
2023-03-06 16:22:01 +02:00
public settingToComponent ( key : string , value : any ) {
2020-09-15 15:01:07 +02:00
const themeId = this . props . themeId ;
2017-11-19 01:59:07 +02:00
const theme = themeStyle ( themeId ) ;
2021-06-19 18:32:36 +02:00
const output : any = null ;
2017-07-23 20:26:50 +02:00
2023-04-09 11:28:18 +02:00
const updateSettingValue = async ( key : string , value : any ) = > {
const handled = await this . handleSetting ( key , value ) ;
if ( ! handled ) shared . updateSettingValue ( this , key , value ) ;
2019-07-30 09:35:42 +02:00
} ;
2017-07-23 20:26:50 +02:00
2017-07-31 22:51:24 +02:00
const md = Setting . settingMetadata ( key ) ;
2019-05-26 20:39:07 +02:00
const settingDescription = md . description ? md . description ( ) : '' ;
2017-07-31 22:51:24 +02:00
2020-03-25 12:50:45 +02:00
const descriptionComp = ! settingDescription ? null : < Text style = { this . styles ( ) . settingDescriptionText } > { settingDescription } < / Text > ;
2021-06-19 18:32:36 +02:00
const containerStyle = this . containerStyle ( ! ! settingDescription ) ;
2020-03-25 12:50:45 +02:00
2017-07-31 22:51:24 +02:00
if ( md . isEnum ) {
value = value . toString ( ) ;
2017-07-23 20:26:50 +02:00
2022-05-05 14:19:23 +02:00
const items = Setting . enumOptionsToValueLabels ( md . options ( ) , md . optionsOrder ? md . optionsOrder ( ) : [ ] ) ;
2017-07-23 20:26:50 +02:00
return (
2019-07-30 09:35:42 +02:00
< View key = { key } style = { { flexDirection : 'column' , borderBottomWidth : 1 , borderBottomColor : theme.dividerColor } } >
2019-06-08 01:23:17 +02:00
< View style = { containerStyle } >
2019-07-30 09:35:42 +02:00
< Text key = "label" style = { this . styles ( ) . settingText } >
{ md . label ( ) }
< / Text >
2019-05-26 20:39:07 +02:00
< 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 ,
} }
2022-08-27 14:36:59 +02:00
onValueChange = { ( itemValue : string ) = > {
2023-04-09 11:28:18 +02:00
void updateSettingValue ( key , itemValue ) ;
2019-07-30 09:35:42 +02:00
} }
2019-05-26 20:39:07 +02:00
/ >
< / View >
{ descriptionComp }
2017-07-23 20:26:50 +02:00
< / View >
) ;
2022-07-23 09:31:32 +02:00
} else if ( md . type === Setting . TYPE_BOOL ) {
2021-06-19 18:32:36 +02:00
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>
// );
2022-07-23 09:31:32 +02:00
} else if ( md . type === Setting . TYPE_INT ) {
2019-05-06 22:35:29 +02:00
const unitLabel = md . unitLabel ? md . unitLabel ( value ) : value ;
2023-01-03 22:22:44 +02:00
const minimum = 'minimum' in md ? md.minimum : 0 ;
const maximum = 'maximum' in md ? md.maximum : 10 ;
2020-03-13 19:42:50 +02:00
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
// https://github.com/laurent22/joplin/issues/2733
// https://github.com/react-native-community/react-native-slider/issues/161
2017-07-25 23:55:26 +02:00
return (
2017-08-01 19:59:01 +02:00
< View key = { key } style = { this . styles ( ) . settingContainer } >
2019-07-30 09:35:42 +02:00
< Text key = "label" style = { this . styles ( ) . settingText } >
{ md . label ( ) }
< / Text >
< View style = { { display : 'flex' , flexDirection : 'row' , alignItems : 'center' , flex : 1 } } >
2019-06-08 01:23:17 +02:00
< Text style = { this . styles ( ) . sliderUnits } > { unitLabel } < / Text >
2023-04-09 11:28:18 +02:00
< Slider key = "control" style = { { flex : 1 } } step = { md . step } minimumValue = { minimum } maximumValue = { maximum } value = { value } onValueChange = { value = > void updateSettingValue ( key , value ) } / >
2019-05-06 22:35:29 +02:00
< / View >
2017-07-25 23:55:26 +02:00
< / View >
) ;
2022-07-23 09:31:32 +02:00
} else if ( md . type === Setting . TYPE_STRING ) {
2022-10-13 23:02:06 +02:00
if ( md . key === 'sync.2.path' && shim . fsDriver ( ) . isUsingAndroidSAF ( ) ) {
2022-07-10 16:26:24 +02:00
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 >
) ;
}
2018-01-25 21:01:14 +02:00
return (
< View key = { key } style = { this . styles ( ) . settingContainer } >
2019-07-30 09:35:42 +02:00
< Text key = "label" style = { this . styles ( ) . settingText } >
{ md . label ( ) }
< / Text >
2023-04-09 11:28:18 +02:00
< 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 } / >
2018-01-25 21:01:14 +02:00
< / View >
) ;
2017-07-26 19:49:01 +02:00
} else {
2019-10-09 21:35:13 +02:00
// throw new Error('Unsupported setting type: ' + md.type);
2017-07-23 20:26:50 +02:00
}
return output ;
}
2021-06-26 11:19:48 +02:00
private renderFeatureFlags ( settings : any , featureFlagKeys : string [ ] ) : any [ ] {
2021-06-19 18:32:36 +02:00
const updateSettingValue = ( key : string , value : any ) = > {
return shared . updateSettingValue ( this , key , value ) ;
} ;
const output : any [ ] = [ ] ;
2021-06-26 11:19:48 +02:00
for ( const key of featureFlagKeys ) {
2021-06-19 18:32:36 +02:00
output . push ( this . renderToggle ( key , key , settings [ key ] , updateSettingValue ) ) ;
}
return output ;
}
2023-03-06 16:22:01 +02:00
public render() {
2018-01-25 21:01:14 +02:00
const settings = this . state . settings ;
2017-07-23 20:26:50 +02:00
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2020-04-17 00:35:31 +02:00
2019-06-08 01:23:17 +02:00
const settingComps = shared . settingsToComponents2 ( this , 'mobile' , settings ) ;
2018-02-06 20:59:36 +02:00
2019-06-26 19:51:12 +02:00
settingComps . push ( this . renderHeader ( 'tools' , _ ( 'Tools' ) ) ) ;
2019-06-26 01:13:13 +02:00
2023-01-10 14:08:13 +02:00
settingComps . push ( this . renderButton ( 'profiles_buttons' , _ ( 'Manage profiles' ) , this . manageProfilesButtonPress_ ) ) ;
2019-06-26 19:51:12 +02:00
settingComps . push ( this . renderButton ( 'status_button' , _ ( 'Sync Status' ) , this . syncStatusButtonPress_ ) ) ;
settingComps . push ( this . renderButton ( 'log_button' , _ ( 'Log' ) , this . logButtonPress_ ) ) ;
2020-03-13 21:55:26 +02:00
if ( Platform . OS === 'android' ) {
settingComps . push ( this . renderButton ( 'export_report_button' , this . state . creatingReport ? _ ( 'Creating report...' ) : _ ( 'Export Debug Report' ) , this . exportDebugButtonPress_ , { disabled : this.state.creatingReport } ) ) ;
}
2019-07-22 00:27:13 +02:00
settingComps . push ( this . renderButton ( 'fix_search_engine_index' , this . state . fixingSearchIndex ? _ ( 'Fixing search index...' ) : _ ( 'Fix search index' ) , this . fixSearchEngineIndexButtonPress_ , { disabled : this.state.fixingSearchIndex , description : _ ( 'Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.' ) } ) ) ;
2019-06-26 01:13:13 +02:00
2019-12-28 19:47:37 +02:00
if ( shim . mobilePlatform ( ) === 'android' ) {
settingComps . push ( this . renderButton ( 'export_data' , this . state . profileExportStatus === 'exporting' ? _ ( 'Exporting profile...' ) : _ ( 'Export profile' ) , this . exportProfileButtonPress_ , { disabled : this.state.profileExportStatus === 'exporting' , description : _ ( 'For debugging purpose only: export your profile to an external SD card.' ) } ) ) ;
if ( this . state . profileExportStatus === 'prompt' ) {
const profileExportPrompt = (
2020-06-08 10:01:33 +02:00
< View style = { this . styles ( ) . settingContainer } key = "profileExport" >
2022-04-10 12:22:30 +02:00
< 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_ } / >
2019-12-28 19:47:37 +02:00
< / View >
) ;
settingComps . push ( profileExportPrompt ) ;
}
}
2021-06-26 11:19:48 +02:00
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 > ) ;
}
2021-06-19 18:32:36 +02:00
2019-06-08 01:23:17 +02:00
settingComps . push ( this . renderHeader ( 'moreInfo' , _ ( 'More information' ) ) ) ;
2018-03-09 11:09:13 +02:00
2018-05-21 18:24:09 +02:00
if ( Platform . OS === 'android' && Platform . Version >= 23 ) {
// Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually
// set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again
2019-06-08 01:23:17 +02:00
2018-05-21 18:24:09 +02:00
settingComps . push (
< View key = "permission_info" style = { this . styles ( ) . settingContainer } >
< View key = "permission_info_wrapper" >
2019-07-30 09:35:42 +02:00
< 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 >
2018-05-21 18:24:09 +02:00
< / View >
< / View >
) ;
}
2018-03-09 11:09:13 +02:00
settingComps . push (
< View key = "donate_link" style = { this . styles ( ) . settingContainer } >
2019-07-30 09:35:42 +02:00
< TouchableOpacity
onPress = { ( ) = > {
2023-01-11 20:45:00 +02:00
void Linking . openURL ( 'https://joplinapp.org/donate/' ) ;
2019-07-30 09:35:42 +02:00
} }
>
< Text key = "label" style = { this . styles ( ) . linkText } >
{ _ ( 'Make a donation' ) }
< / Text >
2018-03-09 11:09:13 +02:00
< / TouchableOpacity >
< / View >
) ;
2018-11-02 02:43:42 +02:00
2017-11-20 20:25:23 +02:00
settingComps . push (
< View key = "website_link" style = { this . styles ( ) . settingContainer } >
2019-07-30 09:35:42 +02:00
< TouchableOpacity
onPress = { ( ) = > {
2023-01-11 20:45:00 +02:00
void Linking . openURL ( 'https://joplinapp.org/' ) ;
2019-07-30 09:35:42 +02:00
} }
>
< Text key = "label" style = { this . styles ( ) . linkText } >
{ _ ( 'Joplin website' ) }
< / Text >
2017-11-20 20:25:23 +02:00
< / TouchableOpacity >
< / View >
) ;
settingComps . push (
< View key = "privacy_link" style = { this . styles ( ) . settingContainer } >
2019-07-30 09:35:42 +02:00
< TouchableOpacity
onPress = { ( ) = > {
2023-01-11 20:45:00 +02:00
void Linking . openURL ( 'https://joplinapp.org/privacy/' ) ;
2019-07-30 09:35:42 +02:00
} }
>
< Text key = "label" style = { this . styles ( ) . linkText } >
2021-01-21 20:23:32 +02:00
{ _ ( 'Privacy Policy' ) }
2019-07-30 09:35:42 +02:00
< / Text >
2017-11-20 20:25:23 +02:00
< / TouchableOpacity >
< / View >
) ;
2017-07-23 20:26:50 +02:00
2018-11-02 02:43:42 +02:00
settingComps . push (
2018-12-28 22:40:29 +02:00
< View key = "version_info_app" style = { this . styles ( ) . settingContainer } >
2019-09-19 23:51:18 +02:00
< Text style = { this . styles ( ) . settingText } > { ` Joplin ${ VersionInfo . appVersion } ` } < / Text >
2018-12-28 22:40:29 +02:00
< / 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 >
2018-11-02 02:43:42 +02:00
< / View >
) ;
2023-05-08 18:50:19 +02:00
settingComps . push (
< View key = "version_info_hermes" style = { this . styles ( ) . settingContainer } >
< Text style = { this . styles ( ) . settingText } > { _ ( 'Hermes enabled: %d' , ( global as any ) . HermesInternal ? 1 : 0 ) } < / Text >
< / View >
) ;
2017-07-23 20:26:50 +02:00
return (
2020-09-15 15:01:07 +02:00
< View style = { this . rootStyle ( this . props . themeId ) . root } >
2019-07-30 09:35:42 +02:00
< ScreenHeader title = { _ ( 'Configuration' ) } showSaveButton = { true } showSearchButton = { false } showSideMenuButton = { false } saveButtonDisabled = { ! this . state . changedSettingKeys . length } onSaveButtonPress = { this . saveButton_press } / >
2021-08-16 17:18:32 +02:00
< ScrollView ref = { this . scrollViewRef_ } > { settingComps } < / ScrollView >
2017-07-23 20:26:50 +02:00
< / View >
) ;
}
}
2021-06-19 18:32:36 +02:00
const ConfigScreen = connect ( ( state : State ) = > {
2019-07-30 09:35:42 +02:00
return {
settings : state.settings ,
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2019-07-30 09:35:42 +02:00
} ;
} ) ( ConfigScreenComponent ) ;
2017-07-23 20:26:50 +02:00
2021-06-19 18:32:36 +02:00
export default ConfigScreen ;