Merge branch 'dev' into release-2.10

pull/7622/head
Laurent Cozic 2023-01-14 16:57:14 +00:00
commit 6d4394a88d
67 changed files with 862 additions and 3279 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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'],

1549
.gitignore vendored

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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()));

View File

@ -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();

View File

@ -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();

View File

@ -35,14 +35,14 @@ describe('useCursorUtils', () => {
const numberedListWithEmptyLines = [
'1. item1',
'2. item2',
'3. ' ,
'3. ',
'4. item3',
];
const noPrefixListWithEmptyLines = [
'item1',
'item2',
'' ,
'',
'item3',
];

View File

@ -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);
}

View File

@ -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;

View File

@ -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%',

View File

@ -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) => {

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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`,

View File

@ -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>
);

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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,

View File

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

View File

@ -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}>

View File

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

View File

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

View File

@ -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

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -111,7 +111,7 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
new HeadBucketCommand({
Bucket: options.path(),
}),(err, response) => {
}), (err, response) => {
if (err) reject(err);
else resolve(response);
});

View File

@ -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);
});

View File

@ -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';

View File

@ -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);
},
};

View File

@ -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] },

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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}`);
}

View File

@ -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 () => {

View File

@ -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 {

View File

@ -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,

View File

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

View File

@ -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 () => {

View File

@ -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;

View File

@ -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();

View File

@ -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(' ');

View File

@ -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();

View File

@ -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.

View File

@ -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,

View File

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

View File

@ -60,7 +60,7 @@ const useScrollSaver = ({ container, scaledSize, pdfId, rememberScroll, onActive
useEffect(() => {
currentScaleSize.current = scaledSize;
} , [scaledSize]);
}, [scaledSize]);
};
export default useScrollSaver;

View File

@ -20,7 +20,7 @@ const useVisibleOnSelect = ({ container, wrapperRef, isVisible, isSelected }: Vi
useEffect(() => {
isVisibleRef.current = isVisible;
} , [isVisible]);
}, [isVisible]);
};

View File

@ -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)} />

View File

@ -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 });

View File

@ -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;
};

View File

@ -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' },

View File

@ -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');

View File

@ -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)/;

View File

@ -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);

View File

@ -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
}
]
}

View File

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

View File

@ -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