1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Add support for multiple profiles (#7586)

This commit is contained in:
Laurent Cozic 2023-01-10 10:54:49 +00:00 committed by GitHub
parent b60a94bb6e
commit 44a7bf3b2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 540 additions and 38 deletions

View File

@ -984,6 +984,15 @@ packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
packages/app-mobile/components/NoteEditor/types.d.ts
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteEditor/types.js.map
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.d.ts
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js.map
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.d.ts
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js.map
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.d.ts
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js.map
packages/app-mobile/components/ScreenHeader.d.ts
packages/app-mobile/components/ScreenHeader.js
packages/app-mobile/components/ScreenHeader.js.map
@ -993,6 +1002,9 @@ packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map
packages/app-mobile/components/TextInput.d.ts
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/TextInput.js.map
packages/app-mobile/components/biometrics/BiometricPopup.d.ts
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/BiometricPopup.js.map
@ -1038,6 +1050,9 @@ packages/app-mobile/services/AlarmServiceDriver.ios.js.map
packages/app-mobile/services/e2ee/RSA.react-native.d.ts
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/services/profiles/index.d.ts
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/profiles/index.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
@ -1056,6 +1071,9 @@ packages/app-mobile/utils/TlsUtils.js.map
packages/app-mobile/utils/checkPermissions.d.ts
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/checkPermissions.js.map
packages/app-mobile/utils/createRootStyle.d.ts
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/createRootStyle.js.map
packages/app-mobile/utils/debounce.d.ts
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/debounce.js.map

22
.gitignore vendored
View File

@ -972,6 +972,15 @@ packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
packages/app-mobile/components/NoteEditor/types.d.ts
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteEditor/types.js.map
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.d.ts
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js.map
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.d.ts
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js.map
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.d.ts
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js.map
packages/app-mobile/components/ScreenHeader.d.ts
packages/app-mobile/components/ScreenHeader.js
packages/app-mobile/components/ScreenHeader.js.map
@ -981,6 +990,9 @@ packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map
packages/app-mobile/components/TextInput.d.ts
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/TextInput.js.map
packages/app-mobile/components/biometrics/BiometricPopup.d.ts
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/BiometricPopup.js.map
@ -1026,6 +1038,9 @@ packages/app-mobile/services/AlarmServiceDriver.ios.js.map
packages/app-mobile/services/e2ee/RSA.react-native.d.ts
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/RSA.react-native.js.map
packages/app-mobile/services/profiles/index.d.ts
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/profiles/index.js.map
packages/app-mobile/setupQuickActions.d.ts
packages/app-mobile/setupQuickActions.js
packages/app-mobile/setupQuickActions.js.map
@ -1044,6 +1059,9 @@ packages/app-mobile/utils/TlsUtils.js.map
packages/app-mobile/utils/checkPermissions.d.ts
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/checkPermissions.js.map
packages/app-mobile/utils/createRootStyle.d.ts
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/createRootStyle.js.map
packages/app-mobile/utils/debounce.d.ts
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/debounce.js.map
@ -2371,6 +2389,4 @@ packages/tools/website/utils/types.d.ts
packages/tools/website/utils/types.js
packages/tools/website/utils/types.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-mobile/components/get-responsive-value.test.js
packages/app-mobile/components/get-responsive-value.test.js
packages/app-mobile/components/get-responsive-value.test.js

View File

@ -0,0 +1,102 @@
const React = require('react');
import { useCallback, useEffect, useMemo, useState } from 'react';
const { View, StyleSheet } = require('react-native');
import createRootStyle from '../../utils/createRootStyle';
import ScreenHeader from '../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import { loadProfileConfig, saveProfileConfig } from '../../services/profiles';
import { createNewProfile } from '@joplin/lib/services/profileConfig';
import useProfileConfig from './useProfileConfig';
const { TextInput } = require('react-native-paper');
interface NavigationState {
profileId: string;
}
interface Navigation {
state: NavigationState;
}
interface Props {
themeId: number;
dispatch: Function;
navigation: Navigation;
}
const useStyle = (themeId: number) => {
return useMemo(() => {
return StyleSheet.create({
...createRootStyle(themeId),
});
}, [themeId]);
};
export default (props: Props) => {
const profileId = props.navigation.state?.profileId;
const isNew = !profileId;
const profileConfig = useProfileConfig();
const style = useStyle(props.themeId);
const [name, setName] = useState('');
const profile = !isNew && profileConfig ? profileConfig.profiles.find(p => p.id === profileId) : null;
useEffect(() => {
if (!profile) return;
setName(profile.name);
}, [profile]);
const onSaveButtonPress = useCallback(async () => {
if (isNew) {
const profileConfig = await loadProfileConfig();
const result = createNewProfile(profileConfig, name);
await saveProfileConfig(result.newConfig);
} else {
const newProfiles = profileConfig.profiles.map(p => {
if (p.id === profile.id) {
return {
...profile,
name,
};
}
return p;
});
const newProfileConfig = {
...profileConfig,
profiles: newProfiles,
};
await saveProfileConfig(newProfileConfig);
}
props.dispatch({
type: 'NAV_BACK',
});
}, [name, isNew, profileConfig, profile, props.dispatch]);
const isModified = useMemo(() => {
if (isNew) return true;
if (!profile) return false;
return profile.name !== name;
}, [isNew, profile, name]);
return (
<View style={style.root}>
<ScreenHeader
title={isNew ? _('Create new profile...') : _('Edit profile')}
onSaveButtonPress={onSaveButtonPress}
saveButtonDisabled={!isModified}
showSaveButton={true}
showSideMenuButton={false}
showSearchButton={false}
/>
<View style={{}}>
<TextInput label={_('Profile name')}
value={name}
onChangeText={(text: string) => setName(text)}
/>
</View>
</View>
);
};

View File

@ -0,0 +1,176 @@
const React = require('react');
import { useCallback, useMemo, useState } from 'react';
const { View, FlatList, StyleSheet } = require('react-native');
import createRootStyle from '../../utils/createRootStyle';
import ScreenHeader from '../ScreenHeader';
const { FAB, List } = require('react-native-paper');
import { Profile } from '@joplin/lib/services/profileConfig/types';
import useProfileConfig from './useProfileConfig';
import { Alert } from 'react-native';
import { _ } from '@joplin/lib/locale';
import { deleteProfileById } from '@joplin/lib/services/profileConfig';
import { saveProfileConfig, switchProfile } from '../../services/profiles';
const { themeStyle } = require('../global-style');
interface Props {
themeId: number;
dispatch: Function;
}
const useStyle = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
...createRootStyle(themeId),
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
},
profileListItem: {
paddingLeft: theme.margin,
paddingRight: theme.margin,
},
});
}, [themeId]);
};
export default (props: Props) => {
const style = useStyle(props.themeId);
const [profileConfigTime, setProfileConfigTime] = useState(Date.now());
const profileConfig = useProfileConfig(profileConfigTime);
const profiles = useMemo(() => {
return profileConfig ? profileConfig.profiles : [];
}, [profileConfig]);
const onProfileItemPress = useCallback(async (profile: Profile) => {
const doIt = async () => {
try {
await switchProfile(profile.id);
} catch (error) {
Alert.alert(_('Could not switch profile: %s', error.message));
}
};
Alert.alert(
_('Confirmation'),
_('To switch the profile, the app is going to close and you will need to restart it.'),
[
{
text: _('Continue'),
onPress: () => doIt(),
style: 'default',
},
{
text: _('Cancel'),
onPress: () => {},
style: 'cancel',
},
]
);
}, []);
const onEditProfile = useCallback(async (profileId: string) => {
props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
profileId: profileId,
});
}, [props.dispatch]);
const onDeleteProfile = useCallback(async (profile: Profile) => {
const doIt = async () => {
try {
const newConfig = deleteProfileById(profileConfig, profile.id);
await saveProfileConfig(newConfig);
setProfileConfigTime(Date.now());
} catch (error) {
Alert.alert(error.message);
}
};
Alert.alert(
_('Delete this profile?'),
_('All data, including notes, notebooks and tags will be permanently deleted.'),
[
{
text: _('Delete profile "%s"', profile.name),
onPress: () => doIt(),
style: 'destructive',
},
{
text: _('Cancel'),
onPress: () => {},
style: 'cancel',
},
]
);
}, [profileConfig]);
const renderProfileItem = (event: any) => {
const profile = event.item as Profile;
const titleStyle = { fontWeight: profile.id === profileConfig.currentProfileId ? 'bold' : 'normal' };
return (
<List.Item
title={profile.name}
style={style.profileListItem}
titleStyle={titleStyle}
left={() => <List.Icon icon="file-account-outline" />}
key={profile.id}
profileId={profile.id}
onPress={() => { void onProfileItemPress(profile); }}
onLongPress={() => {
Alert.alert(
_('Configuration'),
'',
[
{
text: _('Edit'),
onPress: () => onEditProfile(profile.id),
style: 'default',
},
{
text: _('Delete'),
onPress: () => onDeleteProfile(profile),
style: 'default',
},
{
text: _('Close'),
onPress: () => {},
style: 'cancel',
},
]
);
}}
/>
);
};
return (
<View style={style.root}>
<ScreenHeader title={_('Profiles')} showSaveButton={false} showSideMenuButton={false} showSearchButton={false} />
<View>
<FlatList
data={profiles}
renderItem={renderProfileItem}
keyExtractor={(profile: Profile) => profile.id}
/>
</View>
<FAB
icon="plus"
style={style.fab}
onPress={() => {
props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
});
}}
/>
</View>
);
};

View File

@ -0,0 +1,20 @@
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import { useState } from 'react';
import { loadProfileConfig } from '../../services/profiles';
export default (timestamp: number = 0) => {
const [profileConfig, setProfileConfig] = useState<ProfileConfig>(null);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const load = async () => {
const r = await loadProfileConfig();
if (event.cancelled) return;
setProfileConfig(r);
};
void load();
}, [timestamp]);
return profileConfig;
};

View File

@ -0,0 +1,37 @@
const React = require('react');
import { useMemo } from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { TextInput, TextInputProps, StyleSheet } from 'react-native';
interface Props extends TextInputProps {
themeId: number;
}
export default (props: Props) => {
const theme = themeStyle(props.themeId);
const finalProps = { ...props };
if (!('placeholderTextColor' in finalProps)) finalProps.placeholderTextColor = theme.colorFaded;
if (!('underlineColorAndroid' in finalProps)) finalProps.underlineColorAndroid = theme.dividerColor;
if (!('selectionColor' in finalProps)) finalProps.selectionColor = theme.textSelectionColor;
if (!('keyboardAppearance' in finalProps)) finalProps.keyboardAppearance = theme.keyboardAppearance;
if (!('style' in finalProps)) finalProps.style = {};
const defaultStyle = useMemo(() => {
const theme = themeStyle(finalProps.themeId);
return StyleSheet.create({
textInput: {
color: theme.color,
paddingLeft: 14,
paddingRight: 14,
paddingTop: 12,
paddingBottom: 12,
},
});
}, [finalProps.themeId]);
finalProps.style = [defaultStyle.textInput, finalProps.style];
return <TextInput {...finalProps} />;
};

View File

@ -71,7 +71,7 @@ class AppNavComponent extends Component {
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : null} style={style}>
<NotesScreen visible={notesScreenVisible} navigation={{ state: route }} />
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} />}
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} />}
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} themeId={this.props.themeId} dispatch={this.props.dispatch} />}
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
</KeyboardAvoidingView>
);

View File

@ -69,6 +69,9 @@ function addExtraStyles(style) {
style.keyboardAppearance = style.appearance;
style.color5 = style.backgroundColor4;
style.backgroundColor5 = style.color4;
return style;
}

View File

@ -100,6 +100,13 @@ class ConfigScreenComponent extends BaseScreenComponent {
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();
@ -564,6 +571,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
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_));
if (Platform.OS === 'android') {

View File

@ -1,14 +1,14 @@
const React = require('react');
const { View, TextInput, StyleSheet } = require('react-native');
const { View } = require('react-native');
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 { dialogs } = require('../../utils/dialogs.js');
const { themeStyle } = require('../global-style.js');
const { _ } = require('@joplin/lib/locale');
const TextInput = require('../TextInput').default;
class FolderScreenComponent extends BaseScreenComponent {
static navigationOptions() {
@ -21,25 +21,6 @@ class FolderScreenComponent extends BaseScreenComponent {
folder: Folder.new(),
lastSavedFolder: null,
};
this.styles_ = {};
}
styles() {
const theme = themeStyle(this.props.themeId);
if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
this.styles_ = {};
const styles = {
textInput: {
color: theme.color,
paddingLeft: theme.marginLeft,
marginTop: theme.marginTop,
},
};
this.styles_[this.props.themeId] = StyleSheet.create(styles);
return this.styles_[this.props.themeId];
}
UNSAFE_componentWillMount() {
@ -103,12 +84,17 @@ class FolderScreenComponent extends BaseScreenComponent {
render() {
const saveButtonDisabled = !this.isModified();
const theme = themeStyle(this.props.themeId);
return (
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Edit notebook')} showSaveButton={true} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={() => this.saveFolderButton_press()} showSideMenuButton={false} showSearchButton={false} />
<TextInput placeholder={_('Enter notebook title')} placeholderTextColor={theme.colorFaded} underlineColorAndroid={theme.dividerColor} selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} style={this.styles().textInput} autoFocus={true} value={this.state.folder.title} onChangeText={text => this.title_changeText(text)} />
<TextInput
themeId={this.props.themeId}
placeholder={_('Enter notebook title')}
autoFocus={true}
value={this.state.folder.title}
onChangeText={text => this.title_changeText(text)}
/>
<dialogs.DialogBox
ref={dialogbox => {
this.dialogbox = dialogbox;

View File

@ -13,6 +13,7 @@ import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
import { AppState } from '../utils/types';
import Setting from '@joplin/lib/models/Setting';
import { reg } from '@joplin/lib/registry';
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
// We need this to suppress the useless warning
// https://github.com/oblador/react-native-vector-icons/issues/1465
@ -31,6 +32,7 @@ interface Props {
notesParentType: string;
folders: FolderEntity[];
opacity: number;
profileConfig: ProfileConfig;
}
const syncIconRotationValue = new Animated.Value(0);
@ -200,6 +202,15 @@ const SideMenuContentComponent = (props: Props) => {
});
};
const switchProfileButton_press = () => {
props.dispatch({ type: 'SIDE_MENU_CLOSE' });
props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileSwitcher',
});
};
const configButton_press = () => {
props.dispatch({ type: 'SIDE_MENU_CLOSE' });
void NavService.go('Config');
@ -403,6 +414,10 @@ const SideMenuContentComponent = (props: Props) => {
items.push(renderSidebarButton('tag_button', _('Tags'), 'md-pricetag', tagButton_press));
if (props.profileConfig && props.profileConfig.profiles.length > 1) {
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'md-people-circle-outline', switchProfileButton_press));
}
items.push(renderSidebarButton('config_button', _('Configuration'), 'md-settings', configButton_press));
items.push(makeDivider('divider_2'));
@ -502,5 +517,6 @@ export default connect((state: AppState) => {
resourceFetcher: state.resourceFetcher,
isOnMobileData: state.isOnMobileData,
syncOnlyOverWifi: state.settings['sync.mobileWifiOnly'],
profileConfig: state.profileConfig,
};
})(SideMenuContentComponent);

View File

@ -335,6 +335,8 @@ PODS:
- React-Core
- RNDateTimePicker (6.7.1):
- React-Core
- RNExitApp (1.1.0):
- React
- RNFileViewer (2.1.5):
- React-Core
- RNFS (2.20.0):
@ -403,6 +405,7 @@ DEPENDENCIES:
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
- "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- RNExitApp (from `../node_modules/react-native-exit-app`)
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
- RNFS (from `../node_modules/react-native-fs`)
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
@ -518,6 +521,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/push-notification-ios"
RNDateTimePicker:
:path: "../node_modules/@react-native-community/datetimepicker"
RNExitApp:
:path: "../node_modules/react-native-exit-app"
RNFileViewer:
:path: "../node_modules/react-native-file-viewer"
RNFS:
@ -586,6 +591,7 @@ SPEC CHECKSUMS:
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNCPushNotificationIOS: 87b8d16d3ede4532745e05b03c42cff33a36cc45
RNDateTimePicker: 0530a73a6f3a1a85814cbde0802736993b9e675e
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93

View File

@ -44,6 +44,7 @@
"react-native-dialogbox": "0.6.10",
"react-native-document-picker": "8.1.3",
"react-native-dropdownalert": "4.5.1",
"react-native-exit-app": "1.1.0",
"react-native-file-viewer": "2.1.5",
"react-native-fingerprint-scanner": "6.0.0",
"react-native-fs": "2.20.0",

View File

@ -15,7 +15,6 @@ import KvStore from '@joplin/lib/services/KvStore';
import NoteScreen from './components/screens/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting, { Env } from '@joplin/lib/models/Setting';
import RNFetchBlob from 'rn-fetch-blob';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import reducer from '@joplin/lib/reducer';
import ShareExtension from './utils/ShareExtension';
@ -27,6 +26,7 @@ import { setLocale, closestSupportedLocale, defaultLocale } from '@joplin/lib/lo
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
const VersionInfo = require('react-native-version-info').default;
const { Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking, Platform, Dimensions } = require('react-native');
const RNAppState = require('react-native').AppState;
@ -36,7 +36,7 @@ const DropdownAlert = require('react-native-dropdownalert').default;
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
const SafeAreaView = require('./components/SafeAreaView');
const { connect, Provider } = require('react-redux');
import { Provider as PaperProvider, MD2DarkTheme as PaperDarkTheme, MD2LightTheme as PaperLightTheme } from 'react-native-paper';
import { Provider as PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
const { BackButtonService } = require('./services/back-button.js');
import NavService from '@joplin/lib/services/NavService';
import { createStore, applyMiddleware } from 'redux';
@ -111,7 +111,11 @@ import RSA from './services/e2ee/RSA.react-native';
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { Theme, ThemeAppearance } from '@joplin/lib/themes/type';
import { AppState } from './utils/types';
import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher';
import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
import sensorInfo from './components/biometrics/sensorInfo';
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
import { getDatabaseName, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles';
let storeDispatch = function(_action: any) {};
@ -409,11 +413,23 @@ function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
async function initialize(dispatch: Function) {
shimInit();
setDispatch(dispatch);
const { profileConfig, isSubProfile } = await initProfile(getProfilesRootDir());
const currentProfile = getCurrentProfile(profileConfig);
dispatch({
type: 'PROFILE_CONFIG_SET',
value: profileConfig,
});
// @ts-ignore
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
Setting.setConstant('appType', 'mobile');
Setting.setConstant('resourceDir', RNFetchBlob.fs.dirs.DocumentDir);
const resourceDir = getResourceDir(currentProfile, isSubProfile);
Setting.setConstant('resourceDir', resourceDir);
await shim.fsDriver().mkdir(resourceDir);
const logDatabase = new Database(new DatabaseDriverReactNative());
await logDatabase.open({ name: 'log.sqlite' });
@ -481,9 +497,9 @@ async function initialize(dispatch: Function) {
try {
if (Setting.value('env') === 'prod') {
await db.open({ name: 'joplin.sqlite' });
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
} else {
await db.open({ name: 'joplin-101.sqlite' });
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
// await db.clearForTesting();
}
@ -771,6 +787,13 @@ class AppComponent extends React.Component {
type: 'APP_STATE_SET',
state: 'ready',
});
// setTimeout(() => {
// this.props.dispatch({
// type: 'NAV_GO',
// routeName: 'ProfileSwitcher',
// });
// }, 1000);
}
Linking.addEventListener('url', this.handleOpenURL_);
@ -904,6 +927,8 @@ class AppComponent extends React.Component {
DropboxLogin: { screen: DropboxLoginScreen },
EncryptionConfig: { screen: EncryptionConfigScreen },
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
ProfileSwitcher: { screen: ProfileSwitcher },
ProfileEditor: { screen: ProfileEditor },
Log: { screen: LogScreen },
Status: { screen: StatusScreen },
Search: { screen: SearchScreen },
@ -934,7 +959,7 @@ class AppComponent extends React.Component {
<SafeAreaView style={{ flex: 0, backgroundColor: theme.backgroundColor2 }}/>
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
<AppNav screens={appNavInit} />
<AppNav screens={appNavInit} dispatch={this.props.dispatch} />
</View>
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
<Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/>
@ -949,17 +974,20 @@ class AppComponent extends React.Component {
);
const paperTheme = theme.appearance === ThemeAppearance.Dark ? PaperDarkTheme : PaperLightTheme;
const paperTheme = theme.appearance === ThemeAppearance.Dark ? MD3DarkTheme : MD3LightTheme;
// Wrap everything in a PaperProvider -- this allows using components from react-native-paper
return (
<PaperProvider theme={{
...paperTheme,
version: 2,
version: 3,
colors: {
...paperTheme.colors,
primary: theme.backgroundColor,
accent: theme.backgroundColor2,
onPrimaryContainer: theme.color5,
primaryContainer: theme.backgroundColor5,
surfaceVariant: theme.backgroundColor,
onSurfaceVariant: theme.color,
primary: theme.color,
},
}}>
{mainContent}

View File

@ -0,0 +1,51 @@
// Helper functions to reduce the boiler plate of loading and saving profiles on
// mobile
const RNExitApp = require('react-native-exit-app').default;
import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import { loadProfileConfig as libLoadProfileConfig, saveProfileConfig as libSaveProfileConfig } from '@joplin/lib/services/profileConfig/index';
import RNFetchBlob from 'rn-fetch-blob';
let dispatch_: Function = null;
export const setDispatch = (dispatch: Function) => {
dispatch_ = dispatch;
};
export const getProfilesRootDir = () => {
return RNFetchBlob.fs.dirs.DocumentDir;
};
export const getProfilesConfigPath = () => {
return `${getProfilesRootDir()}/profiles.json`;
};
export const getResourceDir = (profile: Profile, isSubProfile: boolean) => {
if (!isSubProfile) return getProfilesRootDir();
return `${getProfilesRootDir()}/resources-${profile.id}`;
};
export const getDatabaseName = (profile: Profile, isSubProfile: boolean) => {
if (!isSubProfile) return 'joplin.sqlite';
return `joplin-${profile.id}.sqlite`;
};
export const loadProfileConfig = async () => {
return libLoadProfileConfig(getProfilesConfigPath());
};
export const saveProfileConfig = async (profileConfig: ProfileConfig) => {
await libSaveProfileConfig(getProfilesConfigPath(), profileConfig);
dispatch_({
type: 'PROFILE_CONFIG_SET',
value: profileConfig,
});
};
export const switchProfile = async (profileId: string) => {
const config = await loadProfileConfig();
if (config.currentProfileId === profileId) throw new Error('This profile is already active');
config.currentProfileId = profileId;
await saveProfileConfig(config);
RNExitApp.exitApp();
};

View File

@ -0,0 +1,11 @@
const { themeStyle } = require('../components/global-style');
export default (themeId: number) => {
const theme = themeStyle(themeId);
return {
root: {
flex: 1,
backgroundColor: theme.backgroundColor,
},
};
};

View File

@ -2,6 +2,7 @@ import { rtrimSlashes } from '../../path-utils';
import shim from '../../shim';
import { CurrentProfileVersion, defaultProfile, defaultProfileConfig, DefaultProfileId, Profile, ProfileConfig } from './types';
import { customAlphabet } from 'nanoid/non-secure';
import { _ } from '../../locale';
export const migrateProfileConfig = (profileConfig: any, toVersion: number): ProfileConfig => {
let version = 2;
@ -99,6 +100,17 @@ export const createNewProfile = (config: ProfileConfig, profileName: string) =>
};
};
export const deleteProfileById = (config: ProfileConfig, profileId: string): ProfileConfig => {
if (profileId === DefaultProfileId) throw new Error(_('The default profile cannot be deleted'));
if (profileId === config.currentProfileId) throw new Error(_('The active profile cannot be deleted. Switch to a different profile and try again.'));
const newProfiles = config.profiles.filter(p => p.id !== profileId);
return {
...config,
profiles: newProfiles,
};
};
export const profileIdByIndex = (config: ProfileConfig, index: number): string => {
return config.profiles[index].id;
};

View File

@ -41,6 +41,9 @@ export interface Theme {
backgroundColor4: string;
color4: string;
backgroundColor5?: string;
color5?: string;
raisedBackgroundColor: string;
raisedColor: string;
searchMarkerBackgroundColor: string;

View File

@ -112,7 +112,7 @@
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"labels": ["automerge"],
"schedule": "on the first day of the week",
"extends": ["schedule:monthly"],
"matchPackageNames": [
// AWS packages are updated too frequently and we can assume minor
// updates are stable.

View File

@ -4737,6 +4737,7 @@ __metadata:
react-native-dialogbox: 0.6.10
react-native-document-picker: 8.1.3
react-native-dropdownalert: 4.5.1
react-native-exit-app: 1.1.0
react-native-file-viewer: 2.1.5
react-native-fingerprint-scanner: 6.0.0
react-native-fs: 2.20.0
@ -27618,6 +27619,13 @@ __metadata:
languageName: node
linkType: hard
"react-native-exit-app@npm:1.1.0":
version: 1.1.0
resolution: "react-native-exit-app@npm:1.1.0"
checksum: 383e28e03759ebf21ae54cb914e06462fb3ef43ed78895e83395386d615c9a56faacba599edf17677e44a4ebf641102b295545964b787cc09b01b16c9384e8d9
languageName: node
linkType: hard
"react-native-file-viewer@npm:2.1.5":
version: 2.1.5
resolution: "react-native-file-viewer@npm:2.1.5"