mirror of https://github.com/laurent22/joplin.git
Merge branch 'dev' into release-2.10
commit
6d4394a88d
1545
.eslintignore
1545
.eslintignore
File diff suppressed because it is too large
Load Diff
|
@ -109,6 +109,7 @@ module.exports = {
|
|||
'exports': 'always-multiline',
|
||||
'functions': 'never',
|
||||
}],
|
||||
'comma-spacing': ['error', { 'before': false, 'after': true }],
|
||||
'no-trailing-spaces': 'error',
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'prefer-template': ['error'],
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -36,7 +36,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/J
|
|||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.8.1/joplin-v2.8.1.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.8.1/joplin-v2.8.1-32bit.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8-32bit.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
|
|
@ -26,7 +26,7 @@ class Command extends BaseCommand {
|
|||
|
||||
const destinationDuplicates = await Folder.search({ titlePattern: destination, limit: 2 });
|
||||
if (destinationDuplicates.length > 1) {
|
||||
throw new Error(_('Ambiguous notebook "%s". Please use short notebook id instead - press "ti" to see the short notebook id' , destination));
|
||||
throw new Error(_('Ambiguous notebook "%s". Please use short notebook id instead - press "ti" to see the short notebook id', destination));
|
||||
}
|
||||
|
||||
const itemFolder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
|
|
|
@ -36,7 +36,7 @@ class FolderListWidget extends ListWidget {
|
|||
if (Setting.value('showNoteCounts')) {
|
||||
let noteCount = item.note_count;
|
||||
// Subtract children note_count from parent folder.
|
||||
if (this.folderHasChildren_(this.folders,item.id)) {
|
||||
if (this.folderHasChildren_(this.folders, item.id)) {
|
||||
for (let i = 0; i < this.folders.length; i++) {
|
||||
if (this.folders[i].parent_id === item.id) {
|
||||
noteCount -= this.folders[i].note_count;
|
||||
|
|
|
@ -559,7 +559,12 @@ class Application extends BaseApplication {
|
|||
|
||||
await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
|
||||
|
||||
// await populateDatabase(reg.db());
|
||||
// await populateDatabase(reg.db(), {
|
||||
// clearDatabase: true,
|
||||
// folderCount: 1000,
|
||||
// rootFolderCount: 1,
|
||||
// subFolderDepth: 1,
|
||||
// });
|
||||
|
||||
// setTimeout(() => {
|
||||
// console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
|
||||
|
|
|
@ -40,7 +40,7 @@ async function fetchLatestRelease(options: CheckForUpdateOptions) {
|
|||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(`Cannot get latest release info: ${responseText.substr(0,500)}`);
|
||||
throw new Error(`Cannot get latest release info: ${responseText.substr(0, 500)}`);
|
||||
}
|
||||
|
||||
const releases = await response.json();
|
||||
|
|
|
@ -16,6 +16,7 @@ import NoteTextViewer from '../../../NoteTextViewer';
|
|||
import Editor from './Editor';
|
||||
import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import markdownUtils from '@joplin/lib/markdownUtils';
|
||||
|
@ -292,9 +293,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const editorPasteText = useCallback(() => {
|
||||
const editorPasteText = useCallback(async () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.replaceSelection(clipboard.readText());
|
||||
const modifiedMd = await Note.replaceResourceExternalToInternalLinks(clipboard.readText(), { useAbsolutePaths: true });
|
||||
editorRef.current.replaceSelection(modifiedMd);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
@ -302,7 +304,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
|||
const clipboardText = clipboard.readText();
|
||||
|
||||
if (clipboardText) {
|
||||
editorPasteText();
|
||||
void editorPasteText();
|
||||
} else {
|
||||
// To handle pasting images
|
||||
void onEditorPaste();
|
||||
|
|
|
@ -35,14 +35,14 @@ describe('useCursorUtils', () => {
|
|||
const numberedListWithEmptyLines = [
|
||||
'1. item1',
|
||||
'2. item2',
|
||||
'3. ' ,
|
||||
'3. ',
|
||||
'4. item3',
|
||||
];
|
||||
|
||||
const noPrefixListWithEmptyLines = [
|
||||
'item1',
|
||||
'item2',
|
||||
'' ,
|
||||
'',
|
||||
'item3',
|
||||
];
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ export default function useCursorUtils(CodeMirror: any) {
|
|||
const lines = selected.split(/\r?\n/);
|
||||
// Save the newline character to restore it later
|
||||
const newLines = selected.match(/\r?\n/);
|
||||
modifyListLines(lines,num,string1);
|
||||
modifyListLines(lines, num, string1);
|
||||
const newLine = newLines !== null ? newLines[0] : '\n';
|
||||
selectedStrings[i] = lines.join(newLine);
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
|||
return prop_htmlToMarkdownRef.current(props.contentMarkupLanguage, editorRef.current.getContent(), props.contentOriginalCss);
|
||||
},
|
||||
resetScroll: () => {
|
||||
if (editor) editor.getWin().scrollTo(0,0);
|
||||
if (editor) editor.getWin().scrollTo(0, 0);
|
||||
},
|
||||
scrollTo: (options: ScrollOptions) => {
|
||||
if (!editor) return;
|
||||
|
|
|
@ -7,7 +7,7 @@ export default function styles(props: NoteEditorProps) {
|
|||
return {
|
||||
root: {
|
||||
boxSizing: 'border-box',
|
||||
paddingLeft: 0,// theme.mainPadding,
|
||||
paddingLeft: 0, // theme.mainPadding,
|
||||
paddingTop: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
|
@ -124,7 +124,7 @@ const NoteListComponent = (props: Props) => {
|
|||
});
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.selectedNoteIds, props.notes, props.dispatch, props.watchedNoteFiles,props.plugins, props.selectedFolderId, props.customCss]);
|
||||
}, [props.selectedNoteIds, props.notes, props.dispatch, props.watchedNoteFiles, props.plugins, props.selectedFolderId, props.customCss]);
|
||||
|
||||
const onGlobalDrop_ = () => {
|
||||
unregisterGlobalDragEndEvent_();
|
||||
|
@ -307,7 +307,7 @@ const NoteListComponent = (props: Props) => {
|
|||
updateSizeState();
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [previousSelectedNoteIds,previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
|
||||
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
|
||||
|
||||
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
|
||||
|
||||
|
|
|
@ -135,8 +135,7 @@ const SidebarComponent = (props: Props) => {
|
|||
tagItemsOrder_.current = [];
|
||||
|
||||
const rootRef = useRef(null);
|
||||
const anchorItemRefs = useRef<Record<string, any>>(null);
|
||||
anchorItemRefs.current = {};
|
||||
const anchorItemRefs = useRef<Record<string, any>>({});
|
||||
|
||||
// This whole component is a bit of a mess and rather than passing
|
||||
// a plugins prop around, not knowing how it's going to affect
|
||||
|
@ -695,6 +694,11 @@ const SidebarComponent = (props: Props) => {
|
|||
})
|
||||
);
|
||||
|
||||
const foldersStyle = useMemo(() => {
|
||||
return { display: props.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 };
|
||||
}, [props.folderHeaderIsExpanded]);
|
||||
|
||||
|
||||
if (props.folders.length) {
|
||||
const allNotesSelected = props.notesParentType === 'SmartFilter' && props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
||||
const result = shared.renderFolders(props, renderFolderItem);
|
||||
|
@ -704,7 +708,7 @@ const SidebarComponent = (props: Props) => {
|
|||
<div
|
||||
className={`folders ${props.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
||||
key="folder_items"
|
||||
style={{ display: props.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}
|
||||
style={foldersStyle}
|
||||
>
|
||||
{folderItems}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
const smalltalk = require('smalltalk/bundle');
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
// Can't upgrade beyond 2.x because it doesn't work with Electron. If trying to
|
||||
// upgrade again, check that adding a link from the CodeMirror editor works/
|
||||
const smalltalk = require('smalltalk');
|
||||
|
||||
const logger = Logger.create('dialogs');
|
||||
|
||||
class Dialogs {
|
||||
async alert(message: string, title = '') {
|
||||
|
@ -10,6 +16,7 @@ class Dialogs {
|
|||
await smalltalk.confirm(title, message, options);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +28,7 @@ class Dialogs {
|
|||
const answer = await smalltalk.prompt(title, message, defaultValue, options);
|
||||
return answer;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
"redux": "4.2.0",
|
||||
"reselect": "4.1.7",
|
||||
"roboto-fontface": "0.10.0",
|
||||
"smalltalk": "4.1.1",
|
||||
"smalltalk": "2.5.1",
|
||||
"sqlite3": "5.1.4",
|
||||
"styled-components": "5.3.6",
|
||||
"styled-system": "5.1.5",
|
||||
|
|
|
@ -80,13 +80,15 @@ async function main() {
|
|||
|
||||
const files = [
|
||||
'@fortawesome/fontawesome-free/css/all.min.css',
|
||||
'react-datetime/css/react-datetime.css',
|
||||
'smalltalk/css/smalltalk.css',
|
||||
'roboto-fontface/css/roboto/roboto-fontface.css',
|
||||
'codemirror/lib/codemirror.css',
|
||||
'codemirror/addon/dialog/dialog.css',
|
||||
'@joeattardi/emoji-button/dist/index.js',
|
||||
'codemirror/addon/dialog/dialog.css',
|
||||
'codemirror/lib/codemirror.css',
|
||||
'mark.js/dist/mark.min.js',
|
||||
'react-datetime/css/react-datetime.css',
|
||||
'roboto-fontface/css/roboto/roboto-fontface.css',
|
||||
'smalltalk/css/smalltalk.css',
|
||||
'smalltalk/img/IDR_CLOSE_DIALOG_H.png',
|
||||
'smalltalk/img/IDR_CLOSE_DIALOG.png',
|
||||
{
|
||||
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
|
||||
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const React = require('react');
|
||||
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle } from 'react-native';
|
||||
import { Component } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { ItemList } = require('./ItemList.js');
|
||||
|
||||
type ValueType = string;
|
||||
|
@ -58,6 +59,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
|||
const items = this.props.items;
|
||||
const itemHeight = 60;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
|
||||
// Dimensions doesn't return quite the right dimensions so leave an extra gap to make
|
||||
// sure nothing is off screen.
|
||||
|
@ -66,11 +68,20 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
|||
const maxListTop = windowHeight - listHeight;
|
||||
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
|
||||
|
||||
const wrapperStyle = {
|
||||
const wrapperStyle: ViewStyle = {
|
||||
width: this.state.headerSize.width,
|
||||
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
|
||||
marginTop: listTop,
|
||||
marginLeft: this.state.headerSize.x,
|
||||
top: listTop,
|
||||
left: this.state.headerSize.x,
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
const backgroundCloseButtonStyle: ViewStyle = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: windowHeight,
|
||||
width: windowWidth,
|
||||
};
|
||||
|
||||
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
|
||||
|
@ -126,6 +137,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
|||
return (
|
||||
<TouchableOpacity
|
||||
style={itemWrapperStyle}
|
||||
accessibilityRole="menuitem"
|
||||
key={key}
|
||||
onPress={() => {
|
||||
closeList();
|
||||
|
@ -139,6 +151,22 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
|||
);
|
||||
};
|
||||
|
||||
// Use a separate screen-reader-only button for closing the menu. If we
|
||||
// allow the background to be focusable, instead, the focus order might be
|
||||
// incorrect on some devices. For example, the background button might be focused
|
||||
// when navigating near the middle of the dropdown's list.
|
||||
const screenReaderCloseMenuButton = (
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityRole='button'
|
||||
onPress={()=> closeList()}
|
||||
>
|
||||
<Text style={{
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
}}>{_('Close dropdown')}</Text>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<TouchableOpacity
|
||||
|
@ -163,21 +191,28 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
|||
}}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPressOut={() => {
|
||||
accessibilityElementsHidden={true}
|
||||
importantForAccessibility='no-hide-descendants'
|
||||
onPress={() => {
|
||||
closeList();
|
||||
}}
|
||||
style={backgroundCloseButtonStyle}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={wrapperStyle}>
|
||||
<ItemList
|
||||
style={itemListStyle}
|
||||
items={this.props.items}
|
||||
itemHeight={itemHeight}
|
||||
itemRenderer={itemRenderer}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}/>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
<View
|
||||
accessibilityRole='menu'
|
||||
style={wrapperStyle}>
|
||||
<ItemList
|
||||
style={itemListStyle}
|
||||
items={this.props.items}
|
||||
itemHeight={itemHeight}
|
||||
itemRenderer={itemRenderer}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{screenReaderCloseMenuButton}
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -103,7 +103,7 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
|
|||
return (
|
||||
<View style={{ flex: 0, margin: 20, alignItems: 'center' }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
{ this.state.date && <Text style={{ ...theme.normalText,color: theme.color, marginRight: 10 }}>{time.formatDateToLocal(this.state.date)}</Text> }
|
||||
{ this.state.date && <Text style={{ ...theme.normalText, color: theme.color, marginRight: 10 }}>{time.formatDateToLocal(this.state.date)}</Text> }
|
||||
<Button title="Set date" onPress={this.onSetDate} />
|
||||
</View>
|
||||
<DateTimePickerModal
|
||||
|
|
|
@ -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} />;
|
||||
};
|
|
@ -1,17 +1,37 @@
|
|||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
const { NotesScreen } = require('./screens/notes.js');
|
||||
const { SearchScreen } = require('./screens/search.js');
|
||||
const { KeyboardAvoidingView, Keyboard, Platform, View } = require('react-native');
|
||||
import { Component } from 'react';
|
||||
import { KeyboardAvoidingView, Keyboard, Platform, View, KeyboardEvent, Dimensions, EmitterSubscription } from 'react-native';
|
||||
import { AppState } from '../utils/types';
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
|
||||
class AppNavComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface State {
|
||||
autoCompletionBarExtraHeight: number;
|
||||
floatingKeyboardEnabled: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
route: any;
|
||||
screens: any;
|
||||
dispatch: (action: any)=> void;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
class AppNavComponent extends Component<Props, State> {
|
||||
private previousRouteName_: string|null = null;
|
||||
private keyboardDidShowListener: EmitterSubscription|null = null;
|
||||
private keyboardDidHideListener: EmitterSubscription|null = null;
|
||||
private keyboardWillChangeFrameListener: EmitterSubscription|null = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.previousRouteName_ = null;
|
||||
this.state = {
|
||||
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
|
||||
floatingKeyboardEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -19,14 +39,18 @@ class AppNavComponent extends Component {
|
|||
if (Platform.OS === 'ios') {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
|
||||
this.keyboardWillChangeFrameListener = Keyboard.addListener('keyboardWillChangeFrame', this.keyboardWillChangeFrame);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidShowListener) this.keyboardDidShowListener.remove();
|
||||
if (this.keyboardDidHideListener) this.keyboardDidHideListener.remove();
|
||||
this.keyboardDidShowListener?.remove();
|
||||
this.keyboardDidHideListener?.remove();
|
||||
this.keyboardWillChangeFrameListener?.remove();
|
||||
|
||||
this.keyboardDidShowListener = null;
|
||||
this.keyboardDidHideListener = null;
|
||||
this.keyboardWillChangeFrameListener = null;
|
||||
}
|
||||
|
||||
keyboardDidShow() {
|
||||
|
@ -37,6 +61,16 @@ class AppNavComponent extends Component {
|
|||
this.setState({ autoCompletionBarExtraHeight: 0 });
|
||||
}
|
||||
|
||||
keyboardWillChangeFrame = (evt: KeyboardEvent) => {
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
|
||||
// If the keyboard isn't as wide as the window, the floating keyboard is diabled.
|
||||
// See https://github.com/facebook/react-native/issues/29473#issuecomment-696658937
|
||||
this.setState({
|
||||
floatingKeyboardEnabled: evt.endCoordinates.width < windowWidth,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.route) throw new Error('Route must not be null');
|
||||
|
||||
|
@ -67,18 +101,27 @@ class AppNavComponent extends Component {
|
|||
|
||||
const style = { flex: 1, backgroundColor: theme.backgroundColor };
|
||||
|
||||
// When the floating keybaord is enabled, the KeyboardAvoidingView can have a very small
|
||||
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
|
||||
// See https://github.com/facebook/react-native/issues/29473
|
||||
const keyboardAvoidingViewEnabled = !this.state.floatingKeyboardEnabled;
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : null} style={style}>
|
||||
<KeyboardAvoidingView
|
||||
enabled={keyboardAvoidingViewEnabled}
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AppNav = connect(state => {
|
||||
const AppNav = connect((state: AppState) => {
|
||||
return {
|
||||
route: state.route,
|
||||
themeId: state.settings.theme,
|
|
@ -69,6 +69,9 @@ function addExtraStyles(style) {
|
|||
|
||||
style.keyboardAppearance = style.appearance;
|
||||
|
||||
style.color5 = style.backgroundColor4;
|
||||
style.backgroundColor5 = style.color4;
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
|
||||
import Slider from '@react-native-community/slider';
|
||||
const React = require('react');
|
||||
const { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } = require('react-native');
|
||||
import { Platform, Linking, View, Switch, StyleSheet, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from 'react-native';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import ReportService from '@joplin/lib/services/ReportService';
|
||||
|
@ -12,6 +12,7 @@ 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 ScreenHeader from '../ScreenHeader';
|
||||
|
@ -100,6 +101,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();
|
||||
|
@ -320,6 +328,36 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private handleBackButtonPress = (): boolean => {
|
||||
const goBack = async () => {
|
||||
BackButtonService.removeHandler(this.handleBackButtonPress);
|
||||
await BackButtonService.back();
|
||||
};
|
||||
|
||||
if (this.state.changedSettingKeys.length > 0) {
|
||||
const dialogTitle: string|null = null;
|
||||
Alert.alert(
|
||||
dialogTitle,
|
||||
_('There are unsaved changes.'),
|
||||
[{
|
||||
text: _('Save changes'),
|
||||
onPress: async () => {
|
||||
await this.saveButton_press();
|
||||
await goBack();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Discard changes'),
|
||||
onPress: goBack,
|
||||
}]
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.navigation.state.sectionName) {
|
||||
setTimeout(() => {
|
||||
|
@ -330,6 +368,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
BackButtonService.addHandler(this.handleBackButtonPress);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
BackButtonService.removeHandler(this.handleBackButtonPress);
|
||||
}
|
||||
|
||||
renderHeader(key: string, title: string) {
|
||||
|
@ -341,7 +385,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderButton(key: string, title: string, clickHandler: Function, options: any = null) {
|
||||
renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
let descriptionComp = null;
|
||||
|
@ -564,6 +608,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') {
|
||||
|
@ -623,7 +668,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||
<View key="donate_link" style={this.styles().settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/donate/');
|
||||
void Linking.openURL('https://joplinapp.org/donate/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={this.styles().linkText}>
|
||||
|
@ -637,7 +682,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||
<View key="website_link" style={this.styles().settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/');
|
||||
void Linking.openURL('https://joplinapp.org/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={this.styles().linkText}>
|
||||
|
@ -651,7 +696,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
|||
<View key="privacy_link" style={this.styles().settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/privacy/');
|
||||
void Linking.openURL('https://joplinapp.org/privacy/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={this.styles().linkText}>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -422,6 +422,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):
|
||||
|
@ -520,6 +522,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`)
|
||||
|
@ -657,6 +660,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:
|
||||
|
@ -741,6 +746,7 @@ SPEC CHECKSUMS:
|
|||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 87b8d16d3ede4532745e05b03c42cff33a36cc45
|
||||
RNDateTimePicker: 0530a73a6f3a1a85814cbde0802736993b9e675e
|
||||
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
|
|
|
@ -45,6 +45,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",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -111,7 +111,7 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
|
|||
|
||||
new HeadBucketCommand({
|
||||
Bucket: options.path(),
|
||||
}),(err, response) => {
|
||||
}), (err, response) => {
|
||||
if (err) reject(err);
|
||||
else resolve(response);
|
||||
});
|
||||
|
|
|
@ -353,7 +353,7 @@ class FileApiDriverAmazonS3 {
|
|||
Bucket: this.s3_bucket_,
|
||||
CopySource: this.makePath_(oldPath),
|
||||
Key: newPath,
|
||||
}),(err, response) => {
|
||||
}), (err, response) => {
|
||||
if (err) reject(err);
|
||||
else resolve(response);
|
||||
});
|
||||
|
|
|
@ -79,7 +79,7 @@ codeToLanguageE_['la'] = 'Latin';
|
|||
codeToLanguageE_['ln'] = 'Lingala';
|
||||
codeToLanguageE_['lo'] = 'Laothian';
|
||||
codeToLanguageE_['lt'] = 'Lithuanian';
|
||||
codeToLanguageE_['lv'] = 'Lettish';
|
||||
codeToLanguageE_['lv'] = 'Latvian';
|
||||
codeToLanguageE_['mg'] = 'Malagasy';
|
||||
codeToLanguageE_['mi'] = 'Maori';
|
||||
codeToLanguageE_['mk'] = 'Macedonian';
|
||||
|
@ -162,6 +162,7 @@ codeToLanguage_['fr'] = 'Français';
|
|||
codeToLanguage_['he'] = 'עיברית';
|
||||
codeToLanguage_['it'] = 'Italiano';
|
||||
codeToLanguage_['lt'] = 'Lietuvių kalba';
|
||||
codeToLanguage_['lv'] = 'Latviešu';
|
||||
codeToLanguage_['nl'] = 'Nederlands';
|
||||
codeToLanguage_['pl'] = 'Polski';
|
||||
codeToLanguage_['pt'] = 'Português';
|
||||
|
|
|
@ -212,7 +212,7 @@ const markdownUtils = {
|
|||
const filterRegex = /^[# \n\t*`-]*/;
|
||||
const lines = body.trim().split('\n');
|
||||
const title = lines[0].trim();
|
||||
return title.replace(filterRegex, '').replace(mdLinkRegex, '$1').replace(emptyMdLinkRegex, '$1').substring(0,80);
|
||||
return title.replace(filterRegex, '').replace(mdLinkRegex, '$1').replace(emptyMdLinkRegex, '$1').substring(0, 80);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -880,7 +880,7 @@ class Setting extends BaseModel {
|
|||
public: false,
|
||||
},
|
||||
|
||||
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop,AppType.Cli], label: () => _('Show note counts') },
|
||||
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop, AppType.Cli], label: () => _('Show note counts') },
|
||||
|
||||
layoutButtonSequence: {
|
||||
value: Setting.LAYOUT_ALL,
|
||||
|
@ -1259,7 +1259,7 @@ class Setting extends BaseModel {
|
|||
isGlobal: true,
|
||||
},
|
||||
|
||||
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true,appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
||||
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
||||
|
||||
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ export default class OneDriveApi {
|
|||
delete options.contentLength;
|
||||
delete options.startByte;
|
||||
|
||||
const response = await shim.fetch(url,options);
|
||||
const response = await shim.fetch(url, options);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -374,7 +374,7 @@ export default class OneDriveApi {
|
|||
async execAccountPropertiesRequest() {
|
||||
|
||||
try {
|
||||
const response = await this.exec('GET','https://graph.microsoft.com/v1.0/me/drive');
|
||||
const response = await this.exec('GET', 'https://graph.microsoft.com/v1.0/me/drive');
|
||||
const data = await response.json();
|
||||
const accountProperties = { accountType: data.driveType, driveId: data.id };
|
||||
return accountProperties;
|
||||
|
|
|
@ -103,7 +103,7 @@ describe('reducer', function() {
|
|||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
||||
|
||||
// expect that the third note is missing, and the 4th note is now selected
|
||||
const expected = createExpectedState(notes, [0,1,3,4], [3]);
|
||||
const expected = createExpectedState(notes, [0, 1, 3, 4], [3]);
|
||||
|
||||
// check the ids of all the remaining notes
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
|
@ -119,7 +119,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
||||
|
||||
const expected = createExpectedState(notes, [1,2,3,4], [1]);
|
||||
const expected = createExpectedState(notes, [1, 2, 3, 4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -147,7 +147,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,1,2,3], [3]);
|
||||
const expected = createExpectedState(notes, [0, 1, 2, 3], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -161,7 +161,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,2,3,4], [3]);
|
||||
const expected = createExpectedState(notes, [0, 2, 3, 4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -175,7 +175,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,1,2,4], [1]);
|
||||
const expected = createExpectedState(notes, [0, 1, 2, 4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -184,13 +184,13 @@ describe('reducer', function() {
|
|||
it('should delete selected notes', (async () => {
|
||||
const folders = await createNTestFolders(1);
|
||||
const notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
let state = initTestState(folders, 0, notes, [1, 2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,3,4], [3]);
|
||||
const expected = createExpectedState(notes, [0, 3, 4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -199,12 +199,12 @@ describe('reducer', function() {
|
|||
it('should delete note when a notes below it are selected', (async () => {
|
||||
const folders = await createNTestFolders(1);
|
||||
const notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
let state = initTestState(folders, 0, notes, [3, 4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,2,3,4], [3,4]);
|
||||
const expected = createExpectedState(notes, [0, 2, 3, 4], [3, 4]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -213,12 +213,12 @@ describe('reducer', function() {
|
|||
it('should delete note when a notes above it are selected', (async () => {
|
||||
const folders = await createNTestFolders(1);
|
||||
const notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
let state = initTestState(folders, 0, notes, [1, 2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,1,2,4], [1,2]);
|
||||
const expected = createExpectedState(notes, [0, 1, 2, 4], [1, 2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -227,13 +227,13 @@ describe('reducer', function() {
|
|||
it('should delete notes at end', (async () => {
|
||||
const folders = await createNTestFolders(1);
|
||||
const notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
let state = initTestState(folders, 0, notes, [3, 4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
||||
|
||||
const expected = createExpectedState(notes, [0,1,2], [2]);
|
||||
const expected = createExpectedState(notes, [0, 1, 2], [2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -242,14 +242,14 @@ describe('reducer', function() {
|
|||
it('should delete notes when non-contiguous selection', (async () => {
|
||||
const folders = await createNTestFolders(1);
|
||||
const notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0,2,4]);
|
||||
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
||||
|
||||
const expected = createExpectedState(notes, [1,3], [1]);
|
||||
const expected = createExpectedState(notes, [1, 3], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
@ -264,7 +264,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
||||
|
||||
const expected = createExpectedState(folders, [0,1,3,4], [3]);
|
||||
const expected = createExpectedState(folders, [0, 1, 3, 4], [3]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
|
@ -278,7 +278,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
||||
|
||||
const expected = createExpectedState(folders, [0,1,3,4], [1]);
|
||||
const expected = createExpectedState(folders, [0, 1, 3, 4], [1]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
|
@ -292,7 +292,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
||||
|
||||
const expected = createExpectedState(folders, [0,1,3,4], [4]);
|
||||
const expected = createExpectedState(folders, [0, 1, 3, 4], [4]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
|
@ -306,7 +306,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'TAG_DELETE', id: tags[2].id });
|
||||
|
||||
const expected = createExpectedState(tags, [0,1,3,4], [3]);
|
||||
const expected = createExpectedState(tags, [0, 1, 3, 4], [3]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
|
@ -319,7 +319,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'TAG_DELETE', id: tags[4].id });
|
||||
|
||||
const expected = createExpectedState(tags, [0,1,2,3], [2]);
|
||||
const expected = createExpectedState(tags, [0, 1, 2, 3], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
|
@ -332,7 +332,7 @@ describe('reducer', function() {
|
|||
// test action
|
||||
state = reducer(state, { type: 'TAG_DELETE', id: tags[0].id });
|
||||
|
||||
const expected = createExpectedState(tags, [1,2,3,4], [2]);
|
||||
const expected = createExpectedState(tags, [1, 2, 3, 4], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
|
@ -345,18 +345,18 @@ describe('reducer', function() {
|
|||
notes.push(...await createNTestNotes(3, folders[i]));
|
||||
}
|
||||
|
||||
let state = initTestState(folders, 0, notes.slice(0,3), [0]);
|
||||
let state = initTestState(folders, 0, notes.slice(0, 3), [0]);
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2], [0]);
|
||||
let expected = createExpectedState(notes, [0, 1, 2], [0]);
|
||||
|
||||
expect(state.notes.length).toEqual(expected.items.length);
|
||||
expect(getIds(state.notes.slice(0,4))).toEqual(getIds(expected.items));
|
||||
expect(getIds(state.notes.slice(0, 4))).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_SELECT_ALL' });
|
||||
|
||||
expected = createExpectedState(notes.slice(0,3), [0,1,2], [0,1,2]);
|
||||
expected = createExpectedState(notes.slice(0, 3), [0, 1, 2], [0, 1, 2]);
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
@ -395,7 +395,7 @@ describe('reducer', function() {
|
|||
notes.push(...await createNTestNotes(3, folders[i]));
|
||||
}
|
||||
|
||||
let state = initTestState(folders, 0, notes.slice(0,3), [0]);
|
||||
let state = initTestState(folders, 0, notes.slice(0, 3), [0]);
|
||||
state = goToNote(notes, [1], state);
|
||||
state = goToNote(notes, [2], state);
|
||||
|
||||
|
@ -417,7 +417,7 @@ describe('reducer', function() {
|
|||
notes.push(...await createNTestNotes(5, folders[i]));
|
||||
}
|
||||
|
||||
let state = initTestState(folders, 0, notes.slice(0,5), [0]);
|
||||
let state = initTestState(folders, 0, notes.slice(0, 5), [0]);
|
||||
state = goToNote(notes, [1], state);
|
||||
state = goToNote(notes, [2], state);
|
||||
state = goToNote(notes, [3], state);
|
||||
|
@ -426,20 +426,20 @@ describe('reducer', function() {
|
|||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 4)));
|
||||
|
||||
state = goBackWard(state);
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,3)));
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
|
||||
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds(notes.slice(4, 5)));
|
||||
|
||||
state = goBackWard(state);
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,2)));
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 2)));
|
||||
// because we push the last seen note to stack.
|
||||
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds([notes[4], notes[3]]));
|
||||
|
||||
state = goForward(state);
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,3)));
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
|
||||
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds([notes[4]]));
|
||||
|
||||
state = goForward(state);
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0,4)));
|
||||
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 4)));
|
||||
expect(getIds(state.forwardHistoryNotes)).toEqual([]);
|
||||
}));
|
||||
|
||||
|
@ -450,7 +450,7 @@ describe('reducer', function() {
|
|||
notes.push(...await createNTestNotes(5, folders[i]));
|
||||
}
|
||||
|
||||
let state = initTestState(folders, 0, notes.slice(0,5), [0]);
|
||||
let state = initTestState(folders, 0, notes.slice(0, 5), [0]);
|
||||
|
||||
state = goToNote(notes, [1], state);
|
||||
state = goToNote(notes, [2], state);
|
||||
|
|
|
@ -13,7 +13,7 @@ class UndoQueue {
|
|||
push(e: any) {
|
||||
this.inner_.push(e);
|
||||
while (this.length > this.size_) {
|
||||
this.inner_.splice(0,1);
|
||||
this.inner_.splice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ export interface Options {
|
|||
tagsPerNote?: number;
|
||||
silent?: number;
|
||||
clearDatabase?: boolean;
|
||||
rootFolderCount?: number;
|
||||
subFolderDepth?: number;
|
||||
}
|
||||
|
||||
function randomIndex(array: any[]): number {
|
||||
|
@ -47,6 +49,8 @@ export default async function populateDatabase(db: any, options: Options = null)
|
|||
tagCount: 0,
|
||||
tagsPerNote: 0,
|
||||
clearDatabase: false,
|
||||
rootFolderCount: 0,
|
||||
subFolderDepth: 0,
|
||||
...options,
|
||||
};
|
||||
|
||||
|
@ -55,21 +59,45 @@ export default async function populateDatabase(db: any, options: Options = null)
|
|||
const createdFolderIds: string[] = [];
|
||||
const createdNoteIds: string[] = [];
|
||||
const createdTagIds: string[] = [];
|
||||
const createdFolderDepths: Record<string, number> = {};
|
||||
const folderDepthToId: Record<number, string[]> = {};
|
||||
let rootFolderCount: number = 0;
|
||||
|
||||
for (let i = 0; i < options.folderCount; i++) {
|
||||
const folder: any = {
|
||||
title: `folder${i}`,
|
||||
};
|
||||
|
||||
const isRoot = Math.random() <= 0.1 || i === 0;
|
||||
let isRoot = Math.random() <= 0.1 || i === 0;
|
||||
|
||||
if (options.rootFolderCount && rootFolderCount >= options.rootFolderCount) isRoot = false;
|
||||
|
||||
let depth: number = 0;
|
||||
|
||||
if (!isRoot) {
|
||||
const parentIndex = randomIndex(createdFolderIds);
|
||||
folder.parent_id = createdFolderIds[parentIndex];
|
||||
let possibleFolderIds: string[] = [];
|
||||
if (options.subFolderDepth) {
|
||||
for (let i = 0; i < options.subFolderDepth; i++) {
|
||||
if (folderDepthToId[i]) possibleFolderIds = possibleFolderIds.concat(folderDepthToId[i]);
|
||||
}
|
||||
} else {
|
||||
possibleFolderIds = createdFolderIds;
|
||||
}
|
||||
|
||||
const parentIndex = randomIndex(possibleFolderIds);
|
||||
const parentId = possibleFolderIds[parentIndex];
|
||||
folder.parent_id = parentId;
|
||||
depth = createdFolderDepths[parentId] + 1;
|
||||
} else {
|
||||
rootFolderCount++;
|
||||
}
|
||||
|
||||
const savedFolder = await Folder.save(folder);
|
||||
createdFolderIds.push(savedFolder.id);
|
||||
createdFolderDepths[savedFolder.id] = depth;
|
||||
|
||||
if (!folderDepthToId[depth]) folderDepthToId[depth] = [];
|
||||
folderDepthToId[depth].push(savedFolder.id);
|
||||
|
||||
if (!options.silent) console.info(`Folders: ${i} / ${options.folderCount}`);
|
||||
}
|
||||
|
|
|
@ -267,14 +267,14 @@ describe('services_EncryptionService', function() {
|
|||
|
||||
// First check that we can replicate the error with the old encryption method
|
||||
service.defaultEncryptionMethod_ = EncryptionMethod.SJCL;
|
||||
const hasThrown = await checkThrowAsync(async () => await service.encryptString('🐶🐶🐶'.substr(0,5)));
|
||||
const hasThrown = await checkThrowAsync(async () => await service.encryptString('🐶🐶🐶'.substr(0, 5)));
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
// Now check that the new one fixes the problem
|
||||
service.defaultEncryptionMethod_ = EncryptionMethod.SJCL1a;
|
||||
const cipherText = await service.encryptString('🐶🐶🐶'.substr(0,5));
|
||||
const cipherText = await service.encryptString('🐶🐶🐶'.substr(0, 5));
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
expect(plainText).toBe('🐶🐶🐶'.substr(0,5));
|
||||
expect(plainText).toBe('🐶🐶🐶'.substr(0, 5));
|
||||
}));
|
||||
|
||||
it('should check if a master key is loaded', (async () => {
|
||||
|
|
|
@ -66,7 +66,7 @@ export interface PluginSettings {
|
|||
|
||||
function makePluginId(source: string): string {
|
||||
// https://www.npmjs.com/package/slug#options
|
||||
return uslug(source).substr(0,32);
|
||||
return uslug(source).substr(0, 32);
|
||||
}
|
||||
|
||||
export default class PluginService extends BaseService {
|
||||
|
|
|
@ -9,7 +9,7 @@ const shared = require('../../../components/shared/config-shared.js');
|
|||
|
||||
const logger = Logger.create('defaultPluginsUtils');
|
||||
|
||||
export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[],pluginSettings: PluginSettings) {
|
||||
export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[], pluginSettings: PluginSettings) {
|
||||
const installedDefaultPlugins: Array<string> = Setting.value('installedDefaultPlugins');
|
||||
for (const pluginId of defaultPluginsId) {
|
||||
// if pluginId is present in pluginSettings and not in installedDefaultPlugins array,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -298,7 +298,11 @@ describe('services_rest_Api', function() {
|
|||
title: 'testing 4',
|
||||
parent_id: f.id,
|
||||
is_todo: '1',
|
||||
todo_due: '2',
|
||||
todo_completed: '3',
|
||||
}));
|
||||
expect(response.todo_due).toBe(2);
|
||||
expect(response.todo_completed).toBe(3);
|
||||
}));
|
||||
|
||||
it('should create folders with supplied ID', (async () => {
|
||||
|
|
|
@ -108,6 +108,8 @@ async function requestNoteToNote(requestNote: any) {
|
|||
if ('user_updated_time' in requestNote) output.user_updated_time = Database.formatValue(Database.TYPE_INT, requestNote.user_updated_time);
|
||||
if ('user_created_time' in requestNote) output.user_created_time = Database.formatValue(Database.TYPE_INT, requestNote.user_created_time);
|
||||
if ('is_todo' in requestNote) output.is_todo = Database.formatValue(Database.TYPE_INT, requestNote.is_todo);
|
||||
if ('todo_due' in requestNote) output.todo_due = Database.formatValue(Database.TYPE_INT, requestNote.todo_due);
|
||||
if ('todo_completed' in requestNote) output.todo_completed = Database.formatValue(Database.TYPE_INT, requestNote.todo_completed);
|
||||
if ('markup_language' in requestNote) output.markup_language = Database.formatValue(Database.TYPE_INT, requestNote.markup_language);
|
||||
if ('longitude' in requestNote) output.longitude = requestNote.longitude;
|
||||
if ('latitude' in requestNote) output.latitude = requestNote.latitude;
|
||||
|
|
|
@ -604,7 +604,7 @@ describe('services_SearchFilter', function() {
|
|||
const n11 = await Note.save({ title: 'I also made this', body: 'today', updated_time: today, user_updated_time: today }, { autoTimestamp: false });
|
||||
|
||||
const n2 = await Note.save({ title: 'I made this', body: 'yesterday', updated_time: yesterday, user_updated_time: yesterday }, { autoTimestamp: false });
|
||||
const n3 = await Note.save({ title: 'I made this', body: 'day before yesterday', updated_time: dayBeforeYesterday ,user_updated_time: dayBeforeYesterday }, { autoTimestamp: false });
|
||||
const n3 = await Note.save({ title: 'I made this', body: 'day before yesterday', updated_time: dayBeforeYesterday, user_updated_time: dayBeforeYesterday }, { autoTimestamp: false });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
|
|
|
@ -247,7 +247,7 @@ const genericFilter = (terms: Term[], conditions: string[], params: string[], re
|
|||
};
|
||||
|
||||
const biConditionalFilter = (terms: Term[], conditions: string[], relation: Relation, filterName: string, useFts: boolean) => {
|
||||
const getCondition = (filterName: string , value: string, relation: Relation) => {
|
||||
const getCondition = (filterName: string, value: string, relation: Relation) => {
|
||||
const tableName = useFts ? (relation === 'AND' ? 'notes_fts' : 'notes_normalized') : 'notes';
|
||||
if (filterName === 'type') {
|
||||
return `${tableName}.is_todo IS ${value === 'todo' ? 1 : 0}`;
|
||||
|
@ -487,9 +487,9 @@ export default function queryBuilder(terms: Term[], useFts: boolean) {
|
|||
// So we're first finding the OR of other filters and then combining them with the notebook filter using AND
|
||||
// in-required-notebook AND (condition #1 OR condition #2 OR ... )
|
||||
const queryString = `${queryParts.slice(0, 2).join(' ')} AND ROWID IN ( SELECT ROWID FROM notes_fts WHERE 1=0 ${queryParts.slice(2).join(' ')})`;
|
||||
query = ['WITH RECURSIVE' , withs.join(',') , queryString].join(' ');
|
||||
query = ['WITH RECURSIVE', withs.join(','), queryString].join(' ');
|
||||
} else {
|
||||
query = ['WITH RECURSIVE' , withs.join(',') ,queryParts.join(' ')].join(' ');
|
||||
query = ['WITH RECURSIVE', withs.join(','), queryParts.join(' ')].join(' ');
|
||||
}
|
||||
} else {
|
||||
query = queryParts.join(' ');
|
||||
|
|
|
@ -98,7 +98,7 @@ describe('Synchronizer.e2ee', function() {
|
|||
expect(remoteInfo.masterKeys[0].hasBeenUsed).toBe(true);
|
||||
}));
|
||||
|
||||
it('should enable encryption automatically when downloading new master key (and none was previously available)',(async () => {
|
||||
it('should enable encryption automatically when downloading new master key (and none was previously available)', (async () => {
|
||||
// Enable encryption on client 1 and sync an item
|
||||
setEncryptionEnabled(true);
|
||||
await loadEncryptionMasterKey();
|
||||
|
|
|
@ -904,7 +904,7 @@ class TestApp extends BaseApplication {
|
|||
if (!argv.includes('--profile')) {
|
||||
argv = argv.concat(['--profile', `tests-build/profile/${uuid.create()}`]);
|
||||
}
|
||||
argv = await super.start(['',''].concat(argv));
|
||||
argv = await super.start(['', ''].concat(argv));
|
||||
|
||||
// For now, disable sync and encryption to avoid spurious intermittent failures
|
||||
// caused by them interupting processing and causing delays.
|
||||
|
|
|
@ -45,7 +45,7 @@ export function themeById(themeId: string) {
|
|||
// globalStyle should be used for properties that do not change across themes
|
||||
// i.e. should not be used for colors
|
||||
const globalStyle: any = {
|
||||
fontFamily: 'Roboto',// 'sans-serif',
|
||||
fontFamily: 'Roboto', // 'sans-serif',
|
||||
margin: 15, // No text and no interactive component should be within this margin
|
||||
itemMarginTop: 10,
|
||||
itemMarginBottom: 10,
|
||||
|
|
|
@ -41,6 +41,9 @@ export interface Theme {
|
|||
backgroundColor4: string;
|
||||
color4: string;
|
||||
|
||||
backgroundColor5?: string;
|
||||
color5?: string;
|
||||
|
||||
raisedBackgroundColor: string;
|
||||
raisedColor: string;
|
||||
searchMarkerBackgroundColor: string;
|
||||
|
|
|
@ -60,7 +60,7 @@ const useScrollSaver = ({ container, scaledSize, pdfId, rememberScroll, onActive
|
|||
|
||||
useEffect(() => {
|
||||
currentScaleSize.current = scaledSize;
|
||||
} , [scaledSize]);
|
||||
}, [scaledSize]);
|
||||
};
|
||||
|
||||
export default useScrollSaver;
|
||||
|
|
|
@ -20,7 +20,7 @@ const useVisibleOnSelect = ({ container, wrapperRef, isVisible, isSelected }: Vi
|
|||
|
||||
useEffect(() => {
|
||||
isVisibleRef.current = isVisible;
|
||||
} , [isVisible]);
|
||||
}, [isVisible]);
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ export default function GotoInput(props: GotoInputProps) {
|
|||
|
||||
const inputFocus = useCallback(() => {
|
||||
inputRef.current?.select();
|
||||
} , []);
|
||||
}, []);
|
||||
|
||||
const onPageNoInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value.length <= 0) return;
|
||||
|
@ -57,7 +57,7 @@ export default function GotoInput(props: GotoInputProps) {
|
|||
|
||||
useEffect(() => {
|
||||
inputRef.current.value = props.currentPage.toString();
|
||||
} , [props.currentPage]);
|
||||
}, [props.currentPage]);
|
||||
|
||||
return (<Group size={props.size || 1}>
|
||||
<FontAwesomeIcon icon={faAngleLeft} title="Previous Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage - 1)} />
|
||||
|
|
|
@ -172,7 +172,7 @@ describe('api_items', function() {
|
|||
});
|
||||
|
||||
test('should report errors when batch uploading', async function() {
|
||||
const { user: user1,session: session1 } = await createUserAndSession(1, false);
|
||||
const { user: user1, session: session1 } = await createUserAndSession(1, false);
|
||||
|
||||
const note1 = makeNoteSerializedBody({ id: '00000000000000000000000000000001' });
|
||||
await models().user().save({ id: user1.id, max_item_size: note1.length });
|
||||
|
|
|
@ -90,7 +90,7 @@ const later = require('later');
|
|||
if (numbers.length === 2) return expectedStep;
|
||||
|
||||
// Check that every number is the previous number + the first number
|
||||
return numbers.slice(1).every(function(n,i,a) {
|
||||
return numbers.slice(1).every(function(n, i, a) {
|
||||
return (i === 0 ? n : n - a[i - 1]) === expectedStep;
|
||||
}) ? expectedStep : 0;
|
||||
};
|
||||
|
|
|
@ -123,7 +123,7 @@ describe('bundleDefaultPlugins', function() {
|
|||
};
|
||||
|
||||
it('should get local plugin versions', async () => {
|
||||
const manifestsPath = join(supportDir, 'pluginRepo','plugins');
|
||||
const manifestsPath = join(supportDir, 'pluginRepo', 'plugins');
|
||||
const testDefaultPluginsInfo = {
|
||||
'joplin.plugin.ambrt.backlinksToNote': { version: '1.0.4' },
|
||||
'org.joplinapp.plugins.ToggleSidebars': { version: '1.0.2' },
|
||||
|
@ -142,14 +142,14 @@ describe('bundleDefaultPlugins', function() {
|
|||
downloadedPlugin1: 'joplin-plugin-rich-markdown-0.9.0.tgz',
|
||||
downloadedPlugin2: 'joplin-plugin-backup-1.1.0.tgz',
|
||||
numberOfCalls: 4,
|
||||
calledWith: ['https://registry.npmjs.org/joplin-plugin-rich-markdown','response-1-link','https://registry.npmjs.org/joplin-plugin-backup','response-2-link'],
|
||||
calledWith: ['https://registry.npmjs.org/joplin-plugin-rich-markdown', 'response-1-link', 'https://registry.npmjs.org/joplin-plugin-backup', 'response-2-link'],
|
||||
},
|
||||
{
|
||||
localVersions: { 'io.github.jackgruber.backup': '1.1.0', 'plugin.calebjohn.rich-markdown': '0.0.0' },
|
||||
downloadedPlugin1: 'joplin-plugin-rich-markdown-0.9.0.tgz',
|
||||
downloadedPlugin2: undefined,
|
||||
numberOfCalls: 2,
|
||||
calledWith: ['https://registry.npmjs.org/joplin-plugin-rich-markdown','response-1-link'],
|
||||
calledWith: ['https://registry.npmjs.org/joplin-plugin-rich-markdown', 'response-1-link'],
|
||||
},
|
||||
{
|
||||
localVersions: { 'io.github.jackgruber.backup': '1.1.0', 'plugin.calebjohn.rich-markdown': '0.9.0' },
|
||||
|
|
|
@ -34,7 +34,7 @@ async function downloadFile(url: string, outputPath: string) {
|
|||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(`Cannot download file from ${url} : ${responseText.substr(0,500)}`);
|
||||
throw new Error(`Cannot download file from ${url} : ${responseText.substr(0, 500)}`);
|
||||
}
|
||||
await writeFile(outputPath, await response.buffer());
|
||||
}
|
||||
|
@ -42,8 +42,8 @@ async function downloadFile(url: string, outputPath: string) {
|
|||
export async function extractPlugins(currentDir: string, defaultPluginDir: string, downloadedPluginsNames: PluginIdAndName): Promise<void> {
|
||||
for (const pluginId of Object.keys(downloadedPluginsNames)) {
|
||||
await execCommand2(`tar xzf ${currentDir}/${downloadedPluginsNames[pluginId]}`, { quiet: true });
|
||||
await move(`package/publish/${pluginId}.jpl`,`${defaultPluginDir}/${pluginId}/plugin.jpl`, { overwrite: true });
|
||||
await move(`package/publish/${pluginId}.json`,`${defaultPluginDir}/${pluginId}/manifest.json`, { overwrite: true });
|
||||
await move(`package/publish/${pluginId}.jpl`, `${defaultPluginDir}/${pluginId}/plugin.jpl`, { overwrite: true });
|
||||
await move(`package/publish/${pluginId}.json`, `${defaultPluginDir}/${pluginId}/manifest.json`, { overwrite: true });
|
||||
await remove(`${downloadedPluginsNames[pluginId]}`);
|
||||
await remove('package');
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export const downloadPlugins = async (localPluginsVersions: PluginAndVersion, de
|
|||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(`Cannot fetch ${manifests[pluginId]._npm_package_name} release info from NPM : ${responseText.substr(0,500)}`);
|
||||
throw new Error(`Cannot fetch ${manifests[pluginId]._npm_package_name} release info from NPM : ${responseText.substr(0, 500)}`);
|
||||
}
|
||||
const releaseText = await response.text();
|
||||
const release = JSON.parse(releaseText);
|
||||
|
@ -74,7 +74,7 @@ export const downloadPlugins = async (localPluginsVersions: PluginAndVersion, de
|
|||
};
|
||||
|
||||
async function start(): Promise<void> {
|
||||
const defaultPluginDir = join(__dirname, '..', '..', 'packages', 'app-desktop', 'build','defaultPlugins');
|
||||
const defaultPluginDir = join(__dirname, '..', '..', 'packages', 'app-desktop', 'build', 'defaultPlugins');
|
||||
const defaultPluginsInfo = getDefaultPluginsInfo();
|
||||
|
||||
const manifestData = await fetch('https://raw.githubusercontent.com/joplin/plugins/master/manifests.json');
|
||||
|
|
|
@ -51,19 +51,20 @@ module.exports = {
|
|||
return `${s.join('.')}.js`;
|
||||
});
|
||||
|
||||
const ignoredMapFiles = tsFiles.map(f => {
|
||||
const s = f.split('.');
|
||||
s.pop();
|
||||
return `${s.join('.')}.js.map`;
|
||||
});
|
||||
// const ignoredMapFiles = tsFiles.map(f => {
|
||||
// const s = f.split('.');
|
||||
// s.pop();
|
||||
// return `${s.join('.')}.js.map`;
|
||||
// });
|
||||
|
||||
const ignoredDefFiles = tsFiles.map(f => {
|
||||
const s = f.split('.');
|
||||
s.pop();
|
||||
return `${s.join('.')}.d.ts`;
|
||||
});
|
||||
// const ignoredDefFiles = tsFiles.map(f => {
|
||||
// const s = f.split('.');
|
||||
// s.pop();
|
||||
// return `${s.join('.')}.d.ts`;
|
||||
// });
|
||||
|
||||
const ignoredFiles = ignoredJsFiles.concat(ignoredMapFiles).concat(ignoredDefFiles);
|
||||
// const ignoredFiles = ignoredJsFiles.concat(ignoredMapFiles).concat(ignoredDefFiles);
|
||||
const ignoredFiles = ignoredJsFiles;
|
||||
ignoredFiles.sort();
|
||||
|
||||
const regex = /(# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD)[\s\S]*(# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD)/;
|
||||
|
|
|
@ -22,7 +22,7 @@ interface PostContent {
|
|||
parsed: MarkdownAndFrontMatter;
|
||||
}
|
||||
|
||||
const ignoredPostIds = ['20180621-172112','20180621-182112','20180906-101039','20180906-111039','20180916-200431','20180916-210431','20180929-111053','20180929-121053','20181004-081123','20181004-091123','20181101-174335','20181213-173459','20190130-230218','20190404-064157','20190404-074157','20190424-102410','20190424-112410','20190523-221026','20190523-231026','20190610-230711','20190611-000711','20190613-192613','20190613-202613','20190814-215957','20190814-225957','20190924-230254','20190925-000254','20190929-142834','20190929-152834','20191012-223121','20191012-233121','20191014-155136','20191014-165136','20191101-131852','20191117-183855','20191118-072700','20200220-190804','20200301-125055','20200314-001555','20200406-214254','20200406-224254','20200505-181736','20200606-151446','20200607-112720','20200613-103545','20200616-191918','20200620-114515','20200622-084127','20200626-134029','20200708-192444','20200906-172325','20200913-163730','20200915-091108','20201030-114530','20201126-114649','20201130-145937','20201212-172039','20201228-112150','20210104-131645','20210105-153008','20210130-144626','20210309-111950','20210310-100852','20210413-091132','20210430-083248','20210506-083359','20210513-095238','20210518-085514','20210621-104753','20210624-171844','20210705-094247','20210706-140228','20210711-095626','20210718-103538','20210729-103234','20210804-085003','20210831-154354','20210901-113415','20210929-144036','20210930-163458','20211031-115215','20211102-150403','20211217-120324','20220215-142000','20220224-release-2-7','20220308-gsoc2022-start','20220405-gsoc-contributor-proposals'];
|
||||
const ignoredPostIds = ['20180621-172112', '20180621-182112', '20180906-101039', '20180906-111039', '20180916-200431', '20180916-210431', '20180929-111053', '20180929-121053', '20181004-081123', '20181004-091123', '20181101-174335', '20181213-173459', '20190130-230218', '20190404-064157', '20190404-074157', '20190424-102410', '20190424-112410', '20190523-221026', '20190523-231026', '20190610-230711', '20190611-000711', '20190613-192613', '20190613-202613', '20190814-215957', '20190814-225957', '20190924-230254', '20190925-000254', '20190929-142834', '20190929-152834', '20191012-223121', '20191012-233121', '20191014-155136', '20191014-165136', '20191101-131852', '20191117-183855', '20191118-072700', '20200220-190804', '20200301-125055', '20200314-001555', '20200406-214254', '20200406-224254', '20200505-181736', '20200606-151446', '20200607-112720', '20200613-103545', '20200616-191918', '20200620-114515', '20200622-084127', '20200626-134029', '20200708-192444', '20200906-172325', '20200913-163730', '20200915-091108', '20201030-114530', '20201126-114649', '20201130-145937', '20201212-172039', '20201228-112150', '20210104-131645', '20210105-153008', '20210130-144626', '20210309-111950', '20210310-100852', '20210413-091132', '20210430-083248', '20210506-083359', '20210513-095238', '20210518-085514', '20210621-104753', '20210624-171844', '20210705-094247', '20210706-140228', '20210711-095626', '20210718-103538', '20210729-103234', '20210804-085003', '20210831-154354', '20210901-113415', '20210929-144036', '20210930-163458', '20211031-115215', '20211102-150403', '20211217-120324', '20220215-142000', '20220224-release-2-7', '20220308-gsoc2022-start', '20220405-gsoc-contributor-proposals'];
|
||||
|
||||
const getPosts = async (newsDir: string): Promise<Post[]> => {
|
||||
const filenames = await readdir(newsDir);
|
||||
|
|
|
@ -127,6 +127,22 @@
|
|||
"created_at": "2023-01-07T15:21:07Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 7592
|
||||
},
|
||||
{
|
||||
"name": "matthijskooijman",
|
||||
"id": 194491,
|
||||
"comment_id": 1378699237,
|
||||
"created_at": "2023-01-11T12:48:10Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 7611
|
||||
},
|
||||
{
|
||||
"name": "andzs",
|
||||
"id": 7015947,
|
||||
"comment_id": 1380461671,
|
||||
"created_at": "2023-01-12T14:38:26Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 7615
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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.
|
||||
|
|
52
yarn.lock
52
yarn.lock
|
@ -3253,16 +3253,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cloudcmd/create-element@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "@cloudcmd/create-element@npm:2.0.2"
|
||||
dependencies:
|
||||
currify: ^4.0.0
|
||||
fullstore: ^3.0.0
|
||||
checksum: cb1ee97980266326289df56f6d2639eaf9ca5180ef4d2d3cf9f0efbb196b66da31d3aca4bbe500f90cf17d8a4a4411b4468950d774dd50401619c6dc83cf2b51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cnakazawa/watch@npm:^1.0.3":
|
||||
version: 1.0.4
|
||||
resolution: "@cnakazawa/watch@npm:1.0.4"
|
||||
|
@ -4749,7 +4739,7 @@ __metadata:
|
|||
redux: 4.2.0
|
||||
reselect: 4.1.7
|
||||
roboto-fontface: 0.10.0
|
||||
smalltalk: 4.1.1
|
||||
smalltalk: 2.5.1
|
||||
sqlite3: 5.1.4
|
||||
styled-components: 5.3.6
|
||||
styled-system: 5.1.5
|
||||
|
@ -4829,6 +4819,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
|
||||
|
@ -13080,10 +13071,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"currify@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "currify@npm:4.0.0"
|
||||
checksum: a0b1ded58985cd5f9ec08b8ca2b9307c7c379504ff8edfe68be69e368108c14384d63a28ae3ac3591e05287ae06261243ea812e137ed91b165bcbc7f725b9cac
|
||||
"currify@npm:^2.0.3":
|
||||
version: 2.0.6
|
||||
resolution: "currify@npm:2.0.6"
|
||||
checksum: c145dde3a97368beac34d5e39bb33199602ad737a1d9af81efe1bc2d213ceb949486c0dcad3f564e807713ba17a97353398e48de3466262b979134e82ce52ae6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -15428,7 +15419,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es6-promise@npm:^4.0.3":
|
||||
"es6-promise@npm:^4.0.3, es6-promise@npm:^4.1.1":
|
||||
version: 4.2.8
|
||||
resolution: "es6-promise@npm:4.2.8"
|
||||
checksum: 95614a88873611cb9165a85d36afa7268af5c03a378b35ca7bda9508e1d4f1f6f19a788d4bc755b3fd37c8ebba40782018e02034564ff24c9d6fa37e959ad57d
|
||||
|
@ -17182,10 +17173,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fullstore@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "fullstore@npm:3.0.0"
|
||||
checksum: 3a84de9d9133eaa0b19a0ab8062d602bae565be0fde404911c5a77306cff368549a19e27abd39aac119526dde30dcef74e575d8ac878b579ee539bc2bf32163a
|
||||
"fullstore@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "fullstore@npm:1.1.0"
|
||||
checksum: 372bfdc0742ca010abee737e7c7522b5b31abe41812a785392bf8e941c3ca14c04b9716b3358ce0a75f1afa20d0656a984d89fa6985dcde2445ca8b42ff8d9fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -27685,6 +27676,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"
|
||||
|
@ -30154,14 +30152,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"smalltalk@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "smalltalk@npm:4.1.1"
|
||||
"smalltalk@npm:2.5.1":
|
||||
version: 2.5.1
|
||||
resolution: "smalltalk@npm:2.5.1"
|
||||
dependencies:
|
||||
"@cloudcmd/create-element": ^2.0.0
|
||||
currify: ^4.0.0
|
||||
fullstore: ^3.0.0
|
||||
checksum: 80a2f749149696f54472bfdfd92362f49304fd6d3d8bcd4ca5f95e651a78758c4b4157be1b45a81cadeb5f1a700397cbb86d3d056269f5b7782007fce16f40da
|
||||
currify: ^2.0.3
|
||||
es6-promise: ^4.1.1
|
||||
fullstore: ^1.0.0
|
||||
checksum: 17a7f8acde9bed94cd83ac089d60f30368b76a5a43025f506b6ce3e99a8549c7533c914ddbaad0fb0ddc068799c441be9df4d827f3d48bc09794fe08aaa6afd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in New Issue