You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-06-18 20:16:34 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d71ac2e218 | |||
| c35c5a5821 | |||
| bcb08ac8a2 | |||
| f91b4edb30 | |||
| b56177a4e3 | |||
| 6ff8d775c2 | |||
| 4e70ca6fd0 | |||
| 5e8b7420ff | |||
| 8ae4e30fd2 | |||
| 3ce947e82c | |||
| c2298213d7 | |||
| 9679f03cfa | |||
| 3cddac3931 | |||
| 41c1e3bec9 | |||
| 25c5892e74 | |||
| a661a73511 | |||
| b00959e143 | |||
| f6f5d6808d | |||
| 01b653fc34 | |||
| 4e7fe66883 | |||
| cd99e675d9 | |||
| a7130ce17a | |||
| 20f8743079 | |||
| 660b53575e | |||
| 6c43b78496 | |||
| 9d5d891fe3 | |||
| 96ac12b460 | |||
| 4b93664240 | |||
| a2c6461af8 | |||
| d33b99cffb | |||
| 267c32143b | |||
| 9260b2a9ab | |||
| 0a54854f54 | |||
| 496039f15c |
+12
-6
@@ -76,6 +76,9 @@ packages/app-cli/app/command-e2ee.js.map
|
||||
packages/app-cli/app/command-settingschema.d.ts
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-settingschema.js.map
|
||||
packages/app-cli/app/command-testing.d.ts
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-testing.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
@@ -109,6 +112,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
packages/app-cli/tests/testUtils.d.ts
|
||||
packages/app-cli/tests/testUtils.js
|
||||
packages/app-cli/tests/testUtils.js.map
|
||||
packages/app-cli/tools/populateDatabase.d.ts
|
||||
packages/app-cli/tools/populateDatabase.js
|
||||
packages/app-cli/tools/populateDatabase.js.map
|
||||
packages/app-desktop/ElectronAppWrapper.d.ts
|
||||
packages/app-desktop/ElectronAppWrapper.js
|
||||
packages/app-desktop/ElectronAppWrapper.js.map
|
||||
@@ -208,9 +214,9 @@ packages/app-desktop/gui/DialogTitle.js.map
|
||||
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||
packages/app-desktop/gui/ErrorBoundary.js
|
||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||
@@ -931,9 +937,9 @@ packages/lib/commands/index.js.map
|
||||
packages/lib/commands/synchronize.d.ts
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/synchronize.js.map
|
||||
packages/lib/components/shared/encryption-config-shared.d.ts
|
||||
packages/lib/components/shared/encryption-config-shared.js
|
||||
packages/lib/components/shared/encryption-config-shared.js.map
|
||||
packages/lib/components/EncryptionConfigScreen/utils.d.ts
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js.map
|
||||
packages/lib/database.d.ts
|
||||
packages/lib/database.js
|
||||
packages/lib/database.js.map
|
||||
|
||||
@@ -35,14 +35,6 @@ jobs:
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
|
||||
# the next line enables multi-architecture support for docker, it basically makes it use qemu for non native platforms
|
||||
# See https://hub.docker.com/r/tonistiigi/binfmt for more info
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
|
||||
# this just prints the info about what platforms are supported in the builder (can help debugging if something isn't working right)
|
||||
# and also proves the above worked properly
|
||||
sudo docker buildx ls
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: olegtarasov/get-tag@v2.1
|
||||
- uses: actions/setup-node@v2
|
||||
|
||||
+12
-6
@@ -61,6 +61,9 @@ packages/app-cli/app/command-e2ee.js.map
|
||||
packages/app-cli/app/command-settingschema.d.ts
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-settingschema.js.map
|
||||
packages/app-cli/app/command-testing.d.ts
|
||||
packages/app-cli/app/command-testing.js
|
||||
packages/app-cli/app/command-testing.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
@@ -94,6 +97,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
packages/app-cli/tests/testUtils.d.ts
|
||||
packages/app-cli/tests/testUtils.js
|
||||
packages/app-cli/tests/testUtils.js.map
|
||||
packages/app-cli/tools/populateDatabase.d.ts
|
||||
packages/app-cli/tools/populateDatabase.js
|
||||
packages/app-cli/tools/populateDatabase.js.map
|
||||
packages/app-desktop/ElectronAppWrapper.d.ts
|
||||
packages/app-desktop/ElectronAppWrapper.js
|
||||
packages/app-desktop/ElectronAppWrapper.js.map
|
||||
@@ -193,9 +199,9 @@ packages/app-desktop/gui/DialogTitle.js.map
|
||||
packages/app-desktop/gui/DropboxLoginScreen.d.ts
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js
|
||||
packages/app-desktop/gui/DropboxLoginScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
|
||||
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||
packages/app-desktop/gui/ErrorBoundary.js
|
||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||
@@ -916,9 +922,9 @@ packages/lib/commands/index.js.map
|
||||
packages/lib/commands/synchronize.d.ts
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/synchronize.js.map
|
||||
packages/lib/components/shared/encryption-config-shared.d.ts
|
||||
packages/lib/components/shared/encryption-config-shared.js
|
||||
packages/lib/components/shared/encryption-config-shared.js.map
|
||||
packages/lib/components/EncryptionConfigScreen/utils.d.ts
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js.map
|
||||
packages/lib/database.d.ts
|
||||
packages/lib/database.js
|
||||
packages/lib/database.js.map
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 297 B |
@@ -23,4 +23,6 @@ tests/support/dropbox-auth.txt
|
||||
tests/support/nextcloud-auth.json
|
||||
tests/support/onedrive-auth.txt
|
||||
build/
|
||||
patches/
|
||||
patches/
|
||||
createUsers-*.txt
|
||||
tools/temp/
|
||||
|
||||
@@ -89,7 +89,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
|
||||
flags = cliUtils.parseFlags(flags);
|
||||
|
||||
if (!flags.arg) {
|
||||
booleanFlags.push(flags.short);
|
||||
if (flags.short) booleanFlags.push(flags.short);
|
||||
if (flags.long) booleanFlags.push(flags.long);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
|
||||
|
||||
function randomElement(array: any[]): any {
|
||||
if (!array.length) return null;
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
function itemCount(args: any) {
|
||||
const count = Number(args.arg0);
|
||||
if (!count || isNaN(count)) throw new Error('Note count must be specified');
|
||||
return count;
|
||||
}
|
||||
|
||||
class Command extends BaseCommand {
|
||||
usage() {
|
||||
return 'testing <command> [arg0]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return 'testing';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
options(): any[] {
|
||||
return [
|
||||
['--folder-count <count>', 'Folders to create'],
|
||||
['--note-count <count>', 'Notes to create'],
|
||||
['--tag-count <count>', 'Tags to create'],
|
||||
['--tags-per-note <count>', 'Tags per note'],
|
||||
['--silent', 'Silent'],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args: any) {
|
||||
const { command, options } = args;
|
||||
|
||||
if (command === 'populate') {
|
||||
await populateDatabase(reg.db(), {
|
||||
folderCount: options['folder-count'],
|
||||
noteCount: options['note-count'],
|
||||
tagCount: options['tag-count'],
|
||||
tagsPerNote: options['tags-per-note'],
|
||||
silent: options['silent'],
|
||||
});
|
||||
}
|
||||
|
||||
const promises: any[] = [];
|
||||
|
||||
if (command === 'createRandomNotes') {
|
||||
const noteCount = itemCount(args);
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
promises.push(Note.save({
|
||||
title: `Note ${uuid.createNano()}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'updateRandomNotes') {
|
||||
const noteCount = itemCount(args);
|
||||
|
||||
const noteIds = await Note.allIds();
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
const noteId = randomElement(noteIds);
|
||||
promises.push(Note.save({
|
||||
id: noteId,
|
||||
title: `Note ${uuid.createNano()}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'deleteRandomNotes') {
|
||||
const noteCount = itemCount(args);
|
||||
const noteIds = await Note.allIds();
|
||||
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
const noteId = randomElement(noteIds);
|
||||
promises.push(Note.delete(noteId));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start the server with:
|
||||
#
|
||||
# JOPLIN_IS_TESTING=1 npm run start-dev
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# curl --data '{"action": "clearDatabase"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
# SMALL
|
||||
|
||||
# curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
NUM=398
|
||||
while [ "$NUM" -lt 400 ]; do
|
||||
NUM=$(( NUM + 1 ))
|
||||
|
||||
echo "User $NUM"
|
||||
|
||||
CMD_FILE="$SCRIPT_DIR/createUsers-$NUM.txt"
|
||||
PROFILE_DIR=~/.config/joplindev-testing-$NUM
|
||||
USER_EMAIL="user$NUM@example.com"
|
||||
|
||||
rm -rf "$CMD_FILE" "$PROFILE_DIR"
|
||||
touch "$CMD_FILE"
|
||||
|
||||
FLAG_FOLDER_COUNT=100
|
||||
FLAG_NOTE_COUNT=1000
|
||||
FLAG_TAG_COUNT=20
|
||||
|
||||
if [ "$NUM" -gt 300 ]; then
|
||||
FLAG_FOLDER_COUNT=2000
|
||||
FLAG_NOTE_COUNT=10000
|
||||
FLAG_TAG_COUNT=200
|
||||
fi
|
||||
|
||||
if [ "$NUM" -gt 399 ]; then
|
||||
FLAG_FOLDER_COUNT=10000
|
||||
FLAG_NOTE_COUNT=150000
|
||||
FLAG_TAG_COUNT=2000
|
||||
fi
|
||||
|
||||
echo "testing populate --silent --folder-count $FLAG_FOLDER_COUNT --note-count $FLAG_NOTE_COUNT --tag-count $FLAG_TAG_COUNT" >> "$CMD_FILE"
|
||||
echo "config keychain.supported 0" >> "$CMD_FILE"
|
||||
echo "config sync.target 10" >> "$CMD_FILE"
|
||||
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
|
||||
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
|
||||
echo "sync" >> "$CMD_FILE"
|
||||
|
||||
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
|
||||
done
|
||||
@@ -10,6 +10,7 @@
|
||||
"test-ci": "jest --config=jest.config.js --forceExit",
|
||||
"build": "gulp build",
|
||||
"start": "gulp build -L && node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
|
||||
"start-no-build": "node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
|
||||
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
|
||||
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// This script can be used to simulate a running production environment, by
|
||||
// having multiple users in parallel changing notes and synchronising.
|
||||
//
|
||||
// To get it working:
|
||||
//
|
||||
// - Run the Postgres database -- `sudo docker-compose --file docker-compose.db-dev.yml up`
|
||||
// - Update the DB parameters in ~/joplin-credentials/server.env to use the dev
|
||||
// database
|
||||
// - Run the server - `JOPLIN_IS_TESTING=1 npm run start-dev`
|
||||
// - Then run this script - `node populateDatabase.js`
|
||||
//
|
||||
// Currently it doesn't actually create the users, so that should be done using:
|
||||
//
|
||||
// curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
//
|
||||
// That will create n users with email `user<n>@example.com`
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import { homedir } from 'os';
|
||||
import { execCommand2 } from '@joplin/tools/tool-utils';
|
||||
import { chdir } from 'process';
|
||||
|
||||
const minUserNum = 1;
|
||||
const maxUserNum = 400;
|
||||
|
||||
const cliDir = `${__dirname}/..`;
|
||||
const tempDir = `${__dirname}/temp`;
|
||||
|
||||
function randomInt(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
const processing_: Record<number, boolean> = {};
|
||||
|
||||
const processUser = async (userNum: number) => {
|
||||
if (processing_[userNum]) {
|
||||
console.info(`User already being processed: ${userNum} - skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
processing_[userNum] = true;
|
||||
|
||||
try {
|
||||
const userEmail = `user${userNum}@example.com`;
|
||||
const userPassword = 'hunter1hunter2hunter3';
|
||||
const commandFile = `${tempDir}/populateDatabase-${userNum}.txt`;
|
||||
const profileDir = `${homedir()}/.config/joplindev-populate/joplindev-testing-${userNum}`;
|
||||
|
||||
const commands: string[] = [];
|
||||
const jackpot = Math.random() >= 0.95 ? 100 : 1;
|
||||
|
||||
commands.push(`testing createRandomNotes ${randomInt(1, 500 * jackpot)}`);
|
||||
commands.push(`testing updateRandomNotes ${randomInt(1, 1500 * jackpot)}`);
|
||||
commands.push(`testing deleteRandomNotes ${randomInt(1, 200 * jackpot)}`);
|
||||
commands.push('config keychain.supported 0');
|
||||
commands.push('config sync.target 10');
|
||||
commands.push(`config sync.10.username ${userEmail}`);
|
||||
commands.push(`config sync.10.password ${userPassword}`);
|
||||
commands.push('sync');
|
||||
|
||||
await fs.writeFile(commandFile, commands.join('\n'), 'utf8');
|
||||
|
||||
await chdir(cliDir);
|
||||
|
||||
await execCommand2(['npm', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
|
||||
} catch (error) {
|
||||
console.error(`Could not process user ${userNum}:`, error);
|
||||
} finally {
|
||||
delete processing_[userNum];
|
||||
}
|
||||
};
|
||||
|
||||
const waitForProcessing = (count: number) => {
|
||||
return new Promise((resolve) => {
|
||||
const iid = setInterval(() => {
|
||||
if (Object.keys(processing_).length <= count) {
|
||||
clearInterval(iid);
|
||||
resolve(null);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
await fs.mkdirp(tempDir);
|
||||
|
||||
// Build the app once before starting, because we'll use start-no-build to
|
||||
// run the scripts (faster)
|
||||
await execCommand2(['npm', 'run', 'build']);
|
||||
|
||||
const focusUserNum = 400;
|
||||
|
||||
while (true) {
|
||||
let userNum = randomInt(minUserNum, maxUserNum);
|
||||
|
||||
if (Math.random() >= .7) userNum = focusUserNum;
|
||||
|
||||
void processUser(userNum);
|
||||
await waitForProcessing(10);
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../services/bridge';
|
||||
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||
import control_PluginsStates from './controls/plugins/PluginsStates';
|
||||
import EncryptionConfigScreen from '../EncryptionConfigScreen';
|
||||
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import dialogs from './dialogs';
|
||||
import bridge from '../services/bridge';
|
||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import MasterKey from '@joplin/lib/models/MasterKey';
|
||||
import StyledInput from './style/StyledInput';
|
||||
import Button, { ButtonLevel } from './Button/Button';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const MasterPasswordInput = styled(StyledInput)`
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface Props {}
|
||||
|
||||
class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
shared.initialize(this, props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
shared.componentWillUnmount();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
shared.componentDidMount(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
shared.componentDidUpdate(this, prevProps);
|
||||
}
|
||||
|
||||
async checkPasswords() {
|
||||
return shared.checkPasswords(this);
|
||||
}
|
||||
|
||||
private renderMasterKey(mk: MasterKeyEntity, _isDefault: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onToggleEnabledClick = () => {
|
||||
return shared.onToggleEnabledClick(this, mk);
|
||||
};
|
||||
|
||||
const passwordStyle = {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const onSaveClick = () => {
|
||||
return shared.onSavePasswordClick(this, mk);
|
||||
};
|
||||
|
||||
const onPasswordChange = (event: any) => {
|
||||
return shared.onPasswordChange(this, mk, event.target.value);
|
||||
};
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
|
||||
({_('Master password')})
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||
const isActive = this.props.activeMasterKeyId === mk.id;
|
||||
const activeIcon = isActive ? '✔' : '';
|
||||
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
return (
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{activeIcon}</td>
|
||||
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
{renderPasswordInput(mk.id)}
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
renderNeedUpgradeSection() {
|
||||
if (!shim.isElectron()) return null;
|
||||
|
||||
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
|
||||
if (!needUpgradeMasterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const rows = [];
|
||||
const comp = this;
|
||||
|
||||
for (const mk of needUpgradeMasterKeys) {
|
||||
rows.push(
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{mk.id}</td>
|
||||
<td><button onClick={() => shared.upgradeMasterKey(comp, mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
|
||||
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Upgrade')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderReencryptData() {
|
||||
if (!shim.isElectron()) return null;
|
||||
if (!this.props.shouldReencrypt) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const buttonLabel = _('Re-encrypt data');
|
||||
|
||||
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||
|
||||
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
|
||||
|
||||
t = t.replace(/\n\n/g, '</p><p>');
|
||||
t = t.replace(/\n/g, '<br>');
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button onClick={() => shared.reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||
</span>
|
||||
|
||||
{ !this.props.shouldReencrypt ? null : <button onClick={() => shared.dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMasterKeySection(masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const mkComps = [];
|
||||
const showTable = isEnabledMasterKeys || this.state.showDisabledMasterKeys;
|
||||
const latestMasterKey = MasterKey.latest();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(mk, isEnabledMasterKeys && latestMasterKey && mk.id === latestMasterKey.id));
|
||||
}
|
||||
|
||||
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
|
||||
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
|
||||
const tableComp = !showTable ? null : (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('Active')}</th>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Date')}</th>
|
||||
<th style={theme.textStyle}>{_('Password')}</th>
|
||||
<th style={theme.textStyle}>{_('Valid')}</th>
|
||||
<th style={theme.textStyle}>{_('Actions')}</th>
|
||||
</tr>
|
||||
{mkComps}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
if (mkComps.length) {
|
||||
return (
|
||||
<div>
|
||||
{headerComp}
|
||||
{tableComp}
|
||||
{infoComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private renderMasterPassword() {
|
||||
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onMasterPasswordSave = async () => {
|
||||
shared.onMasterPasswordSave(this);
|
||||
|
||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<span style={theme.textStyle}>{_('Master password:')}</span>
|
||||
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>✔ {_('Loaded')}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={theme.textStyle}>❌ {'The master password is not set or is invalid. Please type it below:'}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
|
||||
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
padding: theme.configScreenPadding,
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
const isEnabled = getEncryptionEnabled();
|
||||
let masterKey = getDefaultMasterKey();
|
||||
|
||||
// If the user has explicitly disabled the master key, we generate a
|
||||
// new one. Needed for one the password has been forgotten.
|
||||
if (!masterKey.enabled) masterKey = null;
|
||||
|
||||
let answer = null;
|
||||
if (isEnabled) {
|
||||
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
} else {
|
||||
const msg = shared.enableEncryptionConfirmationMessages(masterKey);
|
||||
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
||||
}
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
try {
|
||||
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
|
||||
} catch (error) {
|
||||
await dialogs.alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
|
||||
const toggleButton = (
|
||||
<button
|
||||
style={theme.buttonStyle}
|
||||
onClick={() => {
|
||||
void onToggleButtonClick();
|
||||
}}
|
||||
>
|
||||
{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const needUpgradeSection = this.renderNeedUpgradeSection();
|
||||
const reencryptDataSection = this.renderReencryptData();
|
||||
|
||||
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
|
||||
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<tr key={id}>
|
||||
<td style={theme.textStyle}>{id}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={containerStyle}>
|
||||
{
|
||||
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||
<p style={theme.textStyle}>
|
||||
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
bridge().openExternal('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
href="#"
|
||||
style={theme.urlStyle}
|
||||
>
|
||||
https://joplinapp.org/e2ee/
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||
<p style={theme.textStyle}>
|
||||
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
||||
</p>
|
||||
{this.renderMasterPassword()}
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{needUpgradeSection}
|
||||
{this.props.shouldReencrypt ? reencryptDataSection : null}
|
||||
{enabledMasterKeySection}
|
||||
{disabledMasterKeySection}
|
||||
{nonExistingMasterKeySection}
|
||||
{!this.props.shouldReencrypt ? reencryptDataSection : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State) => {
|
||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
masterKeys: syncInfo.masterKeys,
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: syncInfo.e2ee,
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
};
|
||||
|
||||
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
|
||||
|
||||
export default EncryptionConfigScreen;
|
||||
@@ -0,0 +1,366 @@
|
||||
const React = require('react');
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import dialogs from '../dialogs';
|
||||
import bridge from '../../services/bridge';
|
||||
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import StyledInput from '../style/StyledInput';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import styled from 'styled-components';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const MasterPasswordInput = styled(StyledInput)`
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: any;
|
||||
masterKeys: MasterKeyEntity[];
|
||||
passwords: Record<string, string>;
|
||||
notLoadedMasterKeys: string[];
|
||||
encryptionEnabled: boolean;
|
||||
shouldReencrypt: boolean;
|
||||
activeMasterKeyId: string;
|
||||
masterPassword: string;
|
||||
}
|
||||
|
||||
const EncryptionConfigScreen = (props: Props) => {
|
||||
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||
|
||||
const theme: any = useMemo(() => {
|
||||
return themeStyle(props.themeId);
|
||||
}, [props.themeId]);
|
||||
|
||||
const stats = useStats();
|
||||
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
|
||||
|
||||
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
|
||||
void upgradeMasterKey(mk, passwordChecks, props.passwords);
|
||||
}, [passwordChecks, props.passwords]);
|
||||
|
||||
const renderNeedUpgradeSection = () => {
|
||||
if (!shim.isElectron()) return null;
|
||||
|
||||
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(props.masterKeys);
|
||||
if (!needUpgradeMasterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const rows = [];
|
||||
|
||||
for (const mk of needUpgradeMasterKeys) {
|
||||
rows.push(
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{mk.id}</td>
|
||||
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
|
||||
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Upgrade')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderReencryptData = () => {
|
||||
if (!shim.isElectron()) return null;
|
||||
if (!props.shouldReencrypt) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const buttonLabel = _('Re-encrypt data');
|
||||
|
||||
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||
|
||||
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
|
||||
|
||||
t = t.replace(/\n\n/g, '</p><p>');
|
||||
t = t.replace(/\n/g, '<br>');
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||
</span>
|
||||
|
||||
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMasterKey = (mk: MasterKeyEntity) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const passwordStyle = {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||
const isActive = props.activeMasterKeyId === mk.id;
|
||||
const activeIcon = isActive ? '✔' : '';
|
||||
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
|
||||
return (
|
||||
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
|
||||
({_('Master password')})
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onInputPasswordChange(mk, event.target.value)} />{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, props.passwords)}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{activeIcon}</td>
|
||||
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
{renderPasswordInput(mk.id)}
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMasterKeySection = (masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const mkComps = [];
|
||||
const showTable = isEnabledMasterKeys || showDisabledMasterKeys;
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(renderMasterKey(mk));
|
||||
}
|
||||
|
||||
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
|
||||
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
|
||||
const tableComp = !showTable ? null : (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('Active')}</th>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
<th style={theme.textStyle}>{_('Date')}</th>
|
||||
<th style={theme.textStyle}>{_('Password')}</th>
|
||||
<th style={theme.textStyle}>{_('Valid')}</th>
|
||||
<th style={theme.textStyle}>{_('Actions')}</th>
|
||||
</tr>
|
||||
{mkComps}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
if (mkComps.length) {
|
||||
return (
|
||||
<div>
|
||||
{headerComp}
|
||||
{tableComp}
|
||||
{infoComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||
|
||||
const renderMasterPassword = () => {
|
||||
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
if (passwordChecks['master']) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<span style={theme.textStyle}>{_('Master password:')}</span>
|
||||
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>✔ {_('Loaded')}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={theme.textStyle}>❌ {'The master password is not set or is invalid. Please type it below:'}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={inputMasterPassword} onChange={(event: any) => onMasterPasswordChange(event.target.value)} />{' '}
|
||||
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
padding: theme.configScreenPadding,
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < props.masterKeys.length; i++) {
|
||||
const mk = props.masterKeys[i];
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
const isEnabled = getEncryptionEnabled();
|
||||
const masterKey = getDefaultMasterKey();
|
||||
|
||||
let answer = null;
|
||||
if (isEnabled) {
|
||||
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
} else {
|
||||
const msg = enableEncryptionConfirmationMessages(masterKey);
|
||||
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
||||
}
|
||||
|
||||
if (!answer) return;
|
||||
|
||||
try {
|
||||
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
|
||||
} catch (error) {
|
||||
await dialogs.alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const decryptedItemsInfo = <p style={theme.textStyle}>{decryptedStatText(stats)}</p>;
|
||||
const toggleButton = (
|
||||
<button
|
||||
style={theme.buttonStyle}
|
||||
onClick={() => {
|
||||
void onToggleButtonClick();
|
||||
}}
|
||||
>
|
||||
{props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const needUpgradeSection = renderNeedUpgradeSection();
|
||||
const reencryptDataSection = renderReencryptData();
|
||||
|
||||
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
|
||||
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<tr key={id}>
|
||||
<td style={theme.textStyle}>{id}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={containerStyle}>
|
||||
{
|
||||
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||
<p style={theme.textStyle}>
|
||||
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
bridge().openExternal('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
href="#"
|
||||
style={theme.urlStyle}
|
||||
>
|
||||
https://joplinapp.org/e2ee/
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||
<p style={theme.textStyle}>
|
||||
{_('Encryption is:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
||||
</p>
|
||||
{renderMasterPassword()}
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{needUpgradeSection}
|
||||
{props.shouldReencrypt ? reencryptDataSection : null}
|
||||
{enabledMasterKeySection}
|
||||
{disabledMasterKeySection}
|
||||
{nonExistingMasterKeySection}
|
||||
{!props.shouldReencrypt ? reencryptDataSection : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
masterKeys: syncInfo.masterKeys,
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: syncInfo.e2ee,
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EncryptionConfigScreen);
|
||||
@@ -0,0 +1,5 @@
|
||||
.encryption-config-test {
|
||||
& > .item {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
const gulp = require('gulp');
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
const compileSass = require('@joplin/tools/compileSass');
|
||||
|
||||
const tasks = {
|
||||
compileScripts: {
|
||||
@@ -20,6 +21,14 @@ const tasks = {
|
||||
tsc: require('@joplin/tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
|
||||
compileSass: {
|
||||
fn: async () => {
|
||||
const guiDir = `${__dirname}/gui`;
|
||||
await compileSass([
|
||||
`${guiDir}/EncryptionConfigScreen/style.scss`,
|
||||
], `${__dirname}/style.min.css`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
@@ -31,6 +40,7 @@ const buildParallel = [
|
||||
'copyTinyMceLangs',
|
||||
'updateIgnoredTypeScriptBuild',
|
||||
'buildCommandIndex',
|
||||
'compileSass',
|
||||
];
|
||||
|
||||
gulp.task('build', gulp.parallel(...buildParallel));
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
.encryption-config-test > .item {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.min.css.map */
|
||||
@@ -2,68 +2,56 @@ const React = require('react');
|
||||
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../screen-header.js');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
||||
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
interface Props {}
|
||||
interface Props {
|
||||
themeId: any;
|
||||
masterKeys: MasterKeyEntity[];
|
||||
passwords: Record<string, string>;
|
||||
notLoadedMasterKeys: string[];
|
||||
encryptionEnabled: boolean;
|
||||
shouldReencrypt: boolean;
|
||||
activeMasterKeyId: string;
|
||||
masterPassword: string;
|
||||
}
|
||||
|
||||
class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
static navigationOptions(): any {
|
||||
return { header: null };
|
||||
}
|
||||
const EncryptionConfigScreen = (props: Props) => {
|
||||
const [passwordPromptShow, setPasswordPromptShow] = useState(false);
|
||||
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
|
||||
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
|
||||
const stats = useStats();
|
||||
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
|
||||
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
|
||||
const dialogBoxRef = useRef(null);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const mkComps = [];
|
||||
|
||||
this.state = {
|
||||
passwordPromptShow: false,
|
||||
passwordPromptAnswer: '',
|
||||
passwordPromptConfirmAnswer: '',
|
||||
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
|
||||
|
||||
const theme: any = useMemo(() => {
|
||||
return themeStyle(props.themeId);
|
||||
}, [props.themeId]);
|
||||
|
||||
const rootStyle = useMemo(() => {
|
||||
return {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
shared.initialize(this, props);
|
||||
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
async refreshStats() {
|
||||
return shared.refreshStats(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
shared.componentDidMount(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
shared.componentDidUpdate(this, prevProps);
|
||||
}
|
||||
|
||||
async checkPasswords() {
|
||||
return shared.checkPasswords(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles = useMemo(() => {
|
||||
const styles = {
|
||||
titleText: {
|
||||
flex: 1,
|
||||
@@ -93,39 +81,32 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
return StyleSheet.create(styles);
|
||||
}, [theme]);
|
||||
|
||||
renderMasterKey(_num: number, mk: MasterKeyEntity) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
|
||||
|
||||
const onSaveClick = () => {
|
||||
return shared.onSavePasswordClick(this, mk);
|
||||
};
|
||||
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const onPasswordChange = (text: string) => {
|
||||
return shared.onPasswordChange(this, mk, text);
|
||||
};
|
||||
|
||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.dividerColor;
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
||||
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
|
||||
return (
|
||||
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
||||
<Text style={{ ...styles.normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput>
|
||||
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
||||
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
|
||||
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, props.passwords)}></Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -133,69 +114,65 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
|
||||
return (
|
||||
<View key={mk.id}>
|
||||
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<Text style={styles.titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
|
||||
{renderPasswordInput(mk.id)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
passwordPromptComponent() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
let masterKey = getDefaultMasterKey();
|
||||
|
||||
// If the user has explicitly disabled the master key, we generate a
|
||||
// new one. Needed for one the password has been forgotten.
|
||||
if (!masterKey.enabled) masterKey = null;
|
||||
const renderPasswordPrompt = () => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const masterKey = getDefaultMasterKey();
|
||||
|
||||
const onEnableClick = async () => {
|
||||
try {
|
||||
const password = this.state.passwordPromptAnswer;
|
||||
const password = passwordPromptAnswer;
|
||||
if (!password) throw new Error(_('Password cannot be empty'));
|
||||
const password2 = this.state.passwordPromptConfirmAnswer;
|
||||
const password2 = passwordPromptConfirmAnswer;
|
||||
if (!password2) throw new Error(_('Confirm password cannot be empty'));
|
||||
if (password !== password2) throw new Error(_('Passwords do not match!'));
|
||||
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
|
||||
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
|
||||
this.setState({ passwordPromptShow: false });
|
||||
setPasswordPromptShow(false);
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const messages = shared.enableEncryptionConfirmationMessages(masterKey);
|
||||
const messages = enableEncryptionConfirmationMessages(masterKey);
|
||||
|
||||
const messageComps = messages.map(msg => {
|
||||
const messageComps = messages.map((msg: string) => {
|
||||
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
|
||||
<View>{messageComps}</View>
|
||||
<Text style={this.styles().normalText}>{_('Password:')}</Text>
|
||||
<Text style={styles.normalText}>{_('Password:')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
style={this.styles().normalTextInput}
|
||||
style={styles.normalTextInput}
|
||||
secureTextEntry={true}
|
||||
value={this.state.passwordPromptAnswer}
|
||||
value={passwordPromptAnswer}
|
||||
onChangeText={(text: string) => {
|
||||
this.setState({ passwordPromptAnswer: text });
|
||||
setPasswordPromptAnswer(text);
|
||||
}}
|
||||
></TextInput>
|
||||
|
||||
<Text style={this.styles().normalText}>{_('Confirm password:')}</Text>
|
||||
<Text style={styles.normalText}>{_('Confirm password:')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
style={this.styles().normalTextInput}
|
||||
style={styles.normalTextInput}
|
||||
secureTextEntry={true}
|
||||
value={this.state.passwordPromptConfirmAnswer}
|
||||
value={passwordPromptConfirmAnswer}
|
||||
onChangeText={(text: string) => {
|
||||
this.setState({ passwordPromptConfirmAnswer: text });
|
||||
setPasswordPromptConfirmAnswer(text);
|
||||
}}
|
||||
></TextInput>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
@@ -211,156 +188,132 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
<Button
|
||||
title={_('Cancel')}
|
||||
onPress={() => {
|
||||
this.setState({ passwordPromptShow: false });
|
||||
setPasswordPromptShow(false);
|
||||
}}
|
||||
></Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private renderMasterPassword() {
|
||||
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onMasterPasswordSave = async () => {
|
||||
shared.onMasterPasswordSave(this);
|
||||
|
||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
};
|
||||
const renderMasterPassword = () => {
|
||||
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
|
||||
|
||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.dividerColor;
|
||||
|
||||
if (this.state.passwordChecks['master']) {
|
||||
if (passwordChecks['master']) {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
|
||||
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
|
||||
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<Button onPress={onMasterPasswordSave} title={_('Save')} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
for (let i = 0; i < props.masterKeys.length; i++) {
|
||||
const mk = props.masterKeys[i];
|
||||
mkComps.push(renderMasterKey(i + 1, mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys = this.props.masterKeys;
|
||||
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
|
||||
const onToggleButtonClick = async () => {
|
||||
if (props.encryptionEnabled) {
|
||||
const ok = await dialogs.confirmRef(dialogBoxRef.current, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
if (!ok) return;
|
||||
|
||||
const mkComps = [];
|
||||
|
||||
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(i + 1, mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
try {
|
||||
await setupAndDisableEncryption(EncryptionService.instance());
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
} else {
|
||||
setPasswordPromptShow(true);
|
||||
setPasswordPromptAnswer('');
|
||||
setPasswordPromptConfirmAnswer('');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
if (this.props.encryptionEnabled) {
|
||||
const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
if (!ok) return;
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
try {
|
||||
await setupAndDisableEncryption(EncryptionService.instance());
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
passwordPromptShow: true,
|
||||
passwordPromptAnswer: '',
|
||||
passwordPromptConfirmAnswer: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<Text style={this.styles().normalText} key={id}>
|
||||
{id}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<View>
|
||||
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
||||
<View style={{ marginTop: 10 }}>{rows}</View>
|
||||
</View>
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<Text style={styles.normalText} key={id}>
|
||||
{id}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
|
||||
const toggleButton = !this.state.passwordPromptShow ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader title={_('Encryption Config')} />
|
||||
<ScrollView style={this.styles().container}>
|
||||
{
|
||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
>
|
||||
<Text>https://joplinapp.org/e2ee/</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
|
||||
<Text style={this.styles().titleText}>{_('Status')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
{decryptedItemsInfo}
|
||||
{this.renderMasterPassword()}
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
{nonExistingMasterKeySection}
|
||||
<View style={{ flex: 1, height: 20 }}></View>
|
||||
</ScrollView>
|
||||
<DialogBox
|
||||
ref={(dialogbox: any) => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
nonExistingMasterKeySection = (
|
||||
<View>
|
||||
<Text style={styles.titleText}>{_('Missing Master Keys')}</Text>
|
||||
<Text style={styles.normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
||||
<View style={{ marginTop: 10 }}>{rows}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const EncryptionConfigScreen = connect((state: State) => {
|
||||
const passwordPromptComp = passwordPromptShow ? renderPasswordPrompt() : null;
|
||||
const toggleButton = !passwordPromptShow ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Button title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={_('Encryption Config')} />
|
||||
<ScrollView style={styles.container}>
|
||||
{
|
||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
>
|
||||
<Text>https://joplinapp.org/e2ee/</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
|
||||
<Text style={styles.titleText}>{_('Status')}</Text>
|
||||
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
{decryptedItemsInfo}
|
||||
{renderMasterPassword()}
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
{nonExistingMasterKeySection}
|
||||
<View style={{ flex: 1, height: 20 }}></View>
|
||||
</ScrollView>
|
||||
<DialogBox ref={dialogBoxRef}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: State) => {
|
||||
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
|
||||
|
||||
return {
|
||||
@@ -372,6 +325,4 @@ const EncryptionConfigScreen = connect((state: State) => {
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
})(EncryptionConfigScreenComponent);
|
||||
|
||||
export default EncryptionConfigScreen;
|
||||
})(EncryptionConfigScreen);
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"postinstall": "jetify && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^2.2.0",
|
||||
"@joplin/renderer": "^2.2.0",
|
||||
"@joplin/lib": "~2.4",
|
||||
"@joplin/renderer": "~2.4",
|
||||
"@react-native-community/clipboard": "^1.5.0",
|
||||
"@react-native-community/datetimepicker": "^3.0.3",
|
||||
"@react-native-community/geolocation": "^2.0.2",
|
||||
@@ -69,7 +69,7 @@
|
||||
"@codemirror/lang-markdown": "^0.18.4",
|
||||
"@codemirror/state": "^0.18.7",
|
||||
"@codemirror/view": "^0.18.19",
|
||||
"@joplin/tools": "^1.0.9",
|
||||
"@joplin/tools": "~2.4",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/node": "^14.14.6",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
module.exports = `LyoKCkF0b20gT25lIERhcmsgV2l0aCBzdXBwb3J0IGZvciBSZWFzb25NTCBieSBHaWRpIE1vcnJpcywgYmFzZWQgb2ZmIHdvcmsgYnkgRGFuaWVsIEdhbWFnZQoKT3JpZ2luYWwgT25lIERhcmsgU3ludGF4IHRoZW1lIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2F0b20vb25lLWRhcmstc3ludGF4CgoqLwouaGxqcyB7CiAgZGlzcGxheTogYmxvY2s7CiAgb3ZlcmZsb3cteDogYXV0bzsKICBwYWRkaW5nOiAwLjVlbTsKICBjb2xvcjogI2FiYjJiZjsKICBiYWNrZ3JvdW5kOiAjMjgyYzM0Owp9Ci5obGpzLWtleXdvcmQsIC5obGpzLW9wZXJhdG9yIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIC5obGpzLWNvbnN0cnVjdG9yIHsKICBjb2xvcjogIzYxYWVlZTsKfQouaGxqcy1mdW5jdGlvbiB7CiAgY29sb3I6ICM2MWFlZWU7Cn0KLmhsanMtZnVuY3Rpb24gLmhsanMtcGFyYW1zIHsKICBjb2xvcjogI0E2RTIyRTsKfQouaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5nIHsKICBjb2xvcjogI0ZEOTcxRjsKfQouaGxqcy1tb2R1bGUtYWNjZXNzIC5obGpzLW1vZHVsZSB7CiAgY29sb3I6ICM3ZTU3YzI7Cn0KLmhsanMtY29uc3RydWN0b3IgewogIGNvbG9yOiAjZTJiOTNkOwp9Ci5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZyB7CiAgY29sb3I6ICM5Q0NDNjU7Cn0KLmhsanMtY29tbWVudCwgLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYjE4ZWIxOwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQouaGxqcy1kb2N0YWcsIC5obGpzLWZvcm11bGEgewogIGNvbG9yOiAjYzY3OGRkOwp9Ci5obGpzLXNlY3Rpb24sIC5obGpzLW5hbWUsIC5obGpzLXNlbGVjdG9yLXRhZywgLmhsanMtZGVsZXRpb24sIC5obGpzLXN1YnN0IHsKICBjb2xvcjogI2UwNmM3NTsKfQouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzU2YjZjMjsKfQouaGxqcy1zdHJpbmcsIC5obGpzLXJlZ2V4cCwgLmhsanMtYWRkaXRpb24sIC5obGpzLWF0dHJpYnV0ZSwgLmhsanMtbWV0YS1zdHJpbmcgewogIGNvbG9yOiAjOThjMzc5Owp9Ci5obGpzLWJ1aWx0X2luLCAuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSB7CiAgY29sb3I6ICNlNmMwN2I7Cn0KLmhsanMtYXR0ciwgLmhsanMtdmFyaWFibGUsIC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLCAuaGxqcy10eXBlLCAuaGxqcy1zZWxlY3Rvci1jbGFzcywgLmhsanMtc2VsZWN0b3ItYXR0ciwgLmhsanMtc2VsZWN0b3ItcHNldWRvLCAuaGxqcy1udW1iZXIgewogIGNvbG9yOiAjZDE5YTY2Owp9Ci5obGpzLXN5bWJvbCwgLmhsanMtYnVsbGV0LCAuaGxqcy1saW5rLCAuaGxqcy1tZXRhLCAuaGxqcy1zZWxlY3Rvci1pZCwgLmhsanMtdGl0bGUgewogIGNvbG9yOiAjNjFhZWVlOwp9Ci5obGpzLWVtcGhhc2lzIHsKICBmb250LXN0eWxlOiBpdGFsaWM7Cn0KLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQouaGxqcy1saW5rIHsKICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsKfQo=`;
|
||||
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiNhYmIyYmY7YmFja2dyb3VuZDojMjgyYzM0fS5obGpzLWtleXdvcmQsLmhsanMtb3BlcmF0b3IsLmhsanMtcGF0dGVybi1tYXRjaHtjb2xvcjojZjkyNjcyfS5obGpzLWZ1bmN0aW9uLC5obGpzLXBhdHRlcm4tbWF0Y2ggLmhsanMtY29uc3RydWN0b3J7Y29sb3I6IzYxYWVlZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXN7Y29sb3I6I2E2ZTIyZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5ne2NvbG9yOiNmZDk3MWZ9LmhsanMtbW9kdWxlLWFjY2VzcyAuaGxqcy1tb2R1bGV7Y29sb3I6IzdlNTdjMn0uaGxqcy1jb25zdHJ1Y3Rvcntjb2xvcjojZTJiOTNkfS5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZ3tjb2xvcjojOWNjYzY1fS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2IxOGViMTtmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYXtjb2xvcjojYzY3OGRkfS5obGpzLWRlbGV0aW9uLC5obGpzLW5hbWUsLmhsanMtc2VjdGlvbiwuaGxqcy1zZWxlY3Rvci10YWcsLmhsanMtc3Vic3R7Y29sb3I6I2UwNmM3NX0uaGxqcy1saXRlcmFse2NvbG9yOiM1NmI2YzJ9LmhsanMtYWRkaXRpb24sLmhsanMtYXR0cmlidXRlLC5obGpzLW1ldGEgLmhsanMtc3RyaW5nLC5obGpzLXJlZ2V4cCwuaGxqcy1zdHJpbmd7Y29sb3I6Izk4YzM3OX0uaGxqcy1idWlsdF9pbiwuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSwuaGxqcy10aXRsZS5jbGFzc197Y29sb3I6I2U2YzA3Yn0uaGxqcy1hdHRyLC5obGpzLW51bWJlciwuaGxqcy1zZWxlY3Rvci1hdHRyLC5obGpzLXNlbGVjdG9yLWNsYXNzLC5obGpzLXNlbGVjdG9yLXBzZXVkbywuaGxqcy10ZW1wbGF0ZS12YXJpYWJsZSwuaGxqcy10eXBlLC5obGpzLXZhcmlhYmxle2NvbG9yOiNkMTlhNjZ9LmhsanMtYnVsbGV0LC5obGpzLWxpbmssLmhsanMtbWV0YSwuaGxqcy1zZWxlY3Rvci1pZCwuaGxqcy1zeW1ib2wsLmhsanMtdGl0bGV7Y29sb3I6IzYxYWVlZX0uaGxqcy1lbXBoYXNpc3tmb250LXN0eWxlOml0YWxpY30uaGxqcy1zdHJvbmd7Zm9udC13ZWlnaHQ6NzAwfS5obGpzLWxpbmt7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZX0=`;
|
||||
@@ -1 +1 @@
|
||||
module.exports = `LyoKCkF0b20gT25lIExpZ2h0IGJ5IERhbmllbCBHYW1hZ2UKT3JpZ2luYWwgT25lIExpZ2h0IFN5bnRheCB0aGVtZSBmcm9tIGh0dHBzOi8vZ2l0aHViLmNvbS9hdG9tL29uZS1saWdodC1zeW50YXgKCmJhc2U6ICAgICNmYWZhZmEKbW9uby0xOiAgIzM4M2E0Mgptb25vLTI6ICAjNjg2Yjc3Cm1vbm8tMzogICNhMGExYTcKaHVlLTE6ICAgIzAxODRiYgpodWUtMjogICAjNDA3OGYyCmh1ZS0zOiAgICNhNjI2YTQKaHVlLTQ6ICAgIzUwYTE0ZgpodWUtNTogICAjZTQ1NjQ5Cmh1ZS01LTI6ICNjOTEyNDMKaHVlLTY6ICAgIzk4NjgwMQpodWUtNi0yOiAjYzE4NDAxCgoqLwoKLmhsanMgewogIGRpc3BsYXk6IGJsb2NrOwogIG92ZXJmbG93LXg6IGF1dG87CiAgcGFkZGluZzogMC41ZW07CiAgY29sb3I6ICMzODNhNDI7CiAgYmFja2dyb3VuZDogI2ZhZmFmYTsKfQoKLmhsanMtY29tbWVudCwKLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYTBhMWE3OwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtZG9jdGFnLAouaGxqcy1rZXl3b3JkLAouaGxqcy1mb3JtdWxhIHsKICBjb2xvcjogI2E2MjZhNDsKfQoKLmhsanMtc2VjdGlvbiwKLmhsanMtbmFtZSwKLmhsanMtc2VsZWN0b3ItdGFnLAouaGxqcy1kZWxldGlvbiwKLmhsanMtc3Vic3QgewogIGNvbG9yOiAjZTQ1NjQ5Owp9CgouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzAxODRiYjsKfQoKLmhsanMtc3RyaW5nLAouaGxqcy1yZWdleHAsCi5obGpzLWFkZGl0aW9uLAouaGxqcy1hdHRyaWJ1dGUsCi5obGpzLW1ldGEtc3RyaW5nIHsKICBjb2xvcjogIzUwYTE0ZjsKfQoKLmhsanMtYnVpbHRfaW4sCi5obGpzLWNsYXNzIC5obGpzLXRpdGxlIHsKICBjb2xvcjogI2MxODQwMTsKfQoKLmhsanMtYXR0ciwKLmhsanMtdmFyaWFibGUsCi5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLAouaGxqcy10eXBlLAouaGxqcy1zZWxlY3Rvci1jbGFzcywKLmhsanMtc2VsZWN0b3ItYXR0ciwKLmhsanMtc2VsZWN0b3ItcHNldWRvLAouaGxqcy1udW1iZXIgewogIGNvbG9yOiAjOTg2ODAxOwp9CgouaGxqcy1zeW1ib2wsCi5obGpzLWJ1bGxldCwKLmhsanMtbGluaywKLmhsanMtbWV0YSwKLmhsanMtc2VsZWN0b3ItaWQsCi5obGpzLXRpdGxlIHsKICBjb2xvcjogIzQwNzhmMjsKfQoKLmhsanMtZW1waGFzaXMgewogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQoKLmhsanMtbGluayB7CiAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7Cn0K`;
|
||||
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiMzODNhNDI7YmFja2dyb3VuZDojZmFmYWZhfS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2EwYTFhNztmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYSwuaGxqcy1rZXl3b3Jke2NvbG9yOiNhNjI2YTR9LmhsanMtZGVsZXRpb24sLmhsanMtbmFtZSwuaGxqcy1zZWN0aW9uLC5obGpzLXNlbGVjdG9yLXRhZywuaGxqcy1zdWJzdHtjb2xvcjojZTQ1NjQ5fS5obGpzLWxpdGVyYWx7Y29sb3I6IzAxODRiYn0uaGxqcy1hZGRpdGlvbiwuaGxqcy1hdHRyaWJ1dGUsLmhsanMtbWV0YSAuaGxqcy1zdHJpbmcsLmhsanMtcmVnZXhwLC5obGpzLXN0cmluZ3tjb2xvcjojNTBhMTRmfS5obGpzLWF0dHIsLmhsanMtbnVtYmVyLC5obGpzLXNlbGVjdG9yLWF0dHIsLmhsanMtc2VsZWN0b3ItY2xhc3MsLmhsanMtc2VsZWN0b3ItcHNldWRvLC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLC5obGpzLXR5cGUsLmhsanMtdmFyaWFibGV7Y29sb3I6Izk4NjgwMX0uaGxqcy1idWxsZXQsLmhsanMtbGluaywuaGxqcy1tZXRhLC5obGpzLXNlbGVjdG9yLWlkLC5obGpzLXN5bWJvbCwuaGxqcy10aXRsZXtjb2xvcjojNDA3OGYyfS5obGpzLWJ1aWx0X2luLC5obGpzLWNsYXNzIC5obGpzLXRpdGxlLC5obGpzLXRpdGxlLmNsYXNzX3tjb2xvcjojYzE4NDAxfS5obGpzLWVtcGhhc2lze2ZvbnQtc3R5bGU6aXRhbGljfS5obGpzLXN0cm9uZ3tmb250LXdlaWdodDo3MDB9LmhsanMtbGlua3t0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lfQ==`;
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hash:"6608023b8053b48e0eec248644475e33", files: {
|
||||
hash:"de3871f000c87478973d7cd0913bd3ff", files: {
|
||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,14 +7,13 @@ const { Keyboard } = require('react-native');
|
||||
|
||||
const dialogs = {};
|
||||
|
||||
dialogs.confirm = (parentComponent, message) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
dialogs.confirmRef = (ref, message) => {
|
||||
if (!ref) throw new Error('ref is required');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
parentComponent.dialogbox.confirm({
|
||||
ref.confirm({
|
||||
content: message,
|
||||
|
||||
ok: {
|
||||
@@ -32,6 +31,13 @@ dialogs.confirm = (parentComponent, message) => {
|
||||
});
|
||||
};
|
||||
|
||||
dialogs.confirm = (parentComponent, message) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
return dialogs.confirmRef(parentComponent.dialogBox, message);
|
||||
};
|
||||
|
||||
dialogs.pop = (parentComponent, message, buttons, options = null) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import shim from '../../shim';
|
||||
import { _ } from '../../locale';
|
||||
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import time from '../../time';
|
||||
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||
import EncryptionService from '../../services/e2ee/EncryptionService';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { reg } from '../../registry';
|
||||
import Setting from '../../models/Setting';
|
||||
const { useCallback, useEffect, useState } = shim.react();
|
||||
|
||||
type PasswordChecks = Record<string, boolean>;
|
||||
|
||||
export const useStats = () => {
|
||||
const [stats, setStats] = useState<EncryptedItemsStats>({ encrypted: null, total: null });
|
||||
const [statsUpdateTime, setStatsUpdateTime] = useState<number>(0);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const r = await BaseItem.encryptedItemsStats();
|
||||
if (event.cancelled) return;
|
||||
setStats(r);
|
||||
}, [statsUpdateTime]);
|
||||
|
||||
useEffect(() => {
|
||||
const iid = shim.setInterval(() => {
|
||||
setStatsUpdateTime(Date.now());
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(iid);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export const decryptedStatText = (stats: EncryptedItemsStats) => {
|
||||
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
||||
const totalCount = stats.total !== null ? stats.total : '-';
|
||||
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const enableEncryptionConfirmationMessages = (masterKey: MasterKeyEntity) => {
|
||||
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
|
||||
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
|
||||
return msg;
|
||||
};
|
||||
|
||||
const masterPasswordIsValid = async (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string = null) => {
|
||||
const activeMasterKey = masterKeys.find((mk: MasterKeyEntity) => mk.id === activeMasterKeyId);
|
||||
masterPassword = masterPassword === null ? masterPassword : masterPassword;
|
||||
if (activeMasterKey && masterPassword) {
|
||||
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const reencryptData = async () => {
|
||||
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
|
||||
if (!ok) return;
|
||||
|
||||
await BaseItem.forceSyncAll();
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
alert(_('Your data is going to be re-encrypted and synced again.'));
|
||||
};
|
||||
|
||||
export const dontReencryptData = () => {
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
};
|
||||
|
||||
export const useToggleShowDisabledMasterKeys = () => {
|
||||
const [showDisabledMasterKeys, setShowDisabledMasterKeys] = useState<boolean>(false);
|
||||
|
||||
const toggleShowDisabledMasterKeys = () => {
|
||||
setShowDisabledMasterKeys((current) => !current);
|
||||
};
|
||||
|
||||
return { showDisabledMasterKeys, toggleShowDisabledMasterKeys };
|
||||
};
|
||||
|
||||
export const onToggleEnabledClick = (mk: MasterKeyEntity) => {
|
||||
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
|
||||
};
|
||||
|
||||
export const onSavePasswordClick = (mk: MasterKeyEntity, passwords: Record<string, string>) => {
|
||||
const password = passwords[mk.id];
|
||||
if (!password) {
|
||||
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
|
||||
} else {
|
||||
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
|
||||
}
|
||||
};
|
||||
|
||||
export const onMasterPasswordSave = (masterPasswordInput: string) => {
|
||||
Setting.setValue('encryption.masterPassword', masterPasswordInput);
|
||||
};
|
||||
|
||||
export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string) => {
|
||||
const [inputMasterPassword, setInputMasterPassword] = useState<string>('');
|
||||
|
||||
const onMasterPasswordSave = useCallback(async () => {
|
||||
Setting.setValue('encryption.masterPassword', inputMasterPassword);
|
||||
|
||||
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
}, [inputMasterPassword]);
|
||||
|
||||
const onMasterPasswordChange = useCallback((password: string) => {
|
||||
setInputMasterPassword(password);
|
||||
}, []);
|
||||
|
||||
return { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange };
|
||||
};
|
||||
|
||||
export const useInputPasswords = (propsPasswords: Record<string, string>) => {
|
||||
const [inputPasswords, setInputPasswords] = useState<Record<string, string>>(propsPasswords);
|
||||
|
||||
useEffect(() => {
|
||||
setInputPasswords(propsPasswords);
|
||||
}, [propsPasswords]);
|
||||
|
||||
const onInputPasswordChange = useCallback((mk: MasterKeyEntity, password: string) => {
|
||||
setInputPasswords(current => {
|
||||
return {
|
||||
...current,
|
||||
[mk.id]: password,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { inputPasswords, onInputPasswordChange };
|
||||
};
|
||||
|
||||
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
|
||||
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
|
||||
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const newPasswordChecks: PasswordChecks = {};
|
||||
const newMasterPasswordKeys: PasswordChecks = {};
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwords);
|
||||
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||
newPasswordChecks[mk.id] = ok;
|
||||
newMasterPasswordKeys[mk.id] = password === masterPassword;
|
||||
}
|
||||
|
||||
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
|
||||
|
||||
if (event.cancelled) return;
|
||||
|
||||
setPasswordChecks(newPasswordChecks);
|
||||
setMasterPasswordKeys(newMasterPasswordKeys);
|
||||
}, [masterKeys, masterPassword]);
|
||||
|
||||
return { passwordChecks, masterPasswordKeys };
|
||||
};
|
||||
|
||||
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
|
||||
const passwordCheck = passwordChecks[masterKey.id];
|
||||
if (!passwordCheck) {
|
||||
return _('Please enter your password in the master key list below before upgrading the key.');
|
||||
}
|
||||
|
||||
try {
|
||||
const password = passwords[masterKey.id];
|
||||
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
|
||||
await MasterKey.save(newMasterKey);
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
return _('The master key has been upgraded successfully!');
|
||||
} catch (error) {
|
||||
return _('Could not upgrade master key: %s', error.message);
|
||||
}
|
||||
};
|
||||
@@ -1,196 +0,0 @@
|
||||
import EncryptionService from '../../services/e2ee/EncryptionService';
|
||||
import { _ } from '../../locale';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import Setting from '../../models/Setting';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { reg } from '../../registry.js';
|
||||
import shim from '../../shim';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import time from '../../time';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||
|
||||
class Shared {
|
||||
|
||||
private refreshStatsIID_: any;
|
||||
|
||||
public initialize(comp: any, props: any) {
|
||||
comp.state = {
|
||||
passwordChecks: {},
|
||||
// Master keys that can be decrypted with the master password
|
||||
// (normally all of them, but for legacy support we need this).
|
||||
masterPasswordKeys: {},
|
||||
stats: {
|
||||
encrypted: null,
|
||||
total: null,
|
||||
},
|
||||
passwords: Object.assign({}, props.passwords),
|
||||
showDisabledMasterKeys: false,
|
||||
masterPasswordInput: '',
|
||||
};
|
||||
comp.isMounted_ = false;
|
||||
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
|
||||
public async refreshStats(comp: any) {
|
||||
const stats = await BaseItem.encryptedItemsStats();
|
||||
comp.setState({
|
||||
stats: stats,
|
||||
});
|
||||
}
|
||||
|
||||
public async toggleShowDisabledMasterKeys(comp: any) {
|
||||
comp.setState({ showDisabledMasterKeys: !comp.state.showDisabledMasterKeys });
|
||||
}
|
||||
|
||||
public async reencryptData() {
|
||||
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
|
||||
if (!ok) return;
|
||||
|
||||
await BaseItem.forceSyncAll();
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
alert(_('Your data is going to be re-encrypted and synced again.'));
|
||||
}
|
||||
|
||||
public dontReencryptData() {
|
||||
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
|
||||
}
|
||||
|
||||
public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) {
|
||||
const passwordCheck = comp.state.passwordChecks[masterKey.id];
|
||||
if (!passwordCheck) {
|
||||
alert(_('Please enter your password in the master key list below before upgrading the key.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const password = comp.state.passwords[masterKey.id];
|
||||
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
|
||||
await MasterKey.save(newMasterKey);
|
||||
void reg.waitForSyncFinishedThenSync();
|
||||
alert(_('The master key has been upgraded successfully!'));
|
||||
} catch (error) {
|
||||
alert(_('Could not upgrade master key: %s', error.message));
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(comp: any) {
|
||||
this.componentDidUpdate(comp);
|
||||
|
||||
void this.refreshStats(comp);
|
||||
|
||||
if (this.refreshStatsIID_) {
|
||||
shim.clearInterval(this.refreshStatsIID_);
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
|
||||
this.refreshStatsIID_ = shim.setInterval(() => {
|
||||
if (!comp.isMounted_) {
|
||||
shim.clearInterval(this.refreshStatsIID_);
|
||||
this.refreshStatsIID_ = null;
|
||||
return;
|
||||
}
|
||||
void this.refreshStats(comp);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
public componentDidUpdate(comp: any, prevProps: any = null) {
|
||||
if (prevProps && comp.props.passwords !== prevProps.passwords) {
|
||||
comp.setState({ passwords: Object.assign({}, comp.props.passwords) });
|
||||
}
|
||||
|
||||
if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) {
|
||||
comp.checkPasswords();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.refreshStatsIID_) {
|
||||
shim.clearInterval(this.refreshStatsIID_);
|
||||
this.refreshStatsIID_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
|
||||
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
|
||||
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
|
||||
if (activeMasterKey && masterPassword) {
|
||||
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async checkPasswords(comp: any) {
|
||||
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
|
||||
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
|
||||
for (let i = 0; i < comp.props.masterKeys.length; i++) {
|
||||
const mk = comp.props.masterKeys[i];
|
||||
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
|
||||
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||
passwordChecks[mk.id] = ok;
|
||||
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
|
||||
}
|
||||
|
||||
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
|
||||
|
||||
comp.setState({ passwordChecks, masterPasswordKeys });
|
||||
}
|
||||
|
||||
public masterPasswordStatus(comp: any) {
|
||||
// Don't translate for now because that's temporary - later it should
|
||||
// always be set and the label should be replaced by a "Change master
|
||||
// password" button.
|
||||
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
|
||||
}
|
||||
|
||||
public decryptedStatText(comp: any) {
|
||||
const stats = comp.state.stats;
|
||||
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
||||
const totalCount = stats.total !== null ? stats.total : '-';
|
||||
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
public onSavePasswordClick(comp: any, mk: MasterKeyEntity) {
|
||||
const password = comp.state.passwords[mk.id];
|
||||
if (!password) {
|
||||
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
|
||||
} else {
|
||||
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
|
||||
}
|
||||
|
||||
comp.checkPasswords();
|
||||
}
|
||||
|
||||
public onMasterPasswordChange(comp: any, value: string) {
|
||||
comp.setState({ masterPasswordInput: value });
|
||||
}
|
||||
|
||||
public onMasterPasswordSave(comp: any) {
|
||||
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
|
||||
}
|
||||
|
||||
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
|
||||
const passwords = Object.assign({}, comp.state.passwords);
|
||||
passwords[mk.id] = password;
|
||||
comp.setState({ passwords: passwords });
|
||||
}
|
||||
|
||||
public onToggleEnabledClick(_comp: any, mk: MasterKeyEntity) {
|
||||
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
|
||||
}
|
||||
|
||||
public enableEncryptionConfirmationMessages(masterKey: MasterKeyEntity) {
|
||||
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
|
||||
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
|
||||
return msg;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const shared = new Shared();
|
||||
|
||||
export default shared;
|
||||
@@ -25,6 +25,7 @@ function useEventListener(
|
||||
const eventListener = (event: Event) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!savedHandler?.current) {
|
||||
// @ts-ignore
|
||||
savedHandler.current(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,6 +33,11 @@ export interface ItemsThatNeedSyncResult {
|
||||
neverSyncedItemIds: string[];
|
||||
}
|
||||
|
||||
export interface EncryptedItemsStats {
|
||||
encrypted: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export default class BaseItem extends BaseModel {
|
||||
|
||||
public static encryptionService_: any = null;
|
||||
@@ -513,7 +518,7 @@ export default class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
static async encryptedItemsStats() {
|
||||
public static async encryptedItemsStats(): Promise<EncryptedItemsStats> {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
let encryptedCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
Generated
+60
-1
@@ -67,6 +67,7 @@
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/react": "^17.0.20",
|
||||
"clean-html": "^1.5.0",
|
||||
"jest": "^26.6.3",
|
||||
"sharp": "^0.26.2",
|
||||
@@ -1073,6 +1074,29 @@
|
||||
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "17.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
|
||||
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
||||
@@ -2253,6 +2277,12 @@
|
||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@@ -5639,6 +5669,7 @@
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
|
||||
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
|
||||
"deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"block-stream": "*",
|
||||
@@ -9758,6 +9789,29 @@
|
||||
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "17.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
|
||||
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@types/scheduler": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
||||
@@ -10725,6 +10779,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
|
||||
"dev": true
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@@ -15643,7 +15703,6 @@
|
||||
},
|
||||
"uslug": {
|
||||
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
|
||||
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
|
||||
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
|
||||
"requires": {
|
||||
"node-emoji": "^1.10.0",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/react": "^17.0.20",
|
||||
"clean-html": "^1.5.0",
|
||||
"jest": "^26.6.3",
|
||||
"sharp": "^0.26.2",
|
||||
|
||||
@@ -589,4 +589,25 @@ describe('reducer', function() {
|
||||
expect(state.selectedFolderId).toEqual(null);
|
||||
expect(state.selectedNoteIds[0]).toEqual(notes[1].id);
|
||||
});
|
||||
|
||||
// tests for NOTE_UPDATE_ALL about issue #5447
|
||||
it('should not change selectedNoteIds object when selections are not changed', async () => {
|
||||
const folders = await createNTestFolders(1);
|
||||
const notes = await createNTestNotes(5, folders[0]);
|
||||
{
|
||||
// Case 1. Selected notes are changed when one of selected notes is deleted.
|
||||
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
||||
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes.slice(0, 4), notesSource: 'test' });
|
||||
const expected = [notes[0].id, notes[2].id].sort();
|
||||
expect([...state.selectedNoteIds].sort()).toEqual(expected);
|
||||
}
|
||||
{
|
||||
// Case 2. Selected notes and object identity are unchanged when notes are not changed.
|
||||
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
||||
const expected = state.selectedNoteIds;
|
||||
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: 'test' });
|
||||
// Object identity is checked. Don't use toEqual() or toStrictEqual() here.
|
||||
expect(state.selectedNoteIds).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,7 +433,7 @@ function updateSelectedNotesFromExistingNotes(draft: Draft<State>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON.stringify(draft.selectedNoteIds) === JSON.stringify(newSelectedNoteIds)) return;
|
||||
draft.selectedNoteIds = newSelectedNoteIds;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ interface MenuItem {
|
||||
click: Function;
|
||||
role?: any;
|
||||
accelerator?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface MenuItems {
|
||||
@@ -78,6 +79,7 @@ export default class MenuUtils {
|
||||
id: command.declaration.name,
|
||||
label: this.service.label(commandName),
|
||||
click: () => onClick(command.declaration.name),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
if (command.declaration.role) item.role = command.declaration.role;
|
||||
@@ -132,10 +134,13 @@ export default class MenuUtils {
|
||||
public pluginContextMenuItems(plugins: PluginStates, location: MenuItemLocation): MenuItem[] {
|
||||
const output: MenuItem[] = [];
|
||||
const pluginViewInfos = pluginUtils.viewInfosByType(plugins, 'menuItem');
|
||||
const whenClauseContext = this.service.currentWhenClauseContext();
|
||||
|
||||
for (const info of pluginViewInfos) {
|
||||
if (info.view.location !== location) continue;
|
||||
output.push(this.commandToStatefulMenuItem(info.view.commandName));
|
||||
const menuItem = this.commandToStatefulMenuItem(info.view.commandName);
|
||||
menuItem.enabled = this.service.isEnabled(info.view.commandName, whenClauseContext);
|
||||
output.push(menuItem);
|
||||
}
|
||||
|
||||
if (output.length) output.splice(0, 0, { type: 'separator' } as any);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -103,7 +103,7 @@ export async function migrateMasterPassword() {
|
||||
// previously any master key could be encrypted with any password, so to support
|
||||
// this legacy case, we first check if the MK decrypts with the master password.
|
||||
// If not, try with the master key specific password, if any is defined.
|
||||
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
|
||||
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity, passwordCache: Record<string, string> = null): Promise<string> {
|
||||
const masterPassword = Setting.value('encryption.masterPassword');
|
||||
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
|
||||
logger.info('findMasterKeyPassword: Using master password');
|
||||
@@ -112,7 +112,7 @@ export async function findMasterKeyPassword(service: EncryptionService, masterKe
|
||||
|
||||
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
|
||||
|
||||
const passwords = Setting.value('encryption.passwordCache');
|
||||
const passwords = passwordCache ? passwordCache : Setting.value('encryption.passwordCache');
|
||||
return passwords[masterKey.id];
|
||||
}
|
||||
|
||||
@@ -161,7 +161,9 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
|
||||
}
|
||||
|
||||
export function getDefaultMasterKey(): MasterKeyEntity {
|
||||
const mk = getActiveMasterKey();
|
||||
if (mk) return mk;
|
||||
return MasterKey.latest();
|
||||
let mk = getActiveMasterKey();
|
||||
if (!mk || masterKeyEnabled(mk)) {
|
||||
mk = MasterKey.latest();
|
||||
}
|
||||
return mk && masterKeyEnabled(mk) ? mk : null;
|
||||
}
|
||||
|
||||
+16
-12
@@ -1,22 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { NoteEntity, ResourceEntity } from './services/database/types';
|
||||
|
||||
let isTestingEnv_ = false;
|
||||
|
||||
// We need to ensure that there's only one instance of React being used by
|
||||
// all the packages. In particular, the lib might need React to define
|
||||
// generic hooks, but it shouldn't have React in its dependencies as that
|
||||
// would cause the following error:
|
||||
// We need to ensure that there's only one instance of React being used by all
|
||||
// the packages. In particular, the lib might need React to define generic
|
||||
// hooks, but it shouldn't have React in its dependencies as that would cause
|
||||
// the following error:
|
||||
//
|
||||
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
|
||||
//
|
||||
// So instead, the **applications** include React as a dependency, then
|
||||
// pass it to any other packages using the shim. Essentially, only one
|
||||
// package should require React, and in our case that should be one of the
|
||||
// applications (app-desktop, app-mobile, etc.) since we are sure they
|
||||
// won't be dependency to other packages (unlike the lib which can be
|
||||
// included anywhere).
|
||||
|
||||
let react_: any = null;
|
||||
// So instead, the **applications** include React as a dependency, then pass it
|
||||
// to any other packages using the shim. Essentially, only one package should
|
||||
// require React, and in our case that should be one of the applications
|
||||
// (app-desktop, app-mobile, etc.) since we are sure they won't be dependency to
|
||||
// other packages (unlike the lib which can be included anywhere).
|
||||
//
|
||||
// Regarding the type - althought we import React, we only use it as a type
|
||||
// using `typeof React`. This is just to get types in hooks.
|
||||
//
|
||||
// https://stackoverflow.com/a/42816077/561309
|
||||
let react_: typeof React = null;
|
||||
|
||||
const shim = {
|
||||
Geolocation: null as any,
|
||||
|
||||
@@ -1,75 +1 @@
|
||||
/*
|
||||
|
||||
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
|
||||
|
||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
||||
|
||||
*/
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #abb2bf;
|
||||
background: #282c34;
|
||||
}
|
||||
.hljs-keyword, .hljs-operator {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match .hljs-constructor {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function .hljs-params {
|
||||
color: #A6E22E;
|
||||
}
|
||||
.hljs-function .hljs-params .hljs-typing {
|
||||
color: #FD971F;
|
||||
}
|
||||
.hljs-module-access .hljs-module {
|
||||
color: #7e57c2;
|
||||
}
|
||||
.hljs-constructor {
|
||||
color: #e2b93d;
|
||||
}
|
||||
.hljs-constructor .hljs-string {
|
||||
color: #9CCC65;
|
||||
}
|
||||
.hljs-comment, .hljs-quote {
|
||||
color: #b18eb1;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag, .hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
|
||||
color: #98c379;
|
||||
}
|
||||
.hljs-built_in, .hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-keyword,.hljs-operator,.hljs-pattern-match{color:#f92672}.hljs-function,.hljs-pattern-match .hljs-constructor{color:#61aeee}.hljs-function .hljs-params{color:#a6e22e}.hljs-function .hljs-params .hljs-typing{color:#fd971f}.hljs-module-access .hljs-module{color:#7e57c2}.hljs-constructor{color:#e2b93d}.hljs-constructor .hljs-string{color:#9ccc65}.hljs-comment,.hljs-quote{color:#b18eb1;font-style:italic}.hljs-doctag,.hljs-formula{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
||||
@@ -1,96 +1 @@
|
||||
/*
|
||||
|
||||
Atom One Light by Daniel Gamage
|
||||
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
|
||||
|
||||
base: #fafafa
|
||||
mono-1: #383a42
|
||||
mono-2: #686b77
|
||||
mono-3: #a0a1a7
|
||||
hue-1: #0184bb
|
||||
hue-2: #4078f2
|
||||
hue-3: #a626a4
|
||||
hue-4: #50a14f
|
||||
hue-5: #e45649
|
||||
hue-5-2: #c91243
|
||||
hue-6: #986801
|
||||
hue-6-2: #c18401
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #383a42;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-formula {
|
||||
color: #a626a4;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-deletion,
|
||||
.hljs-subst {
|
||||
color: #e45649;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #0184bb;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta-string {
|
||||
color: #50a14f;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-class .hljs-title {
|
||||
color: #c18401;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-number {
|
||||
color: #986801;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-title {
|
||||
color: #4078f2;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
||||
+7
-5
File diff suppressed because one or more lines are too long
Generated
+52
-347
@@ -29,7 +29,7 @@
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"mermaid": "^8.10.2",
|
||||
"mermaid": "^8.12.1",
|
||||
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1998,7 +1998,8 @@
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cache-base": {
|
||||
"version": "1.0.1",
|
||||
@@ -2029,15 +2030,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camel-case": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
|
||||
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
|
||||
"dependencies": {
|
||||
"no-case": "^2.2.0",
|
||||
"upper-case": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
@@ -2135,25 +2127,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||
"integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
|
||||
"dependencies": {
|
||||
"source-map": "~0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-css/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
@@ -2286,17 +2259,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/css-b64-images": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
|
||||
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=",
|
||||
"bin": {
|
||||
"css-b64-images": "bin/css-b64-images"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||
@@ -2647,6 +2609,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
|
||||
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
|
||||
"deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -2797,6 +2760,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
|
||||
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -2839,14 +2807,6 @@
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
|
||||
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
|
||||
},
|
||||
"node_modules/entity-decode": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz",
|
||||
"integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==",
|
||||
"dependencies": {
|
||||
"he": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -3542,14 +3502,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
|
||||
@@ -3590,26 +3542,6 @@
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-minifier": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
|
||||
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
|
||||
"dependencies": {
|
||||
"camel-case": "^3.0.0",
|
||||
"clean-css": "^4.2.1",
|
||||
"commander": "^2.19.0",
|
||||
"he": "^1.2.0",
|
||||
"param-case": "^2.1.1",
|
||||
"relateurl": "^0.2.7",
|
||||
"uglify-js": "^3.5.1"
|
||||
},
|
||||
"bin": {
|
||||
"html-minifier": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@@ -6045,11 +5977,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
|
||||
},
|
||||
"node_modules/lower-case": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
|
||||
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -6234,21 +6161,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "8.10.2",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.10.2.tgz",
|
||||
"integrity": "sha512-Za5MrbAOMbEsyY4ONgGjfYz06sbwF1iNGRzp1sQqpOtvXxjxGu/J1jRJ8QyE9kD/D9zj1/KlRrYegWEvA7eZ5Q==",
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.12.1.tgz",
|
||||
"integrity": "sha512-0UCcSF0FLoNcPBsRF4f9OIV32t41fV18//z8o3S+FDz2PbDA1CRGKdQF9IX84VP4Tv9kcgJI/oqJdcBEtB/GPA==",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^3.1.0",
|
||||
"d3": "^5.7.0",
|
||||
"dagre": "^0.8.4",
|
||||
"d3": "^5.16.0",
|
||||
"dagre": "^0.8.5",
|
||||
"dagre-d3": "^0.6.4",
|
||||
"entity-decode": "^2.0.2",
|
||||
"graphlib": "^2.1.7",
|
||||
"he": "^1.2.0",
|
||||
"khroma": "^1.1.0",
|
||||
"minify": "^4.1.1",
|
||||
"moment-mini": "^2.22.1",
|
||||
"stylis": "^3.5.2"
|
||||
"dompurify": "2.3.1",
|
||||
"graphlib": "^2.1.8",
|
||||
"khroma": "^1.4.1",
|
||||
"moment-mini": "^2.24.0",
|
||||
"stylis": "^4.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
@@ -6294,26 +6219,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/minify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
|
||||
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
|
||||
"dependencies": {
|
||||
"clean-css": "^4.1.6",
|
||||
"css-b64-images": "~0.2.5",
|
||||
"debug": "^4.1.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"terser": "^4.0.0",
|
||||
"try-catch": "^2.0.0",
|
||||
"try-to-catch": "^1.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"minify": "bin/minify.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -6365,7 +6270,8 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@@ -6401,14 +6307,6 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/no-case": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
|
||||
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
|
||||
"dependencies": {
|
||||
"lower-case": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
|
||||
@@ -6684,14 +6582,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/param-case": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
|
||||
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
|
||||
"dependencies": {
|
||||
"no-case": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
|
||||
@@ -6973,14 +6863,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/relateurl": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
||||
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/remove-trailing-separator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||
@@ -7661,6 +7543,7 @@
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
|
||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
@@ -7670,6 +7553,7 @@
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7876,9 +7760,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
|
||||
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
|
||||
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
@@ -7948,30 +7832,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
|
||||
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
|
||||
"dependencies": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
@@ -8084,19 +7944,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/try-catch": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
|
||||
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/try-to-catch": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
|
||||
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -8172,17 +8019,6 @@
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.13.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
|
||||
"integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==",
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/union-value": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||
@@ -8262,11 +8098,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/upper-case": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
|
||||
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
|
||||
@@ -10235,7 +10066,8 @@
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
|
||||
"dev": true
|
||||
},
|
||||
"cache-base": {
|
||||
"version": "1.0.1",
|
||||
@@ -10260,15 +10092,6 @@
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"camel-case": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
|
||||
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
|
||||
"requires": {
|
||||
"no-case": "^2.2.0",
|
||||
"upper-case": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
@@ -10347,21 +10170,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||
"integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==",
|
||||
"requires": {
|
||||
"source-map": "~0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
@@ -10475,11 +10283,6 @@
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"css-b64-images": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
|
||||
"integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI="
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||
@@ -10811,6 +10614,7 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
|
||||
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
@@ -10921,6 +10725,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz",
|
||||
"integrity": "sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw=="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -10957,14 +10766,6 @@
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
|
||||
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
|
||||
},
|
||||
"entity-decode": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/entity-decode/-/entity-decode-2.0.2.tgz",
|
||||
"integrity": "sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==",
|
||||
"requires": {
|
||||
"he": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -11512,11 +11313,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
|
||||
@@ -11548,20 +11344,6 @@
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"html-minifier": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
|
||||
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
|
||||
"requires": {
|
||||
"camel-case": "^3.0.0",
|
||||
"clean-css": "^4.2.1",
|
||||
"commander": "^2.19.0",
|
||||
"he": "^1.2.0",
|
||||
"param-case": "^2.1.1",
|
||||
"relateurl": "^0.2.7",
|
||||
"uglify-js": "^3.5.1"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@@ -13427,11 +13209,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
|
||||
},
|
||||
"lower-case": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
|
||||
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -13594,21 +13371,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"mermaid": {
|
||||
"version": "8.10.2",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.10.2.tgz",
|
||||
"integrity": "sha512-Za5MrbAOMbEsyY4ONgGjfYz06sbwF1iNGRzp1sQqpOtvXxjxGu/J1jRJ8QyE9kD/D9zj1/KlRrYegWEvA7eZ5Q==",
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.12.1.tgz",
|
||||
"integrity": "sha512-0UCcSF0FLoNcPBsRF4f9OIV32t41fV18//z8o3S+FDz2PbDA1CRGKdQF9IX84VP4Tv9kcgJI/oqJdcBEtB/GPA==",
|
||||
"requires": {
|
||||
"@braintree/sanitize-url": "^3.1.0",
|
||||
"d3": "^5.7.0",
|
||||
"dagre": "^0.8.4",
|
||||
"d3": "^5.16.0",
|
||||
"dagre": "^0.8.5",
|
||||
"dagre-d3": "^0.6.4",
|
||||
"entity-decode": "^2.0.2",
|
||||
"graphlib": "^2.1.7",
|
||||
"he": "^1.2.0",
|
||||
"khroma": "^1.1.0",
|
||||
"minify": "^4.1.1",
|
||||
"moment-mini": "^2.22.1",
|
||||
"stylis": "^3.5.2"
|
||||
"dompurify": "2.3.1",
|
||||
"graphlib": "^2.1.8",
|
||||
"khroma": "^1.4.1",
|
||||
"moment-mini": "^2.24.0",
|
||||
"stylis": "^4.0.10"
|
||||
}
|
||||
},
|
||||
"micromatch": {
|
||||
@@ -13642,20 +13417,6 @@
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
},
|
||||
"minify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
|
||||
"integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
|
||||
"requires": {
|
||||
"clean-css": "^4.1.6",
|
||||
"css-b64-images": "~0.2.5",
|
||||
"debug": "^4.1.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"terser": "^4.0.0",
|
||||
"try-catch": "^2.0.0",
|
||||
"try-to-catch": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -13700,7 +13461,8 @@
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@@ -13733,14 +13495,6 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"no-case": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
|
||||
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
|
||||
"requires": {
|
||||
"lower-case": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node-emoji": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
|
||||
@@ -13951,14 +13705,6 @@
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
},
|
||||
"param-case": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
|
||||
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
|
||||
"requires": {
|
||||
"no-case": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
|
||||
@@ -14172,11 +13918,6 @@
|
||||
"safe-regex": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"relateurl": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
||||
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
|
||||
},
|
||||
"remove-trailing-separator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
|
||||
@@ -14731,6 +14472,7 @@
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
|
||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
@@ -14739,7 +14481,8 @@
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -14905,9 +14648,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"stylis": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
|
||||
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
|
||||
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
@@ -14961,23 +14704,6 @@
|
||||
"supports-hyperlinks": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
|
||||
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
|
||||
"requires": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
@@ -15068,16 +14794,6 @@
|
||||
"punycode": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"try-catch": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
|
||||
"integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg=="
|
||||
},
|
||||
"try-to-catch": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
|
||||
"integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -15134,11 +14850,6 @@
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.13.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
|
||||
"integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g=="
|
||||
},
|
||||
"union-value": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||
@@ -15201,11 +14912,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"upper-case": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
|
||||
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
|
||||
@@ -15229,7 +14935,6 @@
|
||||
},
|
||||
"uslug": {
|
||||
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
|
||||
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
|
||||
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
|
||||
"requires": {
|
||||
"node-emoji": "^1.10.0",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"mermaid": "^8.10.2",
|
||||
"mermaid": "^8.12.1",
|
||||
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
|
||||
},
|
||||
"gitHead": "80c0089d2c52aff608b2bea74389de5a7f12f2e2"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/server",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.7",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"@koa/cors": "^3.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
|
||||
@@ -48,6 +48,9 @@ export interface EnvVariables {
|
||||
BUSINESS_EMAIL?: string;
|
||||
|
||||
COOKIES_SECURE?: string;
|
||||
|
||||
SLOW_QUERY_LOG_ENABLED?: string;
|
||||
SLOW_QUERY_LOG_MIN_DURATION?: string; // ms
|
||||
}
|
||||
|
||||
let runningInDocker_: boolean = false;
|
||||
@@ -56,6 +59,17 @@ export function runningInDocker(): boolean {
|
||||
return runningInDocker_;
|
||||
}
|
||||
|
||||
function envParseBool(s: string): boolean {
|
||||
return s === '1';
|
||||
}
|
||||
|
||||
function envParseInt(s: string, defaultValue: number = null): number {
|
||||
if (!s) return defaultValue === null ? 0 : defaultValue;
|
||||
const output = Number(s);
|
||||
if (isNaN(output)) throw new Error(`Invalid number: ${s}`);
|
||||
return output;
|
||||
}
|
||||
|
||||
function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): string {
|
||||
if (env.POSTGRES_HOST) {
|
||||
// When running within Docker, the app localhost is different from the
|
||||
@@ -72,8 +86,16 @@ function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): strin
|
||||
}
|
||||
|
||||
function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): DatabaseConfig {
|
||||
const baseConfig: DatabaseConfig = {
|
||||
client: DatabaseConfigClient.Null,
|
||||
name: '',
|
||||
slowQueryLogEnabled: envParseBool(env.SLOW_QUERY_LOG_ENABLED),
|
||||
slowQueryLogMinDuration: envParseInt(env.SLOW_QUERY_LOG_MIN_DURATION, 10000),
|
||||
};
|
||||
|
||||
if (env.DB_CLIENT === 'pg') {
|
||||
return {
|
||||
...baseConfig,
|
||||
client: DatabaseConfigClient.PostgreSQL,
|
||||
name: env.POSTGRES_DATABASE || 'joplin',
|
||||
user: env.POSTGRES_USER || 'joplin',
|
||||
@@ -84,6 +106,7 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
|
||||
}
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
client: DatabaseConfigClient.SQLite,
|
||||
name: env.SQLITE_DATABASE,
|
||||
asyncStackTraces: true,
|
||||
|
||||
+54
-16
@@ -104,25 +104,63 @@ export async function waitForConnection(dbConfig: DatabaseConfig): Promise<Conne
|
||||
}
|
||||
}
|
||||
|
||||
function makeSlowQueryHandler(duration: number, connection: any, sql: string, bindings: any[]) {
|
||||
return setTimeout(() => {
|
||||
try {
|
||||
logger.warn(`Slow query (${duration}ms+):`, connection.raw(sql, bindings).toString());
|
||||
} catch (error) {
|
||||
logger.error('Could not log slow query', { sql, bindings }, error);
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
export function setupSlowQueryLog(connection: DbConnection, slowQueryLogMinDuration: number) {
|
||||
interface QueryInfo {
|
||||
timeoutId: any;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
const queryInfos: Record<any, QueryInfo> = {};
|
||||
|
||||
// These queries do not return a response, so "query-response" is not
|
||||
// called.
|
||||
const ignoredQueries = /^BEGIN|SAVEPOINT|RELEASE SAVEPOINT|COMMIT|ROLLBACK/gi;
|
||||
|
||||
connection.on('query', (data) => {
|
||||
const sql: string = data.sql;
|
||||
|
||||
if (!sql || sql.match(ignoredQueries)) return;
|
||||
|
||||
const timeoutId = makeSlowQueryHandler(slowQueryLogMinDuration, connection, sql, data.bindings);
|
||||
|
||||
queryInfos[data.__knexQueryUid] = {
|
||||
timeoutId,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
connection.on('query-response', (_response, data) => {
|
||||
const q = queryInfos[data.__knexQueryUid];
|
||||
if (q) {
|
||||
clearTimeout(q.timeoutId);
|
||||
delete queryInfos[data.__knexQueryUid];
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('query-error', (_response, data) => {
|
||||
const q = queryInfos[data.__knexQueryUid];
|
||||
if (q) {
|
||||
clearTimeout(q.timeoutId);
|
||||
delete queryInfos[data.__knexQueryUid];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function connectDb(dbConfig: DatabaseConfig): Promise<DbConnection> {
|
||||
const connection = knex(makeKnexConfig(dbConfig));
|
||||
|
||||
const debugSlowQueries = false;
|
||||
|
||||
if (debugSlowQueries) {
|
||||
const startTimes: Record<string, number> = {};
|
||||
|
||||
const slowQueryDuration = 10;
|
||||
|
||||
connection.on('query', (data) => {
|
||||
startTimes[data.__knexQueryUid] = Date.now();
|
||||
});
|
||||
|
||||
connection.on('query-response', (_response, data) => {
|
||||
const duration = Date.now() - startTimes[data.__knexQueryUid];
|
||||
if (duration < slowQueryDuration) return;
|
||||
console.info(`SQL: ${data.sql} (${duration}ms)`);
|
||||
});
|
||||
if (dbConfig.slowQueryLogEnabled) {
|
||||
setupSlowQueryLog(connection, dbConfig.slowQueryLogMinDuration);
|
||||
}
|
||||
|
||||
return connection;
|
||||
|
||||
@@ -42,6 +42,13 @@ async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
function levelClassName(level: NotificationLevel): string {
|
||||
if (level === NotificationLevel.Important) return 'is-warning';
|
||||
if (level === NotificationLevel.Normal) return 'is-info';
|
||||
if (level === NotificationLevel.Error) return 'is-danger';
|
||||
throw new Error(`Unknown level: ${level}`);
|
||||
}
|
||||
|
||||
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
|
||||
const markdownIt = new MarkdownIt();
|
||||
|
||||
@@ -52,7 +59,7 @@ async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
levelClassName: levelClassName(n.level),
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { routeResponseFormat, Response, RouteResponseFormat, execRequest } from
|
||||
import { AppContext, Env } from '../utils/types';
|
||||
import { isView, View } from '../services/MustacheService';
|
||||
import config from '../config';
|
||||
import { userIp } from '../utils/requestUtils';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
const requestStartTime = Date.now();
|
||||
@@ -26,9 +27,9 @@ export default async function(ctx: AppContext) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
ctx.joplin.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
|
||||
ctx.joplin.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + `: ${userIp(ctx)}: ${error.message}`);
|
||||
} else {
|
||||
ctx.joplin.appLogger().error(error);
|
||||
ctx.joplin.appLogger().error(userIp(ctx), error);
|
||||
}
|
||||
|
||||
// Uncomment this when getting HTML blobs as errors while running tests.
|
||||
|
||||
@@ -7,6 +7,9 @@ import { Models } from './factory';
|
||||
import * as EventEmitter from 'events';
|
||||
import { Config } from '../utils/types';
|
||||
import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/personalizedUserContentBaseUrl';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('BaseModel');
|
||||
|
||||
export interface SaveOptions {
|
||||
isNew?: boolean;
|
||||
@@ -163,36 +166,38 @@ export default abstract class BaseModel<T> {
|
||||
//
|
||||
// The `name` argument is only for debugging, so that any stuck transaction
|
||||
// can be more easily identified.
|
||||
protected async withTransaction<T>(fn: Function, name: string = null): Promise<T> {
|
||||
const debugTransaction = false;
|
||||
protected async withTransaction<T>(fn: Function, name: string): Promise<T> {
|
||||
const debugSteps = false;
|
||||
const debugTimeout = true;
|
||||
const timeoutMs = 10000;
|
||||
|
||||
const debugTimerId = debugTransaction ? setTimeout(() => {
|
||||
console.info('Transaction did not complete:', name, txIndex);
|
||||
}, 5000) : null;
|
||||
let txIndex = 0;
|
||||
|
||||
const txIndex = await this.transactionHandler_.start();
|
||||
const debugTimerId = debugTimeout ? setTimeout(() => {
|
||||
logger.error(`Transaction #${txIndex} did not complete:`, name);
|
||||
logger.error('Transaction stack:');
|
||||
logger.error(this.transactionHandler_.stackInfo);
|
||||
}, timeoutMs) : null;
|
||||
|
||||
if (debugTransaction) console.info('START', name, txIndex);
|
||||
txIndex = await this.transactionHandler_.start(name);
|
||||
|
||||
if (debugSteps) console.info('START', name, txIndex);
|
||||
|
||||
let output: T = null;
|
||||
|
||||
try {
|
||||
output = await fn();
|
||||
} catch (error) {
|
||||
if (debugSteps) console.info('ROLLBACK', name, txIndex);
|
||||
|
||||
await this.transactionHandler_.rollback(txIndex);
|
||||
|
||||
if (debugTransaction) {
|
||||
console.info('ROLLBACK', name, txIndex);
|
||||
clearTimeout(debugTimerId);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
if (debugTimerId) clearTimeout(debugTimerId);
|
||||
}
|
||||
|
||||
if (debugTransaction) {
|
||||
console.info('COMMIT', name, txIndex);
|
||||
clearTimeout(debugTimerId);
|
||||
}
|
||||
if (debugSteps) console.info('COMMIT', name, txIndex);
|
||||
|
||||
await this.transactionHandler_.commit(txIndex);
|
||||
return output;
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class ItemResourceModel extends BaseModel<ItemResource> {
|
||||
resource_id: resourceId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 'ItemResourceModel::addResourceIds');
|
||||
}
|
||||
|
||||
public async byItemId(itemId: Uuid): Promise<string[]> {
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class NotificationModel extends BaseModel<KeyValue> {
|
||||
value: this.serializeValue(value),
|
||||
type,
|
||||
});
|
||||
});
|
||||
}, 'KeyValueModel::setValue');
|
||||
}
|
||||
|
||||
public async value<T>(key: string, defaultValue: Value = null): Promise<T> {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Notification, NotificationLevel, Uuid } from '../services/database/types';
|
||||
import { ErrorUnprocessableEntity } from '../utils/errors';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
import BaseModel, { ValidateOptions } from './BaseModel';
|
||||
|
||||
export enum NotificationKey {
|
||||
Any = 'any',
|
||||
ConfirmEmail = 'confirmEmail',
|
||||
PasswordSet = 'passwordSet',
|
||||
EmailConfirmed = 'emailConfirmed',
|
||||
@@ -52,6 +54,10 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'Thank you! Your account has been successfully upgraded to Pro.',
|
||||
},
|
||||
[NotificationKey.Any]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: '',
|
||||
},
|
||||
};
|
||||
|
||||
const type = notificationTypes[key];
|
||||
@@ -72,7 +78,9 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
}
|
||||
}
|
||||
|
||||
return this.save({ key, message, level, owner_id: userId });
|
||||
const actualKey = key === NotificationKey.Any ? `any_${uuidgen()}` : key;
|
||||
|
||||
return this.save({ key: actualKey, message, level, owner_id: userId });
|
||||
}
|
||||
|
||||
public async markAsRead(userId: Uuid, key: NotificationKey): Promise<void> {
|
||||
|
||||
@@ -315,7 +315,7 @@ export default class ShareModel extends BaseModel<Share> {
|
||||
for (const item of items) {
|
||||
await this.models().userItem().add(userId, item.id);
|
||||
}
|
||||
});
|
||||
}, 'ShareModel::createSharedFolderUserItems');
|
||||
}
|
||||
|
||||
public async shareFolder(owner: User, folderId: string): Promise<Share> {
|
||||
|
||||
@@ -126,7 +126,7 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
|
||||
}
|
||||
|
||||
return this.save({ ...shareUser, status });
|
||||
});
|
||||
}, 'ShareUserModel::setStatus');
|
||||
}
|
||||
|
||||
public async deleteByShare(share: Share): Promise<void> {
|
||||
|
||||
@@ -91,14 +91,14 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
|
||||
last_payment_time: now,
|
||||
last_payment_failed_time: 0,
|
||||
});
|
||||
});
|
||||
}, 'SubscriptionModel::handlePayment');
|
||||
} else {
|
||||
// We only update the payment failed time if it's not already set
|
||||
// since the only thing that matter is the first time the payment
|
||||
// failed.
|
||||
//
|
||||
// We don't update the user can_upload and enabled properties here
|
||||
// because it's done after a few days from CronService.
|
||||
// because it's done after a few days from TaskService.
|
||||
if (!sub.last_payment_failed_time) {
|
||||
const user = await this.models().user().load(sub.user_id, { fields: ['email', 'id', 'full_name'] });
|
||||
|
||||
@@ -145,7 +145,7 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
|
||||
});
|
||||
|
||||
return { user, subscription };
|
||||
});
|
||||
}, 'SubscriptionModel::saveUserAndSubscription');
|
||||
}
|
||||
|
||||
public async toggleSoftDelete(id: number, isDeleted: boolean) {
|
||||
|
||||
@@ -70,7 +70,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
|
||||
await this.add(userId, flagType, { updateUser: false });
|
||||
}
|
||||
await this.updateUserFromFlags(userId);
|
||||
});
|
||||
}, 'UserFlagModels::addMulti');
|
||||
}
|
||||
|
||||
public async removeMulti(userId: Uuid, flagTypes: UserFlagType[]) {
|
||||
@@ -79,7 +79,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
|
||||
await this.remove(userId, flagType, { updateUser: false });
|
||||
}
|
||||
await this.updateUserFromFlags(userId);
|
||||
});
|
||||
}, 'UserFlagModels::removeMulti');
|
||||
}
|
||||
|
||||
// As a general rule the `enabled` and `can_upload` properties should not
|
||||
@@ -95,17 +95,34 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
|
||||
enabled: 1,
|
||||
};
|
||||
|
||||
if (flags.find(f => f.type === UserFlagType.AccountWithoutSubscription)) {
|
||||
const accountWithoutSubscriptionFlag = flags.find(f => f.type === UserFlagType.AccountWithoutSubscription);
|
||||
const accountOverLimitFlag = flags.find(f => f.type === UserFlagType.AccountOverLimit);
|
||||
const failedPaymentWarningFlag = flags.find(f => f.type === UserFlagType.FailedPaymentWarning);
|
||||
const failedPaymentFinalFlag = flags.find(f => f.type === UserFlagType.FailedPaymentFinal);
|
||||
const subscriptionCancelledFlag = flags.find(f => f.type === UserFlagType.SubscriptionCancelled);
|
||||
const manuallyDisabledFlag = flags.find(f => f.type === UserFlagType.ManuallyDisabled);
|
||||
|
||||
if (accountWithoutSubscriptionFlag) {
|
||||
newProps.can_upload = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.AccountOverLimit)) {
|
||||
}
|
||||
|
||||
if (accountOverLimitFlag) {
|
||||
newProps.can_upload = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.FailedPaymentWarning)) {
|
||||
}
|
||||
|
||||
if (failedPaymentWarningFlag) {
|
||||
newProps.can_upload = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.FailedPaymentFinal)) {
|
||||
}
|
||||
|
||||
if (failedPaymentFinalFlag) {
|
||||
newProps.enabled = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.SubscriptionCancelled)) {
|
||||
}
|
||||
|
||||
if (subscriptionCancelledFlag) {
|
||||
newProps.enabled = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.ManuallyDisabled)) {
|
||||
}
|
||||
|
||||
if (manuallyDisabledFlag) {
|
||||
newProps.enabled = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ export default class UserItemModel extends BaseModel<UserItem> {
|
||||
}
|
||||
|
||||
return super.save(userItem, options);
|
||||
});
|
||||
}, 'UserItemModel::save');
|
||||
}
|
||||
|
||||
public async delete(_id: string | string[], _options: DeleteOptions = {}): Promise<void> {
|
||||
|
||||
@@ -369,7 +369,7 @@ export default class UserModel extends BaseModel<User> {
|
||||
key: `payment_failed_upload_disabled_${sub.last_payment_failed_time}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 'UserModel::handleFailedPaymentSubscriptions');
|
||||
}
|
||||
|
||||
public async handleOversizedAccounts() {
|
||||
@@ -430,12 +430,12 @@ export default class UserModel extends BaseModel<User> {
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 'UserModel::handleOversizedAccounts');
|
||||
}
|
||||
|
||||
private formatValues(user: User): User {
|
||||
const output: User = { ...user };
|
||||
if ('email' in output) output.email = user.email.trim().toLowerCase();
|
||||
if ('email' in output) output.email = (`${user.email}`).trim().toLowerCase();
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -466,7 +466,15 @@ export default class UserModel extends BaseModel<User> {
|
||||
if (isNew) UserModel.eventEmitter.emit('created');
|
||||
|
||||
return savedUser;
|
||||
});
|
||||
}, 'UserModel::save');
|
||||
}
|
||||
|
||||
public async saveMulti(users: User[], options: SaveOptions = {}): Promise<void> {
|
||||
await this.withTransaction(async () => {
|
||||
for (const user of users) {
|
||||
await this.save(user, options);
|
||||
}
|
||||
}, 'UserModel::saveMulti');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import config from '../../config';
|
||||
import { createTestUsers } from '../../tools/debugTools';
|
||||
import { clearDatabase, createTestUsers, CreateTestUsersOptions } from '../../tools/debugTools';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
@@ -12,6 +12,8 @@ router.public = true;
|
||||
|
||||
interface Query {
|
||||
action: string;
|
||||
count?: number;
|
||||
fromNum?: number;
|
||||
}
|
||||
|
||||
router.post('api/debug', async (_path: SubPath, ctx: AppContext) => {
|
||||
@@ -20,7 +22,16 @@ router.post('api/debug', async (_path: SubPath, ctx: AppContext) => {
|
||||
console.info(`Action: ${query.action}`);
|
||||
|
||||
if (query.action === 'createTestUsers') {
|
||||
await createTestUsers(ctx.joplin.db, config());
|
||||
const options: CreateTestUsersOptions = {};
|
||||
|
||||
if ('count' in query) options.count = query.count;
|
||||
if ('fromNum' in query) options.fromNum = query.fromNum;
|
||||
|
||||
await createTestUsers(ctx.joplin.db, config(), options);
|
||||
}
|
||||
|
||||
if (query.action === 'clearDatabase') {
|
||||
await clearDatabase(ctx.joplin.db);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { bodyFields, userIp } from '../../utils/requestUtils';
|
||||
import { User } from '../../services/database/types';
|
||||
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||
|
||||
@@ -12,7 +12,7 @@ const router = new Router(RouteType.Api);
|
||||
router.public = true;
|
||||
|
||||
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
|
||||
await limiterLoginBruteForce(ctx.ip);
|
||||
await limiterLoginBruteForce(userIp(ctx));
|
||||
|
||||
const fields: User = await bodyFields(ctx.req);
|
||||
const user = await ctx.joplin.models.user().login(fields.email, fields.password);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SubPath, redirect, makeUrl, UrlType } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { formParse } from '../../utils/requestUtils';
|
||||
import { formParse, userIp } from '../../utils/requestUtils';
|
||||
import config from '../../config';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
@@ -27,7 +27,7 @@ router.get('login', async (_path: SubPath, _ctx: AppContext) => {
|
||||
});
|
||||
|
||||
router.post('login', async (_path: SubPath, ctx: AppContext) => {
|
||||
await limiterLoginBruteForce(ctx.ip);
|
||||
await limiterLoginBruteForce(userIp(ctx));
|
||||
|
||||
try {
|
||||
const body = await formParse(ctx.req);
|
||||
|
||||
@@ -137,6 +137,14 @@ export const postHandlers: PostHandlers = {
|
||||
//
|
||||
// - The public config is under packages/server/stripeConfig.json
|
||||
// - The private config is in the server .env file
|
||||
//
|
||||
// # Failed Stripe cli login
|
||||
//
|
||||
// If the tool show this error, with code "api_key_expired":
|
||||
//
|
||||
// > FATAL Error while authenticating with Stripe: Authorization failed
|
||||
//
|
||||
// Need to logout and login again to refresh the CLI token - `stripe logout && stripe login`
|
||||
|
||||
webhook: async (stripe: Stripe, _path: SubPath, ctx: AppContext, event: Stripe.Event = null, logErrors: boolean = true) => {
|
||||
event = event ? event : await stripeEvent(stripe, ctx.req);
|
||||
@@ -426,7 +434,7 @@ const getHandlers: Record<string, StripeRouteHandler> = {
|
||||
<body>
|
||||
<button id="checkout">Subscribe</button>
|
||||
<script>
|
||||
var PRICE_ID = ${basicPrice.id};
|
||||
var PRICE_ID = ${JSON.stringify(basicPrice.id)};
|
||||
|
||||
function handleResult() {
|
||||
console.info('Redirected to checkout');
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { makeUrl, redirect, SubPath, UrlType } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { ErrorBadRequest, ErrorForbidden } from '../../utils/errors';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { makeTableView, Row, Table } from '../../utils/views/table';
|
||||
import { yesOrNo } from '../../utils/strings';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import { createCsrfTag } from '../../utils/csrf';
|
||||
import { RunType } from '../../services/TaskService';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { NotificationLevel } from '../../services/database/types';
|
||||
|
||||
const router: Router = new Router(RouteType.Web);
|
||||
|
||||
router.post('tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
const user = ctx.joplin.owner;
|
||||
if (!user.is_admin) throw new ErrorForbidden();
|
||||
|
||||
const taskService = ctx.joplin.services.tasks;
|
||||
const fields: any = await bodyFields(ctx.req);
|
||||
|
||||
if (fields.startTaskButton) {
|
||||
const errors: Error[] = [];
|
||||
|
||||
for (const k of Object.keys(fields)) {
|
||||
if (k.startsWith('checkbox_')) {
|
||||
const taskId = k.substr(9);
|
||||
try {
|
||||
void taskService.runTask(taskId, RunType.Manual);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
await ctx.joplin.models.notification().add(
|
||||
user.id,
|
||||
NotificationKey.Any,
|
||||
NotificationLevel.Error,
|
||||
`Some tasks could not be started: ${errors.join('. ')}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new ErrorBadRequest('Invalid action');
|
||||
}
|
||||
|
||||
return redirect(ctx, makeUrl(UrlType.Tasks));
|
||||
});
|
||||
|
||||
router.get('tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
const user = ctx.joplin.owner;
|
||||
if (!user.is_admin) throw new ErrorForbidden();
|
||||
|
||||
const taskService = ctx.joplin.services.tasks;
|
||||
|
||||
const taskRows: Row[] = [];
|
||||
for (const [taskId, task] of Object.entries(taskService.tasks)) {
|
||||
const state = taskService.taskState(taskId);
|
||||
|
||||
taskRows.push([
|
||||
{
|
||||
value: `checkbox_${taskId}`,
|
||||
checkbox: true,
|
||||
},
|
||||
{
|
||||
value: taskId,
|
||||
},
|
||||
{
|
||||
value: task.description,
|
||||
},
|
||||
{
|
||||
value: task.schedule,
|
||||
},
|
||||
{
|
||||
value: yesOrNo(state.running),
|
||||
},
|
||||
{
|
||||
value: state.lastRunTime ? formatDateTime(state.lastRunTime) : '-',
|
||||
},
|
||||
{
|
||||
value: state.lastCompletionTime ? formatDateTime(state.lastCompletionTime) : '-',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const table: Table = {
|
||||
headers: [
|
||||
{
|
||||
name: 'select',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'schedule',
|
||||
label: 'Schedule',
|
||||
},
|
||||
{
|
||||
name: 'running',
|
||||
label: 'Running',
|
||||
},
|
||||
{
|
||||
name: 'lastRunTime',
|
||||
label: 'Last Run',
|
||||
},
|
||||
{
|
||||
name: 'lastCompletionTime',
|
||||
label: 'Last Completion',
|
||||
},
|
||||
],
|
||||
rows: taskRows,
|
||||
};
|
||||
|
||||
return {
|
||||
...defaultView('tasks', 'Tasks'),
|
||||
content: {
|
||||
itemTable: makeTableView(table),
|
||||
postUrl: makeUrl(UrlType.Tasks),
|
||||
csrfTag: await createCsrfTag(ctx),
|
||||
},
|
||||
cssFiles: ['index/tasks'],
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -15,7 +15,7 @@ import uuidgen from '../../utils/uuidgen';
|
||||
import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings';
|
||||
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
|
||||
import { yesNoDefaultOptions, yesNoOptions } from '../../utils/views/select';
|
||||
import { confirmUrl } from '../../utils/urlUtils';
|
||||
import { confirmUrl, stripePortalUrl } from '../../utils/urlUtils';
|
||||
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
||||
import { createCsrfTag } from '../../utils/csrf';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
@@ -175,6 +175,7 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
||||
view.content.canShareFolderOptions = yesNoDefaultOptions(user, 'can_share_folder');
|
||||
view.content.canUploadOptions = yesNoOptions(user, 'can_upload');
|
||||
view.content.userFlags = userFlags;
|
||||
view.content.stripePortalUrl = stripePortalUrl();
|
||||
|
||||
view.jsFiles.push('zxcvbn');
|
||||
view.cssFiles.push('index/user');
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { Routers } from '../utils/routeUtils';
|
||||
|
||||
import apiBatch from './api/batch';
|
||||
import apiBatchItems from './api/batch_items';
|
||||
import apiDebug from './api/debug';
|
||||
import apiEvents from './api/events';
|
||||
import apiBatchItems from './api/batch_items';
|
||||
import apiItems from './api/items';
|
||||
import apiPing from './api/ping';
|
||||
import apiSessions from './api/sessions';
|
||||
import apiUsers from './api/users';
|
||||
import apiShares from './api/shares';
|
||||
import apiShareUsers from './api/share_users';
|
||||
import apiUsers from './api/users';
|
||||
|
||||
import indexChanges from './index/changes';
|
||||
import indexHelp from './index/help';
|
||||
import indexHome from './index/home';
|
||||
import indexItems from './index/items';
|
||||
import indexLogin from './index/login';
|
||||
import indexLogout from './index/logout';
|
||||
import indexNotifications from './index/notifications';
|
||||
import indexPassword from './index/password';
|
||||
import indexSignup from './index/signup';
|
||||
import indexShares from './index/shares';
|
||||
import indexUsers from './index/users';
|
||||
import indexStripe from './index/stripe';
|
||||
import indexTerms from './index/terms';
|
||||
import indexPrivacy from './index/privacy';
|
||||
import indexShares from './index/shares';
|
||||
import indexSignup from './index/signup';
|
||||
import indexStripe from './index/stripe';
|
||||
import indexTasks from './index/tasks';
|
||||
import indexTerms from './index/terms';
|
||||
import indexUpgrade from './index/upgrade';
|
||||
import indexHelp from './index/help';
|
||||
import indexUsers from './index/users';
|
||||
|
||||
import defaultRoute from './default';
|
||||
|
||||
@@ -56,6 +57,7 @@ const routes: Routers = {
|
||||
'privacy': indexPrivacy,
|
||||
'upgrade': indexUpgrade,
|
||||
'help': indexHelp,
|
||||
'tasks': indexTasks,
|
||||
|
||||
'': defaultRoute,
|
||||
};
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import BaseService from './BaseService';
|
||||
const cron = require('node-cron');
|
||||
|
||||
const logger = Logger.create('cron');
|
||||
|
||||
async function runCronTask(name: string, callback: Function) {
|
||||
const startTime = Date.now();
|
||||
logger.info(`Running task "${name}"`);
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
logger.error(`On task "${name}"`, error);
|
||||
}
|
||||
logger.info(`Completed task "${name}" in ${Date.now() - startTime}ms`);
|
||||
}
|
||||
|
||||
export default class CronService extends BaseService {
|
||||
|
||||
public async runInBackground() {
|
||||
cron.schedule('0 */6 * * *', async () => {
|
||||
await runCronTask('deleteExpiredTokens', async () => this.models.token().deleteExpiredTokens());
|
||||
});
|
||||
|
||||
cron.schedule('0 * * * *', async () => {
|
||||
await runCronTask('updateTotalSizes', async () => this.models.item().updateTotalSizes());
|
||||
});
|
||||
|
||||
cron.schedule('0 12 * * *', async () => {
|
||||
await runCronTask('handleBetaUserEmails', async () => this.models.user().handleBetaUserEmails());
|
||||
});
|
||||
|
||||
cron.schedule('0 13 * * *', async () => {
|
||||
await runCronTask('handleFailedPaymentSubscriptions', async () => this.models.user().handleFailedPaymentSubscriptions());
|
||||
});
|
||||
|
||||
cron.schedule('0 14 * * *', async () => {
|
||||
await runCronTask('handleOversizedAccounts', async () => this.models.user().handleOversizedAccounts());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import config from '../config';
|
||||
import { Models } from '../models/factory';
|
||||
import { afterAllTests, beforeAllDb, beforeEachDb, expectThrow, models, msleep } from '../utils/testing/testUtils';
|
||||
import { Env } from '../utils/types';
|
||||
import TaskService, { RunType, Task } from './TaskService';
|
||||
|
||||
const newService = () => {
|
||||
return new TaskService(Env.Dev, models(), config());
|
||||
};
|
||||
|
||||
describe('TaskService', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('TaskService');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should register a task', async function() {
|
||||
const service = newService();
|
||||
|
||||
const task: Task = {
|
||||
id: 'test',
|
||||
description: '',
|
||||
run: (_models: Models) => {},
|
||||
schedule: '',
|
||||
};
|
||||
|
||||
service.registerTask(task);
|
||||
|
||||
expect(service.tasks['test']).toBeTruthy();
|
||||
await expectThrow(async () => service.registerTask(task));
|
||||
});
|
||||
|
||||
test('should run a task', async function() {
|
||||
const service = newService();
|
||||
|
||||
let finishTask = false;
|
||||
let taskHasRan = false;
|
||||
|
||||
const task: Task = {
|
||||
id: 'test',
|
||||
description: '',
|
||||
run: async (_models: Models) => {
|
||||
const iid = setInterval(() => {
|
||||
if (finishTask) {
|
||||
clearInterval(iid);
|
||||
taskHasRan = true;
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
schedule: '',
|
||||
};
|
||||
|
||||
service.registerTask(task);
|
||||
|
||||
expect(service.taskState('test').running).toBe(false);
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
void service.runTask('test', RunType.Manual);
|
||||
expect(service.taskState('test').running).toBe(true);
|
||||
expect(service.taskState('test').lastCompletionTime).toBeFalsy();
|
||||
expect(service.taskState('test').lastRunTime.getTime()).toBeGreaterThanOrEqual(startTime.getTime());
|
||||
|
||||
await msleep(1);
|
||||
finishTask = true;
|
||||
await msleep(3);
|
||||
|
||||
expect(taskHasRan).toBe(true);
|
||||
expect(service.taskState('test').running).toBe(false);
|
||||
expect(service.taskState('test').lastCompletionTime.getTime()).toBeGreaterThan(startTime.getTime());
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import { Models } from '../models/factory';
|
||||
import BaseService from './BaseService';
|
||||
const cron = require('node-cron');
|
||||
|
||||
const logger = Logger.create('TaskService');
|
||||
|
||||
type TaskId = string;
|
||||
|
||||
export enum RunType {
|
||||
Scheduled = 1,
|
||||
Manual = 2,
|
||||
}
|
||||
|
||||
const runTypeToString = (runType: RunType) => {
|
||||
if (runType === RunType.Scheduled) return 'scheduled';
|
||||
if (runType === RunType.Manual) return 'manual';
|
||||
throw new Error(`Unknown run type: ${runType}`);
|
||||
};
|
||||
|
||||
export interface Task {
|
||||
id: TaskId;
|
||||
description: string;
|
||||
schedule: string;
|
||||
run(models: Models): void;
|
||||
}
|
||||
|
||||
export type Tasks = Record<TaskId, Task>;
|
||||
|
||||
interface TaskState {
|
||||
running: boolean;
|
||||
lastRunTime: Date;
|
||||
lastCompletionTime: Date;
|
||||
}
|
||||
|
||||
const defaultTaskState: TaskState = {
|
||||
running: false,
|
||||
lastRunTime: null,
|
||||
lastCompletionTime: null,
|
||||
};
|
||||
|
||||
export default class TaskService extends BaseService {
|
||||
|
||||
private tasks_: Tasks = {};
|
||||
private taskStates_: Record<TaskId, TaskState> = {};
|
||||
|
||||
public registerTask(task: Task) {
|
||||
if (this.tasks_[task.id]) throw new Error(`Already a task with this ID: ${task.id}`);
|
||||
this.tasks_[task.id] = task;
|
||||
this.taskStates_[task.id] = { ...defaultTaskState };
|
||||
}
|
||||
|
||||
public registerTasks(tasks: Task[]) {
|
||||
for (const task of tasks) this.registerTask(task);
|
||||
}
|
||||
|
||||
public get tasks(): Tasks {
|
||||
return this.tasks_;
|
||||
}
|
||||
|
||||
public taskState(id: TaskId): TaskState {
|
||||
if (!this.taskStates_[id]) throw new Error(`No such task: ${id}`);
|
||||
return this.taskStates_[id];
|
||||
}
|
||||
|
||||
// TODO: add tests
|
||||
|
||||
public async runTask(id: TaskId, runType: RunType) {
|
||||
const state = this.taskState(id);
|
||||
if (state.running) throw new Error(`Task is already running: ${id}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
this.taskStates_[id] = {
|
||||
...this.taskStates_[id],
|
||||
running: true,
|
||||
lastRunTime: new Date(),
|
||||
};
|
||||
|
||||
try {
|
||||
logger.info(`Running "${id}" (${runTypeToString(runType)})...`);
|
||||
await this.tasks_[id].run(this.models);
|
||||
} catch (error) {
|
||||
logger.error(`On task "${id}"`, error);
|
||||
}
|
||||
|
||||
this.taskStates_[id] = {
|
||||
...this.taskStates_[id],
|
||||
running: false,
|
||||
lastCompletionTime: new Date(),
|
||||
};
|
||||
|
||||
logger.info(`Completed "${id}" in ${Date.now() - startTime}ms`);
|
||||
}
|
||||
|
||||
public async runInBackground() {
|
||||
for (const [taskId, task] of Object.entries(this.tasks_)) {
|
||||
if (!task.schedule) continue;
|
||||
|
||||
logger.info(`Scheduling task "${taskId}": ${task.schedule}`);
|
||||
|
||||
cron.schedule(task.schedule, async () => {
|
||||
await this.runTask(taskId, RunType.Scheduled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export enum ItemAddressingType {
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
Error = 5,
|
||||
Important = 10,
|
||||
Normal = 20,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import CronService from './CronService';
|
||||
import EmailService from './EmailService';
|
||||
import MustacheService from './MustacheService';
|
||||
import ShareService from './ShareService';
|
||||
import TaskService from './TaskService';
|
||||
|
||||
export interface Services {
|
||||
share: ShareService;
|
||||
email: EmailService;
|
||||
cron: CronService;
|
||||
mustache: MustacheService;
|
||||
tasks: TaskService;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { DbConnection, dropTables, migrateLatest } from '../db';
|
||||
import newModelFactory from '../models/factory';
|
||||
import { AccountType } from '../models/UserModel';
|
||||
import { UserFlagType } from '../services/database/types';
|
||||
import { User, UserFlagType } from '../services/database/types';
|
||||
import { Config } from '../utils/types';
|
||||
|
||||
export interface CreateTestUsersOptions {
|
||||
count?: number;
|
||||
fromNum?: number;
|
||||
}
|
||||
|
||||
export async function handleDebugCommands(argv: any, db: DbConnection, config: Config): Promise<boolean> {
|
||||
if (argv.debugCreateTestUsers) {
|
||||
await createTestUsers(db, config);
|
||||
@@ -14,51 +19,79 @@ export async function handleDebugCommands(argv: any, db: DbConnection, config: C
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function createTestUsers(db: DbConnection, config: Config) {
|
||||
export async function clearDatabase(db: DbConnection) {
|
||||
await dropTables(db);
|
||||
await migrateLatest(db);
|
||||
}
|
||||
|
||||
export async function createTestUsers(db: DbConnection, config: Config, options: CreateTestUsersOptions = null) {
|
||||
options = {
|
||||
count: 0,
|
||||
fromNum: 1,
|
||||
...options,
|
||||
};
|
||||
|
||||
const password = 'hunter1hunter2hunter3';
|
||||
const models = newModelFactory(db, config);
|
||||
|
||||
for (let userNum = 1; userNum <= 2; userNum++) {
|
||||
await models.user().save({
|
||||
email: `user${userNum}@example.com`,
|
||||
password,
|
||||
full_name: `User ${userNum}`,
|
||||
});
|
||||
}
|
||||
if (options.count) {
|
||||
const models = newModelFactory(db, config);
|
||||
|
||||
{
|
||||
const { user } = await models.subscription().saveUserAndSubscription(
|
||||
'usersub@example.com',
|
||||
'With Sub',
|
||||
AccountType.Basic,
|
||||
'usr_111',
|
||||
'sub_111'
|
||||
);
|
||||
await models.user().save({ id: user.id, password });
|
||||
}
|
||||
const users: User[] = [];
|
||||
|
||||
{
|
||||
const { user, subscription } = await models.subscription().saveUserAndSubscription(
|
||||
'userfailedpayment@example.com',
|
||||
'Failed Payment',
|
||||
AccountType.Basic,
|
||||
'usr_222',
|
||||
'sub_222'
|
||||
);
|
||||
await models.user().save({ id: user.id, password });
|
||||
await models.subscription().handlePayment(subscription.stripe_subscription_id, false);
|
||||
}
|
||||
for (let i = 0; i < options.count; i++) {
|
||||
const userNum = i + options.fromNum;
|
||||
users.push({
|
||||
email: `user${userNum}@example.com`,
|
||||
password,
|
||||
full_name: `User ${userNum}`,
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const user = await models.user().save({
|
||||
email: 'userwithflags@example.com',
|
||||
password,
|
||||
full_name: 'User Withflags',
|
||||
});
|
||||
await models.user().saveMulti(users);
|
||||
} else {
|
||||
await dropTables(db);
|
||||
await migrateLatest(db);
|
||||
const models = newModelFactory(db, config);
|
||||
|
||||
await models.userFlag().add(user.id, UserFlagType.AccountOverLimit);
|
||||
for (let userNum = 1; userNum <= 2; userNum++) {
|
||||
await models.user().save({
|
||||
email: `user${userNum}@example.com`,
|
||||
password,
|
||||
full_name: `User ${userNum}`,
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const { user } = await models.subscription().saveUserAndSubscription(
|
||||
'usersub@example.com',
|
||||
'With Sub',
|
||||
AccountType.Basic,
|
||||
'usr_111',
|
||||
'sub_111'
|
||||
);
|
||||
await models.user().save({ id: user.id, password });
|
||||
}
|
||||
|
||||
{
|
||||
const { user, subscription } = await models.subscription().saveUserAndSubscription(
|
||||
'userfailedpayment@example.com',
|
||||
'Failed Payment',
|
||||
AccountType.Basic,
|
||||
'usr_222',
|
||||
'sub_222'
|
||||
);
|
||||
await models.user().save({ id: user.id, password });
|
||||
await models.subscription().handlePayment(subscription.stripe_subscription_id, false);
|
||||
}
|
||||
|
||||
{
|
||||
const user = await models.user().save({
|
||||
email: 'userwithflags@example.com',
|
||||
password,
|
||||
full_name: 'User Withflags',
|
||||
});
|
||||
|
||||
await models.userFlag().add(user.id, UserFlagType.AccountOverLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
interface TransactionInfo {
|
||||
name: string;
|
||||
index: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// This transaction handler allows abstracting away the complexity of managing nested transactions
|
||||
// within models.
|
||||
// Any method in a model can start a transaction and, if one is already started, it
|
||||
@@ -9,7 +15,7 @@ import { DbConnection } from '../db';
|
||||
// Set logEnabled_ to `true` to see what happens with nested transactions.
|
||||
export default class TransactionHandler {
|
||||
|
||||
private transactionStack_: number[] = [];
|
||||
private transactionStack_: TransactionInfo[] = [];
|
||||
private activeTransaction_: Knex.Transaction = null;
|
||||
private transactionIndex_: number = 0;
|
||||
private logEnabled_: boolean = false;
|
||||
@@ -36,7 +42,15 @@ export default class TransactionHandler {
|
||||
return this.activeTransaction_;
|
||||
}
|
||||
|
||||
public async start(): Promise<number> {
|
||||
public get stackInfo(): string {
|
||||
const output: string[] = [];
|
||||
for (const t of this.transactionStack_) {
|
||||
output.push(`#${t.index}: ${t.name}: ${t.timestamp.toUTCString()}`);
|
||||
}
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
public async start(name: string): Promise<number> {
|
||||
const txIndex = ++this.transactionIndex_;
|
||||
this.log(`Starting transaction: ${txIndex}`);
|
||||
|
||||
@@ -47,14 +61,19 @@ export default class TransactionHandler {
|
||||
this.log(`Got transaction: ${txIndex}`);
|
||||
}
|
||||
|
||||
this.transactionStack_.push(txIndex);
|
||||
this.transactionStack_.push({
|
||||
name,
|
||||
index: txIndex,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
return txIndex;
|
||||
}
|
||||
|
||||
private finishTransaction(txIndex: number): boolean {
|
||||
if (!this.transactionStack_.length) throw new Error('Committing but no transaction was started');
|
||||
const lastTxIndex = this.transactionStack_.pop();
|
||||
if (lastTxIndex !== txIndex) throw new Error(`Committing a transaction but was not last to start one: ${txIndex}. Expected: ${lastTxIndex}`);
|
||||
const lastTx = this.transactionStack_.pop();
|
||||
if (lastTx.index !== txIndex) throw new Error(`Committing a transaction but was not last to start one: ${txIndex}. Expected: ${lastTx.index}`);
|
||||
return !this.transactionStack_.length;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,3 +70,8 @@ export function contextSessionId(ctx: AppContext, throwIfNotFound = true): strin
|
||||
export function isApiRequest(ctx: AppContext): boolean {
|
||||
return ctx.path.indexOf('/api/') === 0;
|
||||
}
|
||||
|
||||
export function userIp(ctx: AppContext): string {
|
||||
if (ctx.headers['x-real-ip']) return ctx.headers['x-real-ip'];
|
||||
return ctx.ip;
|
||||
}
|
||||
|
||||
@@ -271,6 +271,7 @@ export enum UrlType {
|
||||
Login = 'login',
|
||||
Terms = 'terms',
|
||||
Privacy = 'privacy',
|
||||
Tasks = 'tasks',
|
||||
}
|
||||
|
||||
export function makeUrl(urlType: UrlType): string {
|
||||
|
||||
@@ -7,15 +7,15 @@ import routes from '../routes/routes';
|
||||
import ShareService from '../services/ShareService';
|
||||
import { Services } from '../services/types';
|
||||
import EmailService from '../services/EmailService';
|
||||
import CronService from '../services/CronService';
|
||||
import MustacheService from '../services/MustacheService';
|
||||
import setupTaskService from './setupTaskService';
|
||||
|
||||
async function setupServices(env: Env, models: Models, config: Config): Promise<Services> {
|
||||
const output: Services = {
|
||||
share: new ShareService(env, models, config),
|
||||
email: new EmailService(env, models, config),
|
||||
cron: new CronService(env, models, config),
|
||||
mustache: new MustacheService(config.viewDir, config.baseUrl),
|
||||
tasks: setupTaskService(env, models, config),
|
||||
};
|
||||
|
||||
await output.mustache.loadPartials();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Models } from '../models/factory';
|
||||
import TaskService, { Task } from '../services/TaskService';
|
||||
import { Config, Env } from './types';
|
||||
|
||||
export default function(env: Env, models: Models, config: Config): TaskService {
|
||||
const taskService = new TaskService(env, models, config);
|
||||
|
||||
let tasks: Task[] = [
|
||||
{
|
||||
id: 'deleteExpiredTokens',
|
||||
description: 'Delete expired tokens',
|
||||
schedule: '0 */6 * * *',
|
||||
run: (models: Models) => models.token().deleteExpiredTokens(),
|
||||
},
|
||||
{
|
||||
id: 'updateTotalSizes',
|
||||
description: 'Update total sizes',
|
||||
schedule: '0 * * * *',
|
||||
run: (models: Models) => models.item().updateTotalSizes(),
|
||||
},
|
||||
{
|
||||
id: 'handleOversizedAccounts',
|
||||
description: 'Process oversized accounts',
|
||||
schedule: '0 14 * * *',
|
||||
run: (models: Models) => models.user().handleOversizedAccounts(),
|
||||
},
|
||||
];
|
||||
|
||||
if (config.isJoplinCloud) {
|
||||
tasks = tasks.concat([
|
||||
{
|
||||
id: 'handleBetaUserEmails',
|
||||
description: 'Process beta user emails',
|
||||
schedule: '0 12 * * *',
|
||||
run: (models: Models) => models.user().handleBetaUserEmails(),
|
||||
},
|
||||
{
|
||||
id: 'handleFailedPaymentSubscriptions',
|
||||
description: 'Process failed payment subscriptions',
|
||||
schedule: '0 13 * * *',
|
||||
run: (models: Models) => models.user().handleFailedPaymentSubscriptions(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
taskService.registerTasks(tasks);
|
||||
|
||||
return taskService;
|
||||
}
|
||||
@@ -3,5 +3,5 @@ import { Services } from '../services/types';
|
||||
export default async function startServices(services: Services) {
|
||||
void services.share.runInBackground();
|
||||
void services.email.runInBackground();
|
||||
void services.cron.runInBackground();
|
||||
void services.tasks.runInBackground();
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export function msleep(ms: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDateTime(ms: number): string {
|
||||
export function formatDateTime(ms: number | Date): string {
|
||||
ms = ms instanceof Date ? ms.getTime() : ms;
|
||||
return `${dayjs(ms).format('D MMM YY HH:mm:ss')} (${defaultTimezone()})`;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export enum Env {
|
||||
export interface NotificationView {
|
||||
id: Uuid;
|
||||
messageHtml: string;
|
||||
level: string;
|
||||
levelClassName: string;
|
||||
closeUrl: string;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface AppContext extends Koa.Context {
|
||||
}
|
||||
|
||||
export enum DatabaseConfigClient {
|
||||
Null = 'null',
|
||||
PostgreSQL = 'pg',
|
||||
SQLite = 'sqlite3',
|
||||
}
|
||||
@@ -64,6 +65,8 @@ export interface DatabaseConfig {
|
||||
user?: string;
|
||||
password?: string;
|
||||
asyncStackTraces?: boolean;
|
||||
slowQueryLogEnabled?: boolean;
|
||||
slowQueryLogMinDuration?: number;
|
||||
}
|
||||
|
||||
export interface MailerConfig {
|
||||
|
||||
@@ -4,11 +4,13 @@ import { setQueryParameters } from '../urlUtils';
|
||||
const defaultSortOrder = PaginationOrderDir.ASC;
|
||||
|
||||
function headerIsSelectedClass(name: string, pagination: Pagination): string {
|
||||
if (!pagination) return '';
|
||||
const orderBy = pagination.order[0].by;
|
||||
return name === orderBy ? 'is-selected' : '';
|
||||
}
|
||||
|
||||
function headerSortIconDir(name: string, pagination: Pagination): string {
|
||||
if (!pagination) return '';
|
||||
const orderBy = pagination.order[0].by;
|
||||
const orderDir = orderBy === name ? pagination.order[0].dir : defaultSortOrder;
|
||||
return orderDir === PaginationOrderDir.ASC ? 'up' : 'down';
|
||||
@@ -35,6 +37,7 @@ interface HeaderView {
|
||||
|
||||
interface RowItem {
|
||||
value: string;
|
||||
checkbox?: boolean;
|
||||
url?: string;
|
||||
stretch?: boolean;
|
||||
}
|
||||
@@ -45,6 +48,7 @@ interface RowItemView {
|
||||
value: string;
|
||||
classNames: string[];
|
||||
url: string;
|
||||
checkbox: boolean;
|
||||
}
|
||||
|
||||
type RowView = RowItemView[];
|
||||
@@ -52,10 +56,10 @@ type RowView = RowItemView[];
|
||||
export interface Table {
|
||||
headers: Header[];
|
||||
rows: Row[];
|
||||
baseUrl: string;
|
||||
requestQuery: any;
|
||||
pageCount: number;
|
||||
pagination: Pagination;
|
||||
baseUrl?: string;
|
||||
requestQuery?: any;
|
||||
pageCount?: number;
|
||||
pagination?: Pagination;
|
||||
}
|
||||
|
||||
export interface TableView {
|
||||
@@ -77,7 +81,7 @@ export function makeTablePagination(query: any, defaultOrderField: string, defau
|
||||
function makeHeaderView(header: Header, parentBaseUrl: string, baseUrlQuery: PaginationQueryParams, pagination: Pagination): HeaderView {
|
||||
return {
|
||||
label: header.label,
|
||||
sortLink: setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'order_by': header.name, 'order_dir': headerNextOrder(header.name, pagination) }),
|
||||
sortLink: !pagination ? null : setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'order_by': header.name, 'order_dir': headerNextOrder(header.name, pagination) }),
|
||||
classNames: [header.stretch ? 'stretch' : 'nowrap', headerIsSelectedClass(header.name, pagination)],
|
||||
iconDir: headerSortIconDir(header.name, pagination),
|
||||
};
|
||||
@@ -89,14 +93,21 @@ function makeRowView(row: Row): RowView {
|
||||
value: rowItem.value,
|
||||
classNames: [rowItem.stretch ? 'stretch' : 'nowrap'],
|
||||
url: rowItem.url,
|
||||
checkbox: rowItem.checkbox,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeTableView(table: Table): TableView {
|
||||
const baseUrlQuery = filterPaginationQueryParams(table.requestQuery);
|
||||
const pagination = table.pagination;
|
||||
const paginationLinks = createPaginationLinks(pagination.page, table.pageCount, setQueryParameters(table.baseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
||||
let paginationLinks: PageLink[] = [];
|
||||
let baseUrlQuery: PaginationQueryParams = null;
|
||||
let pagination: Pagination = null;
|
||||
|
||||
if (table.pageCount) {
|
||||
baseUrlQuery = filterPaginationQueryParams(table.requestQuery);
|
||||
pagination = table.pagination;
|
||||
paginationLinks = createPaginationLinks(pagination.page, table.pageCount, setQueryParameters(table.baseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
||||
}
|
||||
|
||||
return {
|
||||
headers: table.headers.map(h => makeHeaderView(h, table.baseUrl, baseUrlQuery, pagination)),
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
|
||||
Most of your details can be found in your Profile page. To open it, click on the Profile button - this is the button in the top right corner, with your name or email on it.
|
||||
|
||||
## How can I cancel my account?
|
||||
|
||||
Click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Cancel subscription".
|
||||
|
||||
## How can I get more space?
|
||||
|
||||
If you are on a Basic account, you may upgrade to a Pro account to get more space. Click on the [Profile button](#how-can-i-change-my-details), then scroll down and select "Upgrade account".
|
||||
|
||||
If you are already on a Pro account, and you need more space for specific reasons, please contact us as we may increase the cap in some cases.
|
||||
|
||||
## How can I manage my payment details?
|
||||
|
||||
To update your card or other payment details, click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Manage payment details".
|
||||
|
||||
## How can I cancel my account?
|
||||
|
||||
Click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Cancel subscription".
|
||||
|
||||
## Further information
|
||||
|
||||
- [Joplin Offical Website](https://joplinapp.org)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<form method='POST' action="{{postUrl}}">
|
||||
{{{csrfTag}}}
|
||||
|
||||
{{#itemTable}}
|
||||
{{>table}}
|
||||
{{/itemTable}}
|
||||
|
||||
<div class="block">
|
||||
<input class="button is-link" type="submit" value="Start selected tasks" name="startTaskButton"/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -131,6 +131,9 @@
|
||||
{{#showUpdateSubscriptionPro}}
|
||||
<a href="{{{global.baseUrl}}}/upgrade" class="button is-warning block">Upgrade to Pro</a>
|
||||
{{/showUpdateSubscriptionPro}}
|
||||
{{#showCancelSubscription}}
|
||||
<p class="block"><a href="{{stripePortalUrl}}">Manage payment details</a></p>
|
||||
{{/showCancelSubscription}}
|
||||
{{#showCancelSubscription}}
|
||||
<p id="user_cancel_subscription_link" class="block"><a href="#">Cancel subscription</a></p>
|
||||
<input type="submit" id="user_cancel_subscription_button" name="user_cancel_subscription_button" class="button is-danger" value="Cancel subscription" />
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
{{/global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/items">Items</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">Log</a>
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/tasks">Tasks</a>
|
||||
{{/global.owner.is_admin}}
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
{{#global.isJoplinCloud}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#global.hasNotifications}}
|
||||
{{#global.notifications}}
|
||||
<div class="notification is-{{level}}" id="notification-{{id}}">
|
||||
<div class="notification {{levelClassName}}" id="notification-{{id}}">
|
||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||
{{{messageHtml}}}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{#headers}}
|
||||
{{>tableHeader}}
|
||||
{{/headers}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#rows}}
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable ">
|
||||
<thead>
|
||||
<tr>
|
||||
{{#.}}
|
||||
{{>tableRowItem}}
|
||||
{{/.}}
|
||||
{{#headers}}
|
||||
{{>tableHeader}}
|
||||
{{/headers}}
|
||||
</tr>
|
||||
{{/rows}}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#rows}}
|
||||
<tr>
|
||||
{{#.}}
|
||||
{{>tableRowItem}}
|
||||
{{/.}}
|
||||
</tr>
|
||||
{{/rows}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{>pagination}}
|
||||
@@ -1,3 +1,8 @@
|
||||
<th class="{{#classNames}}{{.}} {{/classNames}}">
|
||||
<a href="{{sortLink}}" class="sort-button">{{label}} <i class="fas fa-caret-{{iconDir}}"></i></a>
|
||||
{{#sortLink}}
|
||||
<a href="{{sortLink}}" class="sort-button">{{label}} <i class="fas fa-caret-{{iconDir}}"></i></a>
|
||||
{{/sortLink}}
|
||||
{{^sortLink}}
|
||||
{{label}}
|
||||
{{/sortLink}}
|
||||
</th>
|
||||
@@ -1,3 +1,8 @@
|
||||
<td class="{{#classNames}}{{.}} {{/classNames}}">
|
||||
{{#url}}<a href="{{.}}"></span>{{/url}}{{value}}</a>
|
||||
{{#checkbox}}
|
||||
<input type="checkbox" name="{{value}}"/>
|
||||
{{/checkbox}}
|
||||
{{^checkbox}}
|
||||
{{#url}}<a href="{{.}}"></span>{{/url}}{{value}}</a>
|
||||
{{/checkbox}}
|
||||
</td>
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
.encryption-config-test > .item {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.min.css.map */
|
||||
@@ -1,8 +1,6 @@
|
||||
import { execCommand2, rootDir } from './tool-utils';
|
||||
import * as moment from 'moment';
|
||||
|
||||
const DockerImageName = 'joplin/server';
|
||||
|
||||
function getVersionFromTag(tagName: string, isPreRelease: boolean): string {
|
||||
if (tagName.indexOf('server-') !== 0) throw new Error(`Invalid tag: ${tagName}`);
|
||||
const s = tagName.split('-');
|
||||
@@ -14,10 +12,6 @@ function getIsPreRelease(tagName: string): boolean {
|
||||
return tagName.indexOf('-beta') > 0;
|
||||
}
|
||||
|
||||
function normalizePlatform(platform: string) {
|
||||
return platform.replace(/\//g, '-');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const argv = require('yargs').argv;
|
||||
if (!argv.tagName) throw new Error('--tag-name not provided');
|
||||
@@ -33,11 +27,7 @@ async function main() {
|
||||
} catch (error) {
|
||||
console.info('Could not get git commit: metadata revision field will be empty');
|
||||
}
|
||||
const buildArgs = [
|
||||
`--build-arg BUILD_DATE="${buildDate}"`,
|
||||
`--build-arg REVISION="${revision}"`,
|
||||
`--build-arg VERSION="${imageVersion}"`,
|
||||
];
|
||||
const buildArgs = `--build-arg BUILD_DATE="${buildDate}" --build-arg REVISION="${revision}" --build-arg VERSION="${imageVersion}"`;
|
||||
const dockerTags: string[] = [];
|
||||
const versionPart = imageVersion.split('.');
|
||||
dockerTags.push(isPreRelease ? 'beta' : 'latest');
|
||||
@@ -54,48 +44,10 @@ async function main() {
|
||||
console.info('isPreRelease:', isPreRelease);
|
||||
console.info('Docker tags:', dockerTags.join(', '));
|
||||
|
||||
const platforms = [
|
||||
'linux/amd64',
|
||||
'linux/arm64',
|
||||
'linux/arm/v7',
|
||||
];
|
||||
|
||||
// this will build a bunch of local image tags named: ${imageVersion}-${platform} with the slashes replaced with dashes
|
||||
for (const platform of platforms) {
|
||||
const normalizedPlatform = normalizePlatform(platform);
|
||||
await execCommand2([
|
||||
'docker', 'build',
|
||||
'--platform', platform,
|
||||
'-t', `${DockerImageName}:${imageVersion}-${normalizedPlatform}`,
|
||||
...buildArgs,
|
||||
'-f', 'Dockerfile.server',
|
||||
'.',
|
||||
]);
|
||||
if (pushImages) {
|
||||
await execCommand2([
|
||||
'docker', 'push', `${DockerImageName}:${imageVersion}-${normalizedPlatform}`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// now we have to create the right manifests and push them
|
||||
if (pushImages) {
|
||||
for (const tag of dockerTags) {
|
||||
// manifest create requires the tags being amended in to exist on the remote, so this all can only happen if pushImages is true
|
||||
const platformArgs: string[] = [];
|
||||
for (const platform in platforms) {
|
||||
platformArgs.concat('--amend', `${DockerImageName}:${imageVersion}-${normalizePlatform(platform)}`);
|
||||
}
|
||||
await execCommand2([
|
||||
'docker', 'manifest', 'create',
|
||||
`${DockerImageName}:${tag}`,
|
||||
...platformArgs,
|
||||
]);
|
||||
await execCommand2([
|
||||
'docker', 'manifest', 'push',
|
||||
`${DockerImageName}:${tag}`,
|
||||
]);
|
||||
}
|
||||
await execCommand2(`docker build -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`);
|
||||
for (const tag of dockerTags) {
|
||||
await execCommand2(`docker tag "joplin/server:${imageVersion}" "joplin/server:${tag}"`);
|
||||
if (pushImages) await execCommand2(`docker push joplin/server:${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const sass = require('sass');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
// The SASS doc claims that renderSync is twice as fast as render, so if speed
|
||||
// turns out to be an issue we could use that instead. The advantage of async is
|
||||
// that we can run complation of each file in parallel (and running other async
|
||||
// gulp tasks in parallel too).
|
||||
|
||||
// sasss.render is old school async, so convert it to a promise here.
|
||||
async function sassRender(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sass.render(options, ((error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = async function compileSass(inputPaths, outputPath) {
|
||||
const promises = [];
|
||||
for (const inputPath of inputPaths) {
|
||||
console.info(`Compiling ${inputPath}...`);
|
||||
|
||||
promises.push(sassRender({
|
||||
file: inputPath,
|
||||
sourceMap: true,
|
||||
outFile: outputPath,
|
||||
}));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const cssString = results.map(r => r.css.toString()).join('\n');
|
||||
const mapString = results.map(r => r.map.toString()).join('\n');
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(outputPath, cssString, 'utf8'),
|
||||
fs.writeFile(`${outputPath}.map`, mapString, 'utf8'),
|
||||
]);
|
||||
|
||||
console.info(`Generated ${outputPath}`);
|
||||
};
|
||||
+441
-522
File diff suppressed because it is too large
Load Diff
+141
-139
@@ -11,14 +11,16 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: jmontane, 2019\n"
|
||||
"Language-Team: jmontane@softcatala.org\n"
|
||||
"Last-Translator: Xavi Ivars <xavi.ivars@gmail.com>\n"
|
||||
"Language-Team: xavivars@softcatala.org\n"
|
||||
"Language: ca\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 3.0\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
|
||||
#: packages/app-cli/app/app-gui.js:452
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
@@ -179,8 +181,8 @@ msgid ""
|
||||
"Duplicates the notes matching <note> to [notebook]. If no notebook is "
|
||||
"specified the note is duplicated in the current notebook."
|
||||
msgstr ""
|
||||
"Duplica les notes que coincideixen amb <note> a [blocdenotes]. Si no "
|
||||
"indiqueu cap bloc de notes es dupliquen en el bloc de notes actual."
|
||||
"Duplica les notes que coincideixen amb <note> a [notebook]. Si no indiqueu "
|
||||
"cap bloc de notes, es dupliquen en el bloc de notes actual."
|
||||
|
||||
#: packages/app-cli/app/command-done.js:14
|
||||
msgid "Marks a to-do as done."
|
||||
@@ -227,7 +229,7 @@ msgstr "Elements desxifrats: %d"
|
||||
#, javascript-format
|
||||
msgid "Skipped items: %d (use --retry-failed-items to retry decrypting them)"
|
||||
msgstr ""
|
||||
"Elements omesos: %d (utilitza --retry-failed-items per tornar a intentar "
|
||||
"Elements omesos: %d (utilitza --retry-failed-items per a tornar a intentar "
|
||||
"desxifrar-los)"
|
||||
|
||||
#: packages/app-cli/app/command-e2ee.js:78
|
||||
@@ -274,8 +276,8 @@ msgstr "Edita la nota."
|
||||
msgid ""
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`"
|
||||
msgstr ""
|
||||
"No hi ha definit cap editor de text. Establiu-ne un usant `config editor "
|
||||
"<editor-path>`"
|
||||
"No hi ha definit cap editor de text. Establiu-ne un usant \"config editor "
|
||||
"<editor-path>\""
|
||||
|
||||
#: packages/app-cli/app/command-edit.js:40
|
||||
msgid "No active notebook."
|
||||
@@ -289,14 +291,14 @@ msgstr "La nota «%s» no existeix. Voleu crear-la?"
|
||||
#: packages/app-cli/app/command-edit.js:75
|
||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr ""
|
||||
"S'està iniciant l'edició del a nota. Tanqueu l'editor per a tornar a "
|
||||
"S'està iniciant l'edició de la nota. Tanqueu l'editor per a tornar a "
|
||||
"l'indicador."
|
||||
|
||||
#: packages/app-cli/app/command-edit.js:82
|
||||
#: packages/app-desktop/commands/startExternalEditing.js:32
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr "S'ha produït un error a l'obrir la nota amb l'editor: %s"
|
||||
msgstr "S'ha produït un error en l'obrir la nota amb l'editor: %s"
|
||||
|
||||
#: packages/app-cli/app/command-edit.js:97
|
||||
msgid "Note has been saved."
|
||||
@@ -329,7 +331,7 @@ msgstr "Exporta només el bloc de notes indicat."
|
||||
|
||||
#: packages/app-cli/app/command-geoloc.js:13
|
||||
msgid "Displays a geolocation URL for the note."
|
||||
msgstr "Motra una URL de geolocalitzacio de la nota."
|
||||
msgstr "Mostra un URL de geolocalització de la nota."
|
||||
|
||||
#: packages/app-cli/app/command-help.js:13
|
||||
msgid "Displays usage information."
|
||||
@@ -364,10 +366,10 @@ msgid ""
|
||||
"using the shortcuts `$n` or `$b` for, respectively, the currently selected "
|
||||
"note or notebook. `$c` can be used to refer to the currently selected item."
|
||||
msgstr ""
|
||||
"En qualsevol ordre, podeu referenciar una nota o bloc de notes per el títol "
|
||||
"o l'ID, o podeu usar dreceres «$n» o «$b» per a, respectivament, la nota o "
|
||||
"el bloc de nota seleccionat. Podeu usar «$c» per a fer referència a "
|
||||
"l'element seleccionat."
|
||||
"En qualsevol ordre, podeu referenciar una nota o bloc de notes pel títol o "
|
||||
"l'ID, o podeu usar dreceres «$n» o «$b» per a, respectivament, la nota o el "
|
||||
"bloc de nota seleccionat. Podeu usar «$c» per a fer referència a l'element "
|
||||
"seleccionat."
|
||||
|
||||
#: packages/app-cli/app/command-help.js:79
|
||||
msgid "To move from one pane to another, press Tab or Shift+Tab."
|
||||
@@ -551,7 +553,7 @@ msgid ""
|
||||
"be deleted."
|
||||
msgstr ""
|
||||
"Voleu suprimir el bloc de notes? També se suprimiran totes les notes i els "
|
||||
"sub-blocs d'aquest bloc de notes."
|
||||
"subblocs d'aquest bloc de notes."
|
||||
|
||||
#: packages/app-cli/app/command-rmnote.js:13
|
||||
msgid "Deletes the notes matching <note-pattern>."
|
||||
@@ -581,8 +583,8 @@ msgid ""
|
||||
"Start, stop or check the API server. To specify on which port it should run, "
|
||||
"set the api.port config variable. Commands are (%s)."
|
||||
msgstr ""
|
||||
"Arrenca, atura o verifica el servidor API. Per especificar a quin port ha de "
|
||||
"córrer, estableix la variable api.port. Les ordres són (%s)."
|
||||
"Arrenca, atura o verifica el servidor API. Per a especificar a quin port ha "
|
||||
"de córrer, estableix la variable api.port. Les ordres són (%s)."
|
||||
|
||||
#: packages/app-cli/app/command-server.js:38
|
||||
#, javascript-format
|
||||
@@ -620,8 +622,8 @@ msgstr "Mostra un resum sobre les notes i blocs de notes."
|
||||
msgid ""
|
||||
"To retry decryption of these items. Run `e2ee decrypt --retry-failed-items`"
|
||||
msgstr ""
|
||||
"Per a reintentar el desxifratge d'aquests elements. Executa `e2ee decrypt --"
|
||||
"retry-failed-items`"
|
||||
"Per a reintentar el desxifratge d'aquests elements. Executa \"e2ee decrypt --"
|
||||
"retry-failed-items\""
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:29
|
||||
msgid "Synchronises with remote storage."
|
||||
@@ -658,7 +660,7 @@ msgstr ""
|
||||
#: packages/app-desktop/gui/DropboxLoginScreen.js:30
|
||||
#: packages/app-mobile/components/screens/dropbox-login.js:59
|
||||
msgid "Step 1: Open this URL in your browser to authorise the application:"
|
||||
msgstr "Pas 1: Obriu aquest URL al navegador per autoritzar l'aplicació:"
|
||||
msgstr "Pas 1: Obriu aquest URL al navegador per a autoritzar l'aplicació:"
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:94
|
||||
#: packages/app-desktop/gui/DropboxLoginScreen.js:32
|
||||
@@ -738,9 +740,9 @@ msgid ""
|
||||
"target is a regular note it will be converted to a to-do). Use \"clear\" to "
|
||||
"convert the to-do back to a regular note."
|
||||
msgstr ""
|
||||
"<todo-command> pot ser «toggle» o «clear». Useu «toggle» per a canviar el "
|
||||
"<todo-command> pot ser «toggle» o «clear». Useu «toggle» per a canviar els "
|
||||
"llistats de tasques entre l'estat de finalitzat i no finalitzat (si "
|
||||
"l'objectiu és una nota normal es convertirà a un llistat de tasques "
|
||||
"l'objectiu és una nota normal, es convertirà a un llistat de tasques "
|
||||
"pendents). Useu «clear» per a convertir un llistat de tasques pendents a una "
|
||||
"nota normal."
|
||||
|
||||
@@ -795,7 +797,7 @@ msgstr ""
|
||||
|
||||
#: packages/app-cli/app/gui/NoteWidget.js:50
|
||||
msgid "You may also type `status` for more information."
|
||||
msgstr "També podeu escriure `status` per obtenir més informació."
|
||||
msgstr "També podeu escriure \"status\" per a obtenir més informació."
|
||||
|
||||
#: packages/app-cli/app/help-utils.js:56
|
||||
msgid "Enum"
|
||||
@@ -894,7 +896,7 @@ msgstr "Cancel·la"
|
||||
msgid ""
|
||||
"The app is now going to close. Please relaunch it to complete the process."
|
||||
msgstr ""
|
||||
"L'aplicació es tancarà ara. Si us plau, torneu-la a executar per completar "
|
||||
"L'aplicació es tancarà ara. Si us plau, torneu-la a executar per a completar "
|
||||
"el procés."
|
||||
|
||||
#: packages/app-desktop/checkForUpdates.js:171
|
||||
@@ -904,7 +906,7 @@ msgstr "La versió actual està actualitzada."
|
||||
#: packages/app-desktop/checkForUpdates.js:184
|
||||
#, javascript-format
|
||||
msgid "%s (pre-release)"
|
||||
msgstr "%s (pre-llençament)"
|
||||
msgstr "%s (prellançament)"
|
||||
|
||||
#: packages/app-desktop/checkForUpdates.js:187
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
@@ -974,7 +976,7 @@ msgstr "Esteu segur que voleu renovar el testimoni d'autorització?"
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:84
|
||||
msgid "The web clipper service is enabled and set to auto-start."
|
||||
msgstr ""
|
||||
"El servei de desa-retalls de webs és actiu i configurat per a iniciar-se "
|
||||
"El servei de porta-retalls de webs és actiu i configurat per a iniciar-se "
|
||||
"automàticament."
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:69
|
||||
@@ -992,17 +994,17 @@ msgstr "Estat: %s"
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:74
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:103
|
||||
msgid "Disable Web Clipper Service"
|
||||
msgstr "Desactiva el servei del desa-retalls de webs"
|
||||
msgstr "Desactiva el servei del porta-retalls de webs"
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:77
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:109
|
||||
msgid "The web clipper service is not enabled."
|
||||
msgstr "El servei del desa-retalls de webs no està activat."
|
||||
msgstr "El servei del porta-retalls de webs no està activat."
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:78
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:114
|
||||
msgid "Enable Web Clipper Service"
|
||||
msgstr "Activa el servei del desa-retalls de webs"
|
||||
msgstr "Activa el servei del porta-retalls de webs"
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:89
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:137
|
||||
@@ -1010,18 +1012,18 @@ msgid ""
|
||||
"Joplin Web Clipper allows saving web pages and screenshots from your browser "
|
||||
"to Joplin."
|
||||
msgstr ""
|
||||
"El desa-retalls de webs del Joplin us permet desar pàgines web i captures de "
|
||||
"pantalla del navegador web al Joplin."
|
||||
"El porta-retalls de webs del Joplin us permet desar pàgines web i captures "
|
||||
"de pantalla del navegador web al Joplin."
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:90
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:142
|
||||
msgid "In order to use the web clipper, you need to do the following:"
|
||||
msgstr "Per a poder usar el desa-retalls de webs, cal que feu el següent:"
|
||||
msgstr "Per a poder usar el porta-retalls de webs, cal que feu el següent:"
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:92
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:150
|
||||
msgid "Step 1: Enable the clipper service"
|
||||
msgstr "Pas 1: Activeu el servei del desa-retalls de webs"
|
||||
msgstr "Pas 1: Activeu el servei del porta-retalls de webs"
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:93
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:155
|
||||
@@ -1031,7 +1033,7 @@ msgid ""
|
||||
"to a particular port."
|
||||
msgstr ""
|
||||
"Aquest servei permet que l'extensió del navegador pugui comunicar-se amb el "
|
||||
"Joplin. En activar-la, el tallafocs us podria demanar de donar permís al "
|
||||
"Joplin. En activar-la, el tallafoc us podria demanar de donar permís al "
|
||||
"Joplin per a escoltar un port determinat."
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:96
|
||||
@@ -1065,7 +1067,7 @@ msgid ""
|
||||
"This authorisation token is only needed to allow third-party applications to "
|
||||
"access Joplin."
|
||||
msgstr ""
|
||||
"Aquest testimoni d'autorització només és necessari per permetre l'accés "
|
||||
"Aquest testimoni d'autorització només és necessari per a permetre l'accés "
|
||||
"d'aplicacions de tercers al Joplin."
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:111
|
||||
@@ -1171,7 +1173,7 @@ msgstr "Actualització"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:154
|
||||
msgid "Please upgrade Joplin to use this plugin"
|
||||
msgstr "Si us plau, actualitzeu Joplin per utilitzar aquest connector"
|
||||
msgstr "Si us plau, actualitzeu Joplin per a utilitzar aquest connector"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:168
|
||||
#, javascript-format
|
||||
@@ -1235,19 +1237,16 @@ msgid "Submit"
|
||||
msgstr "Tramet"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:73
|
||||
#, fuzzy
|
||||
msgid "Source: "
|
||||
msgstr "Font"
|
||||
msgstr "Font: "
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:76
|
||||
#, fuzzy
|
||||
msgid "Created: "
|
||||
msgstr "Creació: %s"
|
||||
msgstr "Data de creació: "
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:79
|
||||
#, fuzzy
|
||||
msgid "Updated: "
|
||||
msgstr "Actualitzat: %s"
|
||||
msgstr "Última actualització: "
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:84
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:96
|
||||
@@ -1257,9 +1256,8 @@ msgid "Save"
|
||||
msgstr "Desa"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:87
|
||||
#, fuzzy
|
||||
msgid "Disable"
|
||||
msgstr "Desactivat"
|
||||
msgstr "Desactiva"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:87
|
||||
#: packages/app-mobile/components/screens/encryption-config.js:157
|
||||
@@ -1318,9 +1316,9 @@ msgid ""
|
||||
"You may use the tool below to re-encrypt your data, for example if you know "
|
||||
"that some of your notes are encrypted with an obsolete encryption method."
|
||||
msgstr ""
|
||||
"Podeu utilitzar l'eina següent per re-xifrar les vostres dades, per exemple "
|
||||
"si sabeu que algunes de les vostres notes estan xifrades amb un mètode de "
|
||||
"xifratge obsolet."
|
||||
"Podeu utilitzar l'eina següent per tornar a xifrar les vostres dades, per "
|
||||
"exemple si sabeu que algunes de les vostres notes estan xifrades amb un "
|
||||
"mètode de xifratge obsolet."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:122
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:186
|
||||
@@ -1348,7 +1346,7 @@ msgstr ""
|
||||
"1. Sincronitzeu tots els vostres dispositius.\n"
|
||||
"2. Feu click en \"%s\".\n"
|
||||
"3. Deixeu-lo executant-se fins que acabi. Mentre s'executa, eviteu fer "
|
||||
"canvis en cap nota des dels altres dispositius per evitar els conflictes.\n"
|
||||
"canvis en cap nota des dels altres dispositius per a evitar els conflictes.\n"
|
||||
"4. Un cop la sincronització està acabada en aquest dispositiu, sincronitzeu "
|
||||
"tots els altres dispositius, i deixeu-los executant-se fins que acabin.\n"
|
||||
"\n"
|
||||
@@ -1371,11 +1369,11 @@ msgstr "Claus mestres"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:142
|
||||
msgid "Hide disabled master keys"
|
||||
msgstr ""
|
||||
msgstr "Amaga les claus mestres deshabilitades"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:142
|
||||
msgid "Show disabled master keys"
|
||||
msgstr ""
|
||||
msgstr "Mostra les claus mestres deshabilitades"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:143
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:343
|
||||
@@ -1384,7 +1382,7 @@ msgid ""
|
||||
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
"Nota: només s'usarà una clau mestre per al xifratge (la marcada com a "
|
||||
"Nota: només s'usarà una clau mestra per al xifratge (la marcada com a "
|
||||
"«activa»). Qualsevol de les claus es podrien usar per a desxifrar, depenent "
|
||||
"de com es van xifrar originalment les notes o blocs de notes."
|
||||
|
||||
@@ -1395,7 +1393,7 @@ msgstr "Activa"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:149
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
msgstr "Data"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:150
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:329
|
||||
@@ -1403,14 +1401,12 @@ msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:151
|
||||
#, fuzzy
|
||||
msgid "Valid"
|
||||
msgstr "Invàlid"
|
||||
msgstr "Vàlid"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:152
|
||||
#, fuzzy
|
||||
msgid "Actions"
|
||||
msgstr "Acció"
|
||||
msgstr "Accions"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:182
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:244
|
||||
@@ -1450,9 +1446,9 @@ msgid ""
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
"Les claus mestres amb aquests IDs s'usen per a xifrar alguns dels elements. "
|
||||
"Tot i això l'aplicació actualment no hi té accés. Probablement es baixin via "
|
||||
"sincrontizació."
|
||||
"Les claus mestres amb aquests ID s'usen per a xifrar alguns dels elements. "
|
||||
"Tot i això l'aplicació actualment no hi té accés. Probablement es baixaran "
|
||||
"via sincronització."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:226
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:414
|
||||
@@ -1562,7 +1558,7 @@ msgid ""
|
||||
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
|
||||
"shortcut."
|
||||
msgstr ""
|
||||
"Premeu la drecera i premeu RETORN. O, premeu RETROCÉS per esborrar la "
|
||||
"Premeu la drecera i premeu RETORN. O, premeu RETROCÉS per a esborrar la "
|
||||
"drecera."
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:51
|
||||
@@ -1684,13 +1680,12 @@ msgstr "Establiu la contrasenya"
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:619
|
||||
msgid "Use the arrows to move the layout items. Press \"Escape\" to exit."
|
||||
msgstr ""
|
||||
"Utilitzeu les fletxes per moure els elements de la disposició. Premeu "
|
||||
"«Escapa» per sortir."
|
||||
"Utilitzeu les fletxes per a moure els elements de la disposició. Premeu "
|
||||
"«Escapa» per a sortir."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/commandPalette.js:18
|
||||
#, fuzzy
|
||||
msgid "Command palette..."
|
||||
msgstr "Paleta d'ordres"
|
||||
msgstr "Paleta d'ordres..."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/editAlarm.js:20
|
||||
#: packages/app-mobile/components/SelectDateTimeDialog.js:84
|
||||
@@ -1792,9 +1787,8 @@ msgid "Share notebook..."
|
||||
msgstr "Comparteix el quadern de notes..."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js:16
|
||||
#, fuzzy
|
||||
msgid "Publish note..."
|
||||
msgstr "Comparteix nota..."
|
||||
msgstr "Publica la nota..."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js:19
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:180
|
||||
@@ -1882,7 +1876,7 @@ msgstr "&Visualitza"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:506
|
||||
msgid "Layout button sequence"
|
||||
msgstr "Seqûència del botó de disposició"
|
||||
msgstr "Seqüència del botó de disposició"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:551
|
||||
#: packages/app-desktop/gui/MenuBar.js:557
|
||||
@@ -2056,7 +2050,7 @@ msgid ""
|
||||
"switch to %s to edit the note."
|
||||
msgstr ""
|
||||
"Espereu que tots els adjunts hagin estat descarregats i desxifrats. També "
|
||||
"podeu canviar a %s per editar la nota."
|
||||
"podeu canviar a %s per a editar la nota."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
|
||||
msgid "Checkbox list"
|
||||
@@ -2089,7 +2083,7 @@ msgstr "Subíndex"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:285
|
||||
msgid "Click to add tags..."
|
||||
msgstr "Feu clic per afegir etiquetes..."
|
||||
msgstr "Feu clic per a afegir etiquetes..."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:339
|
||||
msgid ""
|
||||
@@ -2231,7 +2225,7 @@ msgstr "Cerca en la nota actual"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.js:59
|
||||
msgid "There was an error downloading this attachment:"
|
||||
msgstr "Hi ha hagut un error al descarregar aquest adjunt:"
|
||||
msgstr "Hi ha hagut un error en baixar aquest adjunt:"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.js:62
|
||||
#: packages/lib/services/ResourceEditWatcher/index.js:196
|
||||
@@ -2266,7 +2260,7 @@ msgstr "Missatge o enllaç no suportat: %s"
|
||||
|
||||
#: packages/app-desktop/gui/NoteList/NoteList.js:163
|
||||
msgid "Custom order"
|
||||
msgstr "Ordrenació personalitzada"
|
||||
msgstr "Ordenació personalitzada"
|
||||
|
||||
#: packages/app-desktop/gui/NoteList/NoteList.js:163
|
||||
msgid "View"
|
||||
@@ -2283,8 +2277,8 @@ msgid ""
|
||||
"To manually sort the notes, the sort order must be changed to \"%s\" in the "
|
||||
"menu \"%s\" > \"%s\""
|
||||
msgstr ""
|
||||
"Per ordenar les notes manualment, l'ordre de classificació s'ha de canviar a "
|
||||
"\"%s\" en el menú \"%s\" > \"%s\""
|
||||
"Per a ordenar les notes manualment, l'ordre de classificació s'ha de canviar "
|
||||
"a \"%s\" en el menú \"%s\" > \"%s\""
|
||||
|
||||
#: packages/app-desktop/gui/NoteList/NoteList.js:415
|
||||
msgid "No notes in here. Create one by clicking on \"New note\"."
|
||||
@@ -2340,7 +2334,7 @@ msgid ""
|
||||
"Click \"%s\" to restore the note. It will be copied in the notebook named "
|
||||
"\"%s\". The current version of the note will not be replaced or modified."
|
||||
msgstr ""
|
||||
"Cliqueu «%s» per restaurar la nota. Aquesta serà copiada al el notebook "
|
||||
"Feu clic «%s» per a restaurar la nota. Aquesta serà copiada al bloc de notes "
|
||||
"anomenat «%s». La versió actual de la nota no serà substituïda o modificada."
|
||||
|
||||
#: packages/app-desktop/gui/PromptDialog.min.js:249
|
||||
@@ -2376,7 +2370,7 @@ msgid ""
|
||||
"notes. Please be careful when deleting one of them as they cannot be "
|
||||
"restored afterwards."
|
||||
msgstr ""
|
||||
"Aquesta és una eina avançada per mostrar els adjunts que estan enllaçats a "
|
||||
"Aquesta és una eina avançada per a mostrar els adjunts que estan enllaçats a "
|
||||
"les vostres notes. Tingueu precaució en suprimir-ne un, ja que després no es "
|
||||
"poden restaurar."
|
||||
|
||||
@@ -2391,18 +2385,18 @@ msgstr ""
|
||||
"Avís: no es mostren tots els recursos per motius de rendiment (límit:% s)."
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:106
|
||||
#, fuzzy
|
||||
msgid "Confirmation"
|
||||
msgstr "Configuració"
|
||||
msgstr "Confirmació"
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:119
|
||||
msgid "The Web Clipper needs your authorisation to access your data."
|
||||
msgstr ""
|
||||
"El porta-retalls web necessita autorització per a accedir a les vostres "
|
||||
"dades."
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:120
|
||||
#, fuzzy
|
||||
msgid "Grant authorisation"
|
||||
msgstr "Testimoni d'autorització:"
|
||||
msgstr "Autoritza"
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:160
|
||||
msgid "OneDrive Login"
|
||||
@@ -2475,9 +2469,8 @@ msgid "Share Notebook"
|
||||
msgstr "Comparteix el quadern de notes"
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:144
|
||||
#, fuzzy
|
||||
msgid "Unpublish note"
|
||||
msgstr "Deixa de compartir la nota"
|
||||
msgstr "Deixa de publicar la nota"
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:171
|
||||
msgid "Synchronising..."
|
||||
@@ -2502,7 +2495,7 @@ msgstr "Nota: quan es comparteix una nota, deixa d'estar xifrada al servidor."
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:187
|
||||
msgid "Publish Notes"
|
||||
msgstr ""
|
||||
msgstr "Publica notes"
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:189
|
||||
msgid "Copy Shareable Link"
|
||||
@@ -2524,7 +2517,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Voleu suprimir el bloc de notes \"%s\"? \n"
|
||||
"\n"
|
||||
"També se suprimiran totes les notes i els sub-blocs d'aquest bloc de notes."
|
||||
"També se suprimiran totes les notes i els subblocs d'aquest bloc de notes."
|
||||
|
||||
#: packages/app-desktop/gui/Sidebar/Sidebar.js:197
|
||||
#, javascript-format
|
||||
@@ -2591,22 +2584,21 @@ msgid "Export debug report"
|
||||
msgstr "Exporta l'informe de depuració"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:157
|
||||
#, fuzzy
|
||||
msgid "Sync your notes"
|
||||
msgstr "Ordena les notes per"
|
||||
msgstr "Sincronitzeu les notes"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:158
|
||||
msgid "Publish notes to the internet"
|
||||
msgstr ""
|
||||
msgstr "Publica les notes a internet"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:159
|
||||
#, fuzzy
|
||||
msgid "Collaborate on notebooks with others"
|
||||
msgstr "Primer heu de crear un bloc de notes"
|
||||
msgstr "Col·laboreu en blocs de notes amb altres"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:182
|
||||
msgid "Thank you! Your Joplin Cloud account is now setup and ready to use."
|
||||
msgstr ""
|
||||
"Gràcies! El vostre compte de Joplin Cloud està preparat per a utilitzar-se."
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:190
|
||||
#, javascript-format
|
||||
@@ -2616,30 +2608,35 @@ msgid ""
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"S'ha produït un error configurant el vostre compte de Joplin Cloud. "
|
||||
"Verifiqueu el vostre correu electrònic i la contrasenya, i proveu de nou. "
|
||||
"L'error ha sigut:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:203
|
||||
msgid "Login below."
|
||||
msgstr ""
|
||||
msgstr "Identifiqueu-vos baix."
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:205
|
||||
#, fuzzy
|
||||
msgid "Or create an account."
|
||||
msgstr "Crea una nota nova."
|
||||
msgstr "O creeu un compte."
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:210
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
msgstr "Identifiqueu-vos"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:231
|
||||
#, fuzzy
|
||||
msgid "Select"
|
||||
msgstr "Seleccioneu tot"
|
||||
msgstr "Selecciona"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:278
|
||||
msgid ""
|
||||
"Joplin can synchronise your notes using various providers. Select one from "
|
||||
"the list below."
|
||||
msgstr ""
|
||||
"Joplin pot sincronitzar les notes utilitzant diversos proveïdors. "
|
||||
"Seleccioneu-ne un de la llista inferior."
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.js:43
|
||||
msgid "Duplicate"
|
||||
@@ -2656,7 +2653,7 @@ msgstr "Alterna entre el tipus nota i tasques pendents"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.js:87
|
||||
msgid "Switch to note type"
|
||||
msgstr "Canvia el tipos a nota"
|
||||
msgstr "Canvia el tipus a nota"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.js:93
|
||||
msgid "Switch to to-do type"
|
||||
@@ -2683,9 +2680,9 @@ msgid ""
|
||||
"by a tag name, or @ followed by a notebook name. Or type : to search for "
|
||||
"commands."
|
||||
msgstr ""
|
||||
"Escriviu un títol de nota o part del seu contingut per saltar a la nota. O "
|
||||
"Escriviu un títol de nota o part del seu contingut per a saltar a la nota. O "
|
||||
"escriviu # seguit d'un nom d'etiqueta, o @ seguit d'un nom de bloc de notes. "
|
||||
"O escriviu : per a cercar entre les ordres."
|
||||
"O escriviu \":\" per a cercar entre les ordres."
|
||||
|
||||
#: packages/app-desktop/plugins/GotoAnything.js:505
|
||||
msgid "Command palette"
|
||||
@@ -2705,11 +2702,11 @@ msgstr "No"
|
||||
|
||||
#: packages/app-mobile/components/CameraView.js:158
|
||||
msgid "Permission to use camera"
|
||||
msgstr "Permís per utilitzar la càmara"
|
||||
msgstr "Permís per a utilitzar la càmera"
|
||||
|
||||
#: packages/app-mobile/components/CameraView.js:159
|
||||
msgid "Your permission to use your camera is required."
|
||||
msgstr "Es necessita el vostre permís per utilitzar la càmara."
|
||||
msgstr "Es necessita el vostre permís per a utilitzar la càmera."
|
||||
|
||||
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:27
|
||||
msgid "Open"
|
||||
@@ -2749,8 +2746,8 @@ msgid ""
|
||||
"In order to use file system synchronisation your permission to write to "
|
||||
"external storage is required."
|
||||
msgstr ""
|
||||
"Per utilitzar la sincronització del sistema de fitxers, cal el vostre permís "
|
||||
"per escriure a l'emmagatzematge extern."
|
||||
"Per a utilitzar la sincronització del sistema de fitxers, cal el vostre "
|
||||
"permís per a escriure a l'emmagatzematge extern."
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:152
|
||||
msgid "Information"
|
||||
@@ -2794,8 +2791,8 @@ msgid ""
|
||||
"Use this to rebuild the search index if there is a problem with search. It "
|
||||
"may take a long time depending on the number of notes."
|
||||
msgstr ""
|
||||
"Utilitzeu això per reconstruir l'índex de cerca si hi ha un problema amb la "
|
||||
"cerca. Pot trigar molt en funció del nombre de notes."
|
||||
"Utilitzeu això per a reconstruir l'índex de cerca si hi ha un problema amb "
|
||||
"la cerca. Pot trigar molt en funció del nombre de notes."
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:425
|
||||
msgid "Exporting profile..."
|
||||
@@ -2907,7 +2904,7 @@ msgid ""
|
||||
"You may turn off this option at any time in the Configuration screen."
|
||||
msgstr ""
|
||||
"Per tal d'associar una geolocalització a la nota, l'aplicació necessita "
|
||||
"permís per accedir a la vostra ubicació.\n"
|
||||
"permís per a accedir a la vostra ubicació.\n"
|
||||
"\n"
|
||||
"Podeu desactivar aquesta opció en qualsevol moment a la pantalla de "
|
||||
"configuració."
|
||||
@@ -2964,7 +2961,7 @@ msgstr "Fes una foto"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.js:807
|
||||
msgid "Choose an option"
|
||||
msgstr "Esculliu una opció"
|
||||
msgstr "Escolliu una opció"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.js:840
|
||||
msgid "Convert to note"
|
||||
@@ -3110,6 +3107,9 @@ msgid ""
|
||||
"Joplin's own sync service. Also gives access to Joplin-specific features "
|
||||
"such as publishing notes or collaborating on notebooks with others."
|
||||
msgstr ""
|
||||
"El servei de sincronització propi de Joplin. També us dona accés a "
|
||||
"funcionalitats específiques de Joplin, com la publicació de notes o la "
|
||||
"col·laboració en blocs de notes amb altres."
|
||||
|
||||
#: packages/lib/SyncTargetJoplinServer.js:60
|
||||
msgid "Joplin Server"
|
||||
@@ -3121,7 +3121,7 @@ msgstr "Nextcloud"
|
||||
|
||||
#: packages/lib/SyncTargetNone.js:22
|
||||
msgid "(None)"
|
||||
msgstr ""
|
||||
msgstr "(cap)"
|
||||
|
||||
#: packages/lib/SyncTargetOneDrive.js:32
|
||||
msgid "OneDrive"
|
||||
@@ -3186,7 +3186,7 @@ msgstr "Ociós"
|
||||
|
||||
#: packages/lib/Synchronizer.js:277
|
||||
msgid "In progress"
|
||||
msgstr "En progés"
|
||||
msgstr "En progrés"
|
||||
|
||||
#: packages/lib/Synchronizer.js:984
|
||||
msgid ""
|
||||
@@ -3229,7 +3229,7 @@ msgid ""
|
||||
"\n"
|
||||
"Please try again."
|
||||
msgstr ""
|
||||
"No s'ha pogut autoritzar l'aplciació:\n"
|
||||
"No s'ha pogut autoritzar l'aplicació:\n"
|
||||
"\n"
|
||||
"%s\n"
|
||||
"\n"
|
||||
@@ -3269,7 +3269,7 @@ msgstr "Elements desxifrats: %s / %s"
|
||||
#: packages/lib/components/shared/encryption-config-shared.js:151
|
||||
#, javascript-format
|
||||
msgid "Encryption will be enabled using the master key created on %s"
|
||||
msgstr ""
|
||||
msgstr "El xifrat s'habilitarà utilitzant la clau mestra creada a %s"
|
||||
|
||||
#: packages/lib/models/BaseItem.js:721
|
||||
msgid "Encrypted"
|
||||
@@ -3339,9 +3339,8 @@ msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: packages/lib/models/Resource.js:408
|
||||
#, fuzzy
|
||||
msgid "Conflicts (attachments)"
|
||||
msgstr "Adjunts de la nota"
|
||||
msgstr "Conflictes (adjunts)"
|
||||
|
||||
#: packages/lib/models/Resource.js:422
|
||||
#, javascript-format
|
||||
@@ -3381,7 +3380,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Atenció: si canvieu aquesta ubicació, assegureu-vos de copiar-hi tot el "
|
||||
"contingut abans de sincronitzar; en cas contrari, se suprimiran tots els "
|
||||
"fitxers. Consulteu les FAQ per obtenir més detalls: %s"
|
||||
"fitxers. Consulteu les FAQ per a obtenir més detalls: %s"
|
||||
|
||||
#: packages/lib/models/Setting.js:124
|
||||
msgid "Light"
|
||||
@@ -3417,7 +3416,7 @@ msgstr "OLED fosc"
|
||||
|
||||
#: packages/lib/models/Setting.js:152
|
||||
msgid "Open Sync Wizard..."
|
||||
msgstr ""
|
||||
msgstr "Obre l'assistent de sincronització..."
|
||||
|
||||
#: packages/lib/models/Setting.js:162
|
||||
msgid "Synchronisation target"
|
||||
@@ -3543,7 +3542,7 @@ msgstr "Tema"
|
||||
|
||||
#: packages/lib/models/Setting.js:510
|
||||
msgid "Automatically switch theme to match system theme"
|
||||
msgstr "Canvia de tema automàticament per coincidir amb el tema del sistema"
|
||||
msgstr "Canvia de tema automàticament per a coincidir amb el tema del sistema"
|
||||
|
||||
#: packages/lib/models/Setting.js:522
|
||||
msgid "Preferred light theme"
|
||||
@@ -3555,7 +3554,7 @@ msgstr "Tema fosc preferit"
|
||||
|
||||
#: packages/lib/models/Setting.js:546
|
||||
msgid "Show note counts"
|
||||
msgstr "Mostra el número de notes"
|
||||
msgstr "Mostra el nombre de notes"
|
||||
|
||||
#: packages/lib/models/Setting.js:554 packages/lib/models/Setting.js:556
|
||||
#: packages/lib/models/Setting.js:557
|
||||
@@ -3619,7 +3618,7 @@ msgstr "Activa els salts de línia"
|
||||
|
||||
#: packages/lib/models/Setting.js:683
|
||||
msgid "Enable typographer support"
|
||||
msgstr "Activa el soport tipogràfic"
|
||||
msgstr "Activa el suport tipogràfic"
|
||||
|
||||
#: packages/lib/models/Setting.js:684
|
||||
msgid "Enable Linkify"
|
||||
@@ -3704,7 +3703,7 @@ msgid ""
|
||||
"reducing the number of conflicts."
|
||||
msgstr ""
|
||||
"Això permetrà que Joplin s’executi en segon pla. Es recomana habilitar "
|
||||
"aquesta configuració perquè les notes es sincronitzin constantment, reduint "
|
||||
"aquesta configuració perquè les notes se sincronitzin constantment, reduint "
|
||||
"així el nombre de conflictes."
|
||||
|
||||
#: packages/lib/models/Setting.js:717
|
||||
@@ -3726,7 +3725,7 @@ msgstr "Per defecte"
|
||||
|
||||
#: packages/lib/models/Setting.js:769
|
||||
msgid "Editor font family"
|
||||
msgstr "Famíllia de lletra de l'editor"
|
||||
msgstr "Família de lletra de l'editor"
|
||||
|
||||
#: packages/lib/models/Setting.js:770
|
||||
msgid ""
|
||||
@@ -3752,11 +3751,11 @@ msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.js:783
|
||||
msgid "Editor maximum width"
|
||||
msgstr ""
|
||||
msgstr "Amplada màxima de l'editor"
|
||||
|
||||
#: packages/lib/models/Setting.js:783
|
||||
msgid "Set it to 0 to make it take the complete available space."
|
||||
msgstr ""
|
||||
msgstr "Definiu-ho com a 0 per a fer que agafi tot l'espai disponible."
|
||||
|
||||
#: packages/lib/models/Setting.js:802
|
||||
msgid "Custom stylesheet for rendered Markdown"
|
||||
@@ -3768,7 +3767,7 @@ msgstr "Full d'estil personalitzat per a estils d'aplicacions de tot Joplin"
|
||||
|
||||
#: packages/lib/models/Setting.js:828
|
||||
msgid "Re-upload local data to sync target"
|
||||
msgstr "Torna a pujar les dades locals per sincronitzar la destinació"
|
||||
msgstr "Torna a pujar les dades locals per a sincronitzar la destinació"
|
||||
|
||||
#: packages/lib/models/Setting.js:838
|
||||
msgid "Delete local data and re-download from sync target"
|
||||
@@ -3782,12 +3781,12 @@ msgstr "Actualitza automàticament l'aplicació"
|
||||
|
||||
#: packages/lib/models/Setting.js:844
|
||||
msgid "Get pre-releases when checking for updates"
|
||||
msgstr "Obtén pre-llançaments quan cerqui actualitzacions"
|
||||
msgstr "Obtén prellançaments quan cerqui actualitzacions"
|
||||
|
||||
#: packages/lib/models/Setting.js:844
|
||||
#, javascript-format
|
||||
msgid "See the pre-release page for more details: %s"
|
||||
msgstr "Consulta la pàgina de pre-llançament per a més detalls: %s"
|
||||
msgstr "Consulta la pàgina de prellançament per a més detalls: %s"
|
||||
|
||||
#: packages/lib/models/Setting.js:852
|
||||
msgid "Synchronisation interval"
|
||||
@@ -3880,7 +3879,7 @@ msgstr "Vim"
|
||||
|
||||
#: packages/lib/models/Setting.js:920
|
||||
msgid "Do not resize images"
|
||||
msgstr ""
|
||||
msgstr "No canvies la mida de les imatges"
|
||||
|
||||
#: packages/lib/models/Setting.js:935
|
||||
msgid "Custom TLS certificates"
|
||||
@@ -3895,9 +3894,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Una llista separada per comes de camins a directoris d'on carregar els "
|
||||
"certificats, o el camí a fitxers de certificats concrets. Per exemple, "
|
||||
"el_meu/dir_cert, /altres/personalitzat.pem. Tingueu en compte que si feu "
|
||||
"canvis en la configuració TLS, cal que els deseu abans de fer clic a "
|
||||
"«Comprova la configuració de la sincronització»."
|
||||
"directori/subdirectori_certificats, /altres/personalitzat.pem. Tingueu en "
|
||||
"compte que si feu canvis en la configuració TLS, cal que els deseu abans de "
|
||||
"fer clic a «Comprova la configuració de la sincronització»."
|
||||
|
||||
#: packages/lib/models/Setting.js:958
|
||||
msgid "Ignore TLS certificate errors"
|
||||
@@ -3953,10 +3952,11 @@ msgid ""
|
||||
"item with a factor of 2 will take twice as much space as an item with a "
|
||||
"factor of 1.Restart app to see changes."
|
||||
msgstr ""
|
||||
"La propietat factor defineix com l’element creixerà o es reduirà per ajustar-"
|
||||
"se a l’espai disponible al seu contenidor respecte als altres elements. Per "
|
||||
"tant, un element amb un factor de 2 ocuparà el doble d’espai que un element "
|
||||
"amb un factor de 1. Reinicieu l’aplicació per veure els canvis."
|
||||
"La propietat factor defineix com l’element creixerà o es reduirà per a "
|
||||
"ajustar-se a l’espai disponible al seu contenidor respecte als altres "
|
||||
"elements. Per tant, un element amb un factor de 2 ocuparà el doble d’espai "
|
||||
"que un element amb un factor d'1. Reinicieu l’aplicació per a veure els "
|
||||
"canvis."
|
||||
|
||||
#: packages/lib/models/Setting.js:1029
|
||||
msgid "Note list growth factor"
|
||||
@@ -4019,7 +4019,7 @@ msgid ""
|
||||
"formatting. It is indicated below which plugins are compatible or not with "
|
||||
"the WYSIWYG editor."
|
||||
msgstr ""
|
||||
"Aquests connectors milloren el renderitzador de Markdown amb funcions "
|
||||
"Aquestes extensions milloren el renderitzador de Markdown amb funcions "
|
||||
"addicionals. Tingueu en compte que, tot i que aquestes funcions poden ser "
|
||||
"útils, no són Markdown estàndard i, per tant, la majoria només funcionaran "
|
||||
"dins de Joplin. A més, alguns d’ells són *incompatibles* amb l’editor "
|
||||
@@ -4065,7 +4065,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Obriu l'URL següent al navegador per a autenticar l'aplicació. L'aplicació "
|
||||
"crearà un directori a «Aplicacions/Joplin» i només llegirà i escriurà "
|
||||
"fitxers en aquest directory. No tindrà accés a cap fitxer fora d'aquesta "
|
||||
"fitxers en aquest directory. No tindrà accés a cap fitxer fora d'aquest "
|
||||
"directori ni a cap dada personal. No es compartirà cap dada amb terceres "
|
||||
"parts."
|
||||
|
||||
@@ -4121,7 +4121,7 @@ msgid ""
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
"Aquests elements es mantindran al dispositiu però no es pujaran a la "
|
||||
"Aquests elements es mantindran al dispositiu, però no es pujaran a la "
|
||||
"destinació de sincronització. Per a poder trobar aquests elements, podeu "
|
||||
"cercar pel títol o la ID (que es mostra entre claudàtors a sobre)."
|
||||
|
||||
@@ -4204,7 +4204,7 @@ msgstr "Conflictius: %d"
|
||||
#: packages/lib/services/ReportService.js:242
|
||||
#, javascript-format
|
||||
msgid "To delete: %d"
|
||||
msgstr "Per suprimir: %d"
|
||||
msgstr "Per a suprimir: %d"
|
||||
|
||||
#: packages/lib/services/ReportService.js:244
|
||||
msgid "Folders"
|
||||
@@ -4276,7 +4276,7 @@ msgstr "Indiqueu el format d'importació per a %s"
|
||||
|
||||
#: packages/lib/services/interop/InteropService_Exporter_Jex.js:43
|
||||
msgid "There is no data to export."
|
||||
msgstr "No hi ha dades per exportar."
|
||||
msgstr "No hi ha dades per a exportar."
|
||||
|
||||
#: packages/lib/services/interop/InteropService_Importer_Md.js:46
|
||||
msgid "Please specify the notebook where the notes should be imported to."
|
||||
@@ -4356,16 +4356,18 @@ msgid "attachment"
|
||||
msgstr "fitxer adjunt"
|
||||
|
||||
#: packages/server/dist/models/UserModel.js:199
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Cannot save %s \"%s\" because it is larger than the allowed limit (%s)"
|
||||
msgstr "No es pot desar %s «%s» perquè és més gran que el límit permès (%s)"
|
||||
|
||||
#: packages/server/dist/models/UserModel.js:204
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Cannot save %s \"%s\" because it would go over the total allowed size (%s) "
|
||||
"for this account"
|
||||
msgstr "No es pot desar %s «%s» perquè és més gran que el límit permès (%s)"
|
||||
msgstr ""
|
||||
"No es pot desar %s «%s» perquè se'n passaria de l'espai total (%s) d'aquest "
|
||||
"compte"
|
||||
|
||||
#, javascript-format
|
||||
#~ msgid "%s %s (%s)"
|
||||
|
||||
+143
-164
@@ -7,6 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: 12.09.2021\n"
|
||||
"Last-Translator: mrkaato\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fi_FI\n"
|
||||
@@ -14,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 2.4.1\n"
|
||||
"X-Generator: Poedit 3.0\n"
|
||||
|
||||
#: packages/app-cli/app/app-gui.js:452
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
@@ -133,6 +135,7 @@ msgid ""
|
||||
"Runs the commands contained in the text file. There should be one command "
|
||||
"per line."
|
||||
msgstr ""
|
||||
"Suorittaa tekstitiedoston komennot. Riviä kohden pitäisi olla yksi komento."
|
||||
|
||||
#: packages/app-cli/app/command-cat.js:14
|
||||
msgid "Displays the given note."
|
||||
@@ -599,8 +602,8 @@ msgid ""
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
||||
"properties are:\n"
|
||||
"Asettaa ominaisuuden <name> annetusta <note> annettuun [arvoon]. Mahdolliset "
|
||||
"ominaisuudet ovat:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
@@ -647,7 +650,7 @@ msgstr ""
|
||||
#: packages/app-desktop/gui/DropboxLoginScreen.js:30
|
||||
#: packages/app-mobile/components/screens/dropbox-login.js:59
|
||||
msgid "Step 1: Open this URL in your browser to authorise the application:"
|
||||
msgstr "Vaihe 1: Avaa tämä URL-osoite selaimessa valtuuttaaksesi sovelluksen:"
|
||||
msgstr "Vaihe 1: Avaa tämä URL osoite selaimessa valtuuttaaksesi sovelluksen:"
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:94
|
||||
#: packages/app-desktop/gui/DropboxLoginScreen.js:32
|
||||
@@ -908,11 +911,11 @@ msgstr "Lataa"
|
||||
|
||||
#: packages/app-desktop/checkForUpdates.js:189
|
||||
msgid "Skip this version"
|
||||
msgstr ""
|
||||
msgstr "Ohita tämä versio"
|
||||
|
||||
#: packages/app-desktop/checkForUpdates.js:189
|
||||
msgid "Full changelog"
|
||||
msgstr ""
|
||||
msgstr "Täysi muutosloki"
|
||||
|
||||
#: packages/app-desktop/commands/copyDevCommand.js:18
|
||||
msgid "Copy dev mode command to clipboard"
|
||||
@@ -939,9 +942,8 @@ msgid "Stop"
|
||||
msgstr "Seis"
|
||||
|
||||
#: packages/app-desktop/commands/toggleSafeMode.js:18
|
||||
#, fuzzy
|
||||
msgid "Toggle safe mode"
|
||||
msgstr "Näytä sivupalkki"
|
||||
msgstr "Vaihda turvalliseen tilaan"
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:34
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:40
|
||||
@@ -951,7 +953,7 @@ msgstr "Tunnus on kopioitu leikepöydälle!"
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:37
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:44
|
||||
msgid "Are you sure you want to renew the authorisation token?"
|
||||
msgstr ""
|
||||
msgstr "Haluatko varmasti uusia valtuutustunnuksen?"
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:67
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:84
|
||||
@@ -1053,7 +1055,7 @@ msgstr ""
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.js:111
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:222
|
||||
msgid "Renew token"
|
||||
msgstr ""
|
||||
msgstr "Uusi tunnus"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.js:27
|
||||
msgid "Apply"
|
||||
@@ -1179,13 +1181,12 @@ msgid "You do not have any installed plugin."
|
||||
msgstr "Sinulla ei ole asennettua laajennusta."
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:232
|
||||
#, fuzzy
|
||||
msgid "Could not connect to plugin repository"
|
||||
msgstr "Laajennuksen asentaminen epäonnistui: %s"
|
||||
msgstr "Yhteyden muodostaminen laajennusten arkistoon epäonnistui"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:234
|
||||
msgid "Try again"
|
||||
msgstr ""
|
||||
msgstr "Yritä uudestaan"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:242
|
||||
msgid "Plugin tools"
|
||||
@@ -1219,19 +1220,16 @@ msgid "Submit"
|
||||
msgstr "Lähetä"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:73
|
||||
#, fuzzy
|
||||
msgid "Source: "
|
||||
msgstr "Lähde"
|
||||
msgstr "Lähde: "
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:76
|
||||
#, fuzzy
|
||||
msgid "Created: "
|
||||
msgstr "Luotu: %s"
|
||||
msgstr "Luotu: "
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:79
|
||||
#, fuzzy
|
||||
msgid "Updated: "
|
||||
msgstr "Päivitetty: %s"
|
||||
msgstr "Päivitetty: "
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:84
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:96
|
||||
@@ -1241,9 +1239,8 @@ msgid "Save"
|
||||
msgstr "Tallenna"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:87
|
||||
#, fuzzy
|
||||
msgid "Disable"
|
||||
msgstr "Poistettu käytöstä"
|
||||
msgstr "Poista käytöstä"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:87
|
||||
#: packages/app-mobile/components/screens/encryption-config.js:157
|
||||
@@ -1355,11 +1352,11 @@ msgstr "Pääavaimet"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:142
|
||||
msgid "Hide disabled master keys"
|
||||
msgstr ""
|
||||
msgstr "Piilota käytöstä poistetut pääavaimet"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:142
|
||||
msgid "Show disabled master keys"
|
||||
msgstr ""
|
||||
msgstr "Näytä käytöstä poistetut pääavaimet"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:143
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:343
|
||||
@@ -1380,7 +1377,7 @@ msgstr "Aktiivinen"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:149
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
msgstr "Päivämäärä"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:150
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:329
|
||||
@@ -1388,14 +1385,12 @@ msgid "Password"
|
||||
msgstr "Salasana"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:151
|
||||
#, fuzzy
|
||||
msgid "Valid"
|
||||
msgstr "Virheellinen"
|
||||
msgstr "Kelvollinen"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:152
|
||||
#, fuzzy
|
||||
msgid "Actions"
|
||||
msgstr "Toiminta"
|
||||
msgstr "Toiminnot"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.js:182
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:244
|
||||
@@ -1575,12 +1570,12 @@ msgstr "Sulje ikkuna"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:24
|
||||
msgid "Preferences"
|
||||
msgstr "Asetukset"
|
||||
msgstr "Oletusasetukset"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:24
|
||||
#: packages/app-desktop/gui/MenuBar.js:310 packages/app-desktop/gui/Root.js:163
|
||||
msgid "Options"
|
||||
msgstr "Vaihtoehdot"
|
||||
msgstr "Asetukset"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:31
|
||||
msgid "Invalid"
|
||||
@@ -1591,10 +1586,12 @@ msgid ""
|
||||
"Safe mode is currently active. Note rendering and all plugins are "
|
||||
"temporarily disabled."
|
||||
msgstr ""
|
||||
"Turvallinen tila on tällä hetkellä aktiivinen. Huomautusten renderöinti ja "
|
||||
"kaikki laajennukset on tilapäisesti poistettu käytöstä."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:470
|
||||
msgid "Disable safe mode and restart"
|
||||
msgstr ""
|
||||
msgstr "Lopeta turvallinen tila ja käynnistä uudelleen"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:473
|
||||
msgid ""
|
||||
@@ -1638,16 +1635,16 @@ msgstr "Lisätietoja"
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:487
|
||||
#, javascript-format
|
||||
msgid "%s (%s) would like to share a notebook with you."
|
||||
msgstr ""
|
||||
msgstr "%s (%s) haluaa jakaa muistikirjan kanssasi."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:487
|
||||
msgid "Accept"
|
||||
msgstr ""
|
||||
msgstr "Hyväksy"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:487
|
||||
#: packages/app-desktop/gui/Root.js:121
|
||||
msgid "Reject"
|
||||
msgstr ""
|
||||
msgstr "Hylkää"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:490
|
||||
msgid "Some items cannot be synchronised."
|
||||
@@ -1666,9 +1663,8 @@ msgid "Use the arrows to move the layout items. Press \"Escape\" to exit."
|
||||
msgstr "Siirrä asettelukohteita nuolilla. Paina \"Esc\" poistuaksesi."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/commandPalette.js:18
|
||||
#, fuzzy
|
||||
msgid "Command palette..."
|
||||
msgstr "Komentovalikoima"
|
||||
msgstr "Komentovalikoima..."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/editAlarm.js:20
|
||||
#: packages/app-mobile/components/SelectDateTimeDialog.js:84
|
||||
@@ -1683,7 +1679,7 @@ msgstr "Aseta hälytys:"
|
||||
#: packages/app-desktop/gui/MainScreen/commands/exportPdf.js:20
|
||||
#: packages/app-desktop/gui/MainScreen/commands/exportPdf.js:32
|
||||
msgid "PDF File"
|
||||
msgstr "PDF-tiedosto"
|
||||
msgstr "PDF tiedosto"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/gotoAnything.js:23
|
||||
#: packages/app-desktop/plugins/GotoAnything.js:497
|
||||
@@ -1766,14 +1762,12 @@ msgid "Note properties"
|
||||
msgstr "Muistiinpanon ominaisuudet"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js:16
|
||||
#, fuzzy
|
||||
msgid "Share notebook..."
|
||||
msgstr "Muistiinpanon jakaminen..."
|
||||
msgstr "Muistikirjan jakaminen..."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js:16
|
||||
#, fuzzy
|
||||
msgid "Publish note..."
|
||||
msgstr "Muistiinpanon jakaminen..."
|
||||
msgstr "Muistiinpanon julkaiseminen..."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js:19
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:180
|
||||
@@ -1833,7 +1827,7 @@ msgstr "Joplinista"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:367
|
||||
msgid "Preferences..."
|
||||
msgstr "Asetukset..."
|
||||
msgstr "Oletusasetukset..."
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:377
|
||||
#: packages/app-desktop/gui/MenuBar.js:618
|
||||
@@ -1861,7 +1855,7 @@ msgstr "&Näytä"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:506
|
||||
msgid "Layout button sequence"
|
||||
msgstr "Asettele painike järjestys"
|
||||
msgstr "Aseta painike järjestys"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:551
|
||||
#: packages/app-desktop/gui/MenuBar.js:557
|
||||
@@ -1882,12 +1876,11 @@ msgstr "&Mene"
|
||||
#: packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js:18
|
||||
#: packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js:18
|
||||
msgid "Focus"
|
||||
msgstr "Keskittää"
|
||||
msgstr "Osoita"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:585
|
||||
#, fuzzy
|
||||
msgid "Note&book"
|
||||
msgstr "Muistikirjat"
|
||||
msgstr "&Muistikirjat"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:591
|
||||
msgid "&Note"
|
||||
@@ -2049,23 +2042,23 @@ msgstr "Muokkaa"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:17
|
||||
msgid "Highlight"
|
||||
msgstr ""
|
||||
msgstr "Korosta"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:22
|
||||
msgid "Strikethrough"
|
||||
msgstr ""
|
||||
msgstr "Yliviivaus"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:27
|
||||
msgid "Insert"
|
||||
msgstr ""
|
||||
msgstr "Aseta"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:33
|
||||
msgid "Superscript"
|
||||
msgstr ""
|
||||
msgstr "Yläindeksi"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js:39
|
||||
msgid "Subscript"
|
||||
msgstr ""
|
||||
msgstr "Alaindeksi"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:285
|
||||
msgid "Click to add tags..."
|
||||
@@ -2162,9 +2155,8 @@ msgid "Delete line"
|
||||
msgstr "Poista rivi"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
|
||||
#, fuzzy
|
||||
msgid "Duplicate line"
|
||||
msgstr "Duplikaatti, toinen samanlainen"
|
||||
msgstr "Rivin kaksoiskappale"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
|
||||
msgid "Undo"
|
||||
@@ -2372,18 +2364,16 @@ msgstr ""
|
||||
"Varoitus: kaikkia resursseja ei näytetä suorituskyvyn vuoksi (raja: %s)."
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:106
|
||||
#, fuzzy
|
||||
msgid "Confirmation"
|
||||
msgstr "Konfigurointi"
|
||||
msgstr "Vahvistus"
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:119
|
||||
msgid "The Web Clipper needs your authorisation to access your data."
|
||||
msgstr ""
|
||||
msgstr "Web Clipper tarvitsee valtuutuksen tietojen käyttöön."
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:120
|
||||
#, fuzzy
|
||||
msgid "Grant authorisation"
|
||||
msgstr "Valtuutuksen tunnus:"
|
||||
msgstr "Myönnä lupa"
|
||||
|
||||
#: packages/app-desktop/gui/Root.js:160
|
||||
msgid "OneDrive Login"
|
||||
@@ -2398,19 +2388,20 @@ msgid "Note attachments"
|
||||
msgstr "Muistiinpanon liitteet"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:141
|
||||
#, fuzzy
|
||||
msgid "Unshare"
|
||||
msgstr "Jaa"
|
||||
msgstr "Poista jakaminen"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:183
|
||||
msgid ""
|
||||
"Delete this invitation? The recipient will no longer have access to this "
|
||||
"shared notebook."
|
||||
msgstr ""
|
||||
"Poistetaanko tämä kutsu? Vastaanottaja ei voi enää käyttää tätä jaettua "
|
||||
"muistikirjaa."
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:197
|
||||
msgid "Add recipient:"
|
||||
msgstr ""
|
||||
msgstr "Lisää vastaanottaja:"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:200
|
||||
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:28
|
||||
@@ -2420,45 +2411,43 @@ msgstr "Jaa"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:209
|
||||
msgid "Recipient has not yet accepted the invitation"
|
||||
msgstr ""
|
||||
msgstr "Vastaanottaja ei ole vielä hyväksynyt kutsua"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:210
|
||||
msgid "Recipient has rejected the invitation"
|
||||
msgstr ""
|
||||
msgstr "Vastaanottaja on hylännyt kutsun"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:211
|
||||
msgid "Recipient has accepted the invitation"
|
||||
msgstr ""
|
||||
msgstr "Vastaanottaja on hyväksynyt kutsun"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:221
|
||||
msgid "Recipients:"
|
||||
msgstr ""
|
||||
msgstr "Vastaanottajat:"
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:233
|
||||
#, fuzzy
|
||||
msgid "Synchronizing..."
|
||||
msgstr "Synkronoidaan..."
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:234
|
||||
#, fuzzy
|
||||
msgid "Sharing notebook..."
|
||||
msgstr "Muistiinpanon jakaminen..."
|
||||
msgstr "Jaetaan muistikirjaa..."
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:244
|
||||
msgid ""
|
||||
"Unshare this notebook? The recipients will no longer have access to its "
|
||||
"content."
|
||||
msgstr ""
|
||||
"Poistetaanko tämän muistikirjan jakaminen? Vastaanottajilla ei ole enää "
|
||||
"pääsyä sen sisältöön."
|
||||
|
||||
#: packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js:254
|
||||
#, fuzzy
|
||||
msgid "Share Notebook"
|
||||
msgstr "Jaa muistiinpanoja"
|
||||
msgstr "Jaa muistikirja"
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:144
|
||||
#, fuzzy
|
||||
msgid "Unpublish note"
|
||||
msgstr "Jaa"
|
||||
msgstr "Peruuta muistiinpanon julkaisu"
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:171
|
||||
msgid "Synchronising..."
|
||||
@@ -2484,7 +2473,7 @@ msgstr ""
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:187
|
||||
msgid "Publish Notes"
|
||||
msgstr ""
|
||||
msgstr "Julkaise muistiinpanot"
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:189
|
||||
msgid "Copy Shareable Link"
|
||||
@@ -2565,32 +2554,28 @@ msgid "Retry"
|
||||
msgstr "Yritä uudelleen"
|
||||
|
||||
#: packages/app-desktop/gui/StatusScreen/StatusScreen.js:137
|
||||
#, fuzzy
|
||||
msgid "Advanced tools"
|
||||
msgstr "Lisäasetukset"
|
||||
msgstr "Lisätyökalut"
|
||||
|
||||
#: packages/app-desktop/gui/StatusScreen/StatusScreen.js:139
|
||||
#, fuzzy
|
||||
msgid "Export debug report"
|
||||
msgstr "Vie virheenkorjausraportti"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:157
|
||||
#, fuzzy
|
||||
msgid "Sync your notes"
|
||||
msgstr "Lajittele muistiinpanot"
|
||||
msgstr "Synkronoi muistiinpanosi"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:158
|
||||
msgid "Publish notes to the internet"
|
||||
msgstr ""
|
||||
msgstr "Julkaise muistiinpanot Internetissä"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:159
|
||||
#, fuzzy
|
||||
msgid "Collaborate on notebooks with others"
|
||||
msgstr "Luo ensin muistikirja"
|
||||
msgstr "Muistikirjojen yhteiskäyttö muiden kanssa"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:182
|
||||
msgid "Thank you! Your Joplin Cloud account is now setup and ready to use."
|
||||
msgstr ""
|
||||
msgstr "Kiitos! Your Joplin Cloud tilisi on nyt määritetty ja käyttövalmis."
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:190
|
||||
#, javascript-format
|
||||
@@ -2600,30 +2585,34 @@ msgid ""
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Joplin Cloud tilisi määrittämisessä tapahtui virhe. Vahvista "
|
||||
"sähköpostiosoitteesi ja salasanasi sekä yritä uudelleen. Virhe oli:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:203
|
||||
msgid "Login below."
|
||||
msgstr ""
|
||||
msgstr "Kirjaudu sisään alla."
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:205
|
||||
#, fuzzy
|
||||
msgid "Or create an account."
|
||||
msgstr "Luo uuden muistiinpanon."
|
||||
msgstr "Tai luo tili."
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:210
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
msgstr "Kirjaudu"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:231
|
||||
#, fuzzy
|
||||
msgid "Select"
|
||||
msgstr "Valitse kaikki"
|
||||
msgstr "Valitse"
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.js:278
|
||||
msgid ""
|
||||
"Joplin can synchronise your notes using various providers. Select one from "
|
||||
"the list below."
|
||||
msgstr ""
|
||||
"Joplin voi synkronoida muistiinpanosi eri palveluntarjoajien avulla. Valitse "
|
||||
"yksi alla olevasta luettelosta."
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.js:43
|
||||
msgid "Duplicate"
|
||||
@@ -2644,7 +2633,7 @@ msgstr "Vaihda muistiinpanotyyppiin"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.js:93
|
||||
msgid "Switch to to-do type"
|
||||
msgstr "Siirry tehtävätyyppiin"
|
||||
msgstr "Vaihda tehtävätyyppiin"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.js:100
|
||||
#: packages/app-mobile/components/screens/Note.js:847
|
||||
@@ -2797,7 +2786,7 @@ msgstr "Vain virheenkorjausta varten: vie profiilisi ulkoiselle SD-kortille."
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:436
|
||||
msgid "Feature flags"
|
||||
msgstr ""
|
||||
msgstr "Ominaisuus liput"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:439
|
||||
msgid "More information"
|
||||
@@ -2809,8 +2798,8 @@ msgid ""
|
||||
"them in your phone settings, in Apps > Joplin > Permissions"
|
||||
msgstr ""
|
||||
"Toimiakseen oikein sovellus tarvitsee seuraavat käyttöoikeudet. Ota ne "
|
||||
"käyttöön puhelimesi asetuksissa, valitsemalla Sovellukset> Joplin> "
|
||||
"Käyttöoikeudet"
|
||||
"käyttöön puhelimesi asetuksissa, valitsemalla Sovellukset> Joplin> Luvat "
|
||||
"(käyttöoikeudet)"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:446
|
||||
msgid ""
|
||||
@@ -2822,11 +2811,11 @@ msgstr ""
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:447
|
||||
msgid "- Camera: to allow taking a picture and attaching it to a note."
|
||||
msgstr "- Kamera: sallii kuvan ottamisen ja liittämisen muistiinpanoon."
|
||||
msgstr "- Kamera: sallia kuvan ottamisen ja liittämisen muistiinpanoon."
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:448
|
||||
msgid "- Location: to allow attaching geo-location information to a note."
|
||||
msgstr "- Sijainti: sallii paikkatietojen liittämisen muistiinpanoon."
|
||||
msgstr "- Sijainti: sallia paikkatietojen liittämisen muistiinpanoon."
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.js:459
|
||||
msgid "Joplin website"
|
||||
@@ -2931,7 +2920,7 @@ msgstr "Näytä kartalla"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.js:761
|
||||
msgid "Go to source URL"
|
||||
msgstr "Siirry lähteen URL-osoitteeseen"
|
||||
msgstr "Siirry lähteen URL osoitteeseen"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.js:790
|
||||
msgid "Attach..."
|
||||
@@ -3048,7 +3037,7 @@ msgstr "Uusi muistikirja"
|
||||
|
||||
#: packages/app-mobile/components/side-menu-content.js:351
|
||||
msgid "Mobile data - auto-sync disabled"
|
||||
msgstr ""
|
||||
msgstr "Mobiilidata - automaattinen synkronointi poistettu käytöstä"
|
||||
|
||||
#: packages/lib/BaseApplication.js:154 packages/lib/BaseApplication.js:166
|
||||
#: packages/lib/BaseApplication.js:198
|
||||
@@ -3070,7 +3059,7 @@ msgid ""
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Yhteyden muodostaminen Joplinin palvelimelle epäonnistui. Tarkista "
|
||||
"vaihtoehdot Synkronointiasetukset-näytössä. Koko virhe oli:\n"
|
||||
"synkronoinnin asetukset. Koko virhe oli:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
@@ -3087,15 +3076,17 @@ msgid "File system"
|
||||
msgstr "Tiedostojärjestelmä"
|
||||
|
||||
#: packages/lib/SyncTargetJoplinCloud.js:28
|
||||
#, fuzzy
|
||||
msgid "Joplin Cloud"
|
||||
msgstr "Joplin Foorumi"
|
||||
msgstr "Joplin Cloud"
|
||||
|
||||
#: packages/lib/SyncTargetJoplinCloud.js:31
|
||||
msgid ""
|
||||
"Joplin's own sync service. Also gives access to Joplin-specific features "
|
||||
"such as publishing notes or collaborating on notebooks with others."
|
||||
msgstr ""
|
||||
"Joplinin oma synkronointipalvelu. Voit myös käyttää Joplin "
|
||||
"erityisominaisuuksia, kuten muistiinpanojen julkaisemista tai muistikirjojen "
|
||||
"yhteiskäyttöä muiden kanssa."
|
||||
|
||||
#: packages/lib/SyncTargetJoplinServer.js:60
|
||||
msgid "Joplin Server"
|
||||
@@ -3107,7 +3098,7 @@ msgstr "Nextcloud"
|
||||
|
||||
#: packages/lib/SyncTargetNone.js:22
|
||||
msgid "(None)"
|
||||
msgstr ""
|
||||
msgstr "(Ei mitään)"
|
||||
|
||||
#: packages/lib/SyncTargetOneDrive.js:32
|
||||
msgid "OneDrive"
|
||||
@@ -3157,9 +3148,9 @@ msgid "Cancelling..."
|
||||
msgstr "Peruutetaan..."
|
||||
|
||||
#: packages/lib/Synchronizer.js:159
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Completed: %s (%s)"
|
||||
msgstr "Valmis: %s"
|
||||
msgstr "Valmis: %s (%s)"
|
||||
|
||||
#: packages/lib/Synchronizer.js:161
|
||||
#, javascript-format
|
||||
@@ -3196,8 +3187,8 @@ msgid ""
|
||||
"Error. Please check that URL, username, password, etc. are correct and that "
|
||||
"the sync target is accessible. The reported error was:"
|
||||
msgstr ""
|
||||
"Virhe. Tarkista URL-osoite, käyttäjänimi, salasana jne. ovat oikeita ja "
|
||||
"synkronointikohde on käytettävissä. Raportoitu virhe oli:"
|
||||
"Virhe. Tarkista URL osoite, käyttäjänimi, salasana jne. ovat oikeita ja "
|
||||
"synkronointi kohde on käytettävissä. Raportoitu virhe oli:"
|
||||
|
||||
#: packages/lib/components/shared/dropbox-login-shared.js:39
|
||||
msgid "The application has been authorised!"
|
||||
@@ -3252,7 +3243,7 @@ msgstr "Purettuja kohteita: %s / %s"
|
||||
#: packages/lib/components/shared/encryption-config-shared.js:151
|
||||
#, javascript-format
|
||||
msgid "Encryption will be enabled using the master key created on %s"
|
||||
msgstr ""
|
||||
msgstr "Salaus otetaan käyttöön käyttämällä luotua pääavainta %s"
|
||||
|
||||
#: packages/lib/models/BaseItem.js:721
|
||||
msgid "Encrypted"
|
||||
@@ -3322,9 +3313,8 @@ msgid "Error"
|
||||
msgstr "Virhe"
|
||||
|
||||
#: packages/lib/models/Resource.js:408
|
||||
#, fuzzy
|
||||
msgid "Conflicts (attachments)"
|
||||
msgstr "Muistiinpanon liitteet"
|
||||
msgstr "Ristiriidat (liitteet)"
|
||||
|
||||
#: packages/lib/models/Resource.js:422
|
||||
#, javascript-format
|
||||
@@ -3400,7 +3390,7 @@ msgstr "OLED Dark"
|
||||
|
||||
#: packages/lib/models/Setting.js:152
|
||||
msgid "Open Sync Wizard..."
|
||||
msgstr ""
|
||||
msgstr "Avaa ohjattu synkronointitoiminto."
|
||||
|
||||
#: packages/lib/models/Setting.js:162
|
||||
msgid "Synchronisation target"
|
||||
@@ -3464,23 +3454,20 @@ msgid "Joplin Server URL"
|
||||
msgstr "Joplin palvelimen URL-osoite"
|
||||
|
||||
#: packages/lib/models/Setting.js:348
|
||||
#, fuzzy
|
||||
msgid "Joplin Server email"
|
||||
msgstr "Joplin Palvelin"
|
||||
msgstr "Joplin palvelimen sähköposti"
|
||||
|
||||
#: packages/lib/models/Setting.js:359
|
||||
msgid "Joplin Server password"
|
||||
msgstr "Joplin palvelimen salasana"
|
||||
|
||||
#: packages/lib/models/Setting.js:386
|
||||
#, fuzzy
|
||||
msgid "Joplin Cloud email"
|
||||
msgstr "Joplin Palvelin"
|
||||
msgstr "Joplin Cloud sähköposti"
|
||||
|
||||
#: packages/lib/models/Setting.js:397
|
||||
#, fuzzy
|
||||
msgid "Joplin Cloud password"
|
||||
msgstr "Joplin palvelimen salasana"
|
||||
msgstr "Joplin Cloud salasana"
|
||||
|
||||
#: packages/lib/models/Setting.js:409
|
||||
msgid "Attachment download behaviour"
|
||||
@@ -3492,8 +3479,8 @@ msgid ""
|
||||
"In \"Auto\", they are downloaded when you open the note. In \"Always\", all "
|
||||
"the attachments are downloaded whether you open the note or not."
|
||||
msgstr ""
|
||||
"Manuaalisessa tilassa liitteet ladataan vain, kun napsautat niitä. \"Auto\" -"
|
||||
"kohdassa ne ladataan, kun avaat muistiinpanon. \"Aina\" -kohdassa kaikki "
|
||||
"Manuaalisessa tilassa liitteet ladataan vain, kun napsautat niitä. \"Auto\" "
|
||||
"valinnassa ne ladataan, kun avaat muistiinpanon. \"Aina\" valinnassa kaikki "
|
||||
"liitteet ladataan riippumatta siitä, avaatko muistiinpanon vai et."
|
||||
|
||||
#: packages/lib/models/Setting.js:413
|
||||
@@ -3720,11 +3707,12 @@ msgid ""
|
||||
"Used for most text in the markdown editor. If not found, a generic "
|
||||
"proportional (variable width) font is used."
|
||||
msgstr ""
|
||||
"Käytetään useimmissa teksteissä markdown editorissa. Jos sitä ei löydy, "
|
||||
"käytetään yleistä suhteellista fonttia (vaihtelevaa leveyttä)."
|
||||
|
||||
#: packages/lib/models/Setting.js:779
|
||||
#, fuzzy
|
||||
msgid "Editor monospace font family"
|
||||
msgstr "Editorin fonttiperhe"
|
||||
msgstr "Editorin monospace fonttiperhe"
|
||||
|
||||
#: packages/lib/models/Setting.js:780
|
||||
msgid ""
|
||||
@@ -3732,14 +3720,17 @@ msgid ""
|
||||
"tables, checkboxes, code). If not found, a generic monospace (fixed width) "
|
||||
"font is used."
|
||||
msgstr ""
|
||||
"Käytetään, kun tekstiin tarvitaan kiinteäleveyksinen fontti (esim. taulukot, "
|
||||
"valintaruudut, koodi). Jos sitä ei löydy, käytetään yleistä monospace "
|
||||
"fonttia (kiinteäleveyksinen)."
|
||||
|
||||
#: packages/lib/models/Setting.js:783
|
||||
msgid "Editor maximum width"
|
||||
msgstr ""
|
||||
msgstr "Editorin enimmäisleveys"
|
||||
|
||||
#: packages/lib/models/Setting.js:783
|
||||
msgid "Set it to 0 to make it take the complete available space."
|
||||
msgstr ""
|
||||
msgstr "Aseta arvoksi 0, jotta se vie koko käytettävissä olevan tilan."
|
||||
|
||||
#: packages/lib/models/Setting.js:802
|
||||
msgid "Custom stylesheet for rendered Markdown"
|
||||
@@ -3751,11 +3742,11 @@ msgstr "Mukautettu tyylitaulukko Joplinin sovellustyyleille"
|
||||
|
||||
#: packages/lib/models/Setting.js:828
|
||||
msgid "Re-upload local data to sync target"
|
||||
msgstr ""
|
||||
msgstr "Lataa paikalliset tiedot uudelleen kohteen synkronointia varten"
|
||||
|
||||
#: packages/lib/models/Setting.js:838
|
||||
msgid "Delete local data and re-download from sync target"
|
||||
msgstr ""
|
||||
msgstr "Poista paikalliset tiedot ja lataa uudelleen synkronointikohteesta"
|
||||
|
||||
#: packages/lib/models/Setting.js:843
|
||||
msgid "Automatically update the application"
|
||||
@@ -3792,7 +3783,7 @@ msgstr "%d tuntia"
|
||||
|
||||
#: packages/lib/models/Setting.js:871
|
||||
msgid "Synchronise only over WiFi connection"
|
||||
msgstr ""
|
||||
msgstr "Synkronoi vain WiFi yhteyden kautta"
|
||||
|
||||
#: packages/lib/models/Setting.js:878
|
||||
msgid "Text editor command"
|
||||
@@ -3809,7 +3800,7 @@ msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.js:879
|
||||
msgid "Page size for PDF export"
|
||||
msgstr "PDF-viennin sivukoko"
|
||||
msgstr "PDF viennin sivukoko"
|
||||
|
||||
#: packages/lib/models/Setting.js:881
|
||||
msgid "A4"
|
||||
@@ -3837,15 +3828,15 @@ msgstr "Legal"
|
||||
|
||||
#: packages/lib/models/Setting.js:889
|
||||
msgid "Page orientation for PDF export"
|
||||
msgstr "Sivun suunta PDF-vientiä varten"
|
||||
msgstr "Sivun suunta PDF vientiä varten"
|
||||
|
||||
#: packages/lib/models/Setting.js:891
|
||||
msgid "Portrait"
|
||||
msgstr "Pysty"
|
||||
msgstr "Pystysuunta"
|
||||
|
||||
#: packages/lib/models/Setting.js:892
|
||||
msgid "Landscape"
|
||||
msgstr "Vaaka"
|
||||
msgstr "Vaakasuunta"
|
||||
|
||||
#: packages/lib/models/Setting.js:902
|
||||
msgid "Keyboard Mode"
|
||||
@@ -3861,11 +3852,11 @@ msgstr "Vim"
|
||||
|
||||
#: packages/lib/models/Setting.js:920
|
||||
msgid "Do not resize images"
|
||||
msgstr ""
|
||||
msgstr "Älä muuta kuvien kokoa"
|
||||
|
||||
#: packages/lib/models/Setting.js:935
|
||||
msgid "Custom TLS certificates"
|
||||
msgstr "Mukautetut TLS-varmenteet"
|
||||
msgstr "Mukautetut TLS varmenteet"
|
||||
|
||||
#: packages/lib/models/Setting.js:936
|
||||
msgid ""
|
||||
@@ -3876,13 +3867,13 @@ msgid ""
|
||||
msgstr ""
|
||||
"Pilkuilla erotettu luettelo hakemistoiden poluista, joilta varmenteet "
|
||||
"ladataan, tai polku yksittäisiin varmennetiedostoihin. Esimerkiksi: /my/"
|
||||
"cert_dir, /other/custom.pem. Huomaa, että jos teet muutoksia TLS-asetuksiin, "
|
||||
"cert_dir, /other/custom.pem. Huomaa, että jos teet muutoksia TLS asetuksiin, "
|
||||
"sinun on tallennettava muutokset ennen kuin napsautat \"Tarkista "
|
||||
"synkronointiasetukset\"."
|
||||
|
||||
#: packages/lib/models/Setting.js:958
|
||||
msgid "Ignore TLS certificate errors"
|
||||
msgstr "Ohita TLS-varmenteen virheet"
|
||||
msgstr "Ohita TLS varmenteen virheet"
|
||||
|
||||
#: packages/lib/models/Setting.js:967
|
||||
msgid "Fail-safe"
|
||||
@@ -3893,15 +3884,15 @@ msgid ""
|
||||
"Fail-safe: Do not wipe out local data when sync target is empty (often the "
|
||||
"result of a misconfiguration or bug)"
|
||||
msgstr ""
|
||||
"Vikasietoinen: Älä pyyhi paikallisia tietoja, kun synkronointikohde on tyhjä "
|
||||
"(usein virheellisen määritysvirheen tai virheen seurauksena)"
|
||||
"Vikaturvallinen: Älä pyyhi paikallisia tietoja, kun synkronointikohde on "
|
||||
"tyhjä (usein virheellisen määritysvirheen tai virheen seurauksena)"
|
||||
|
||||
#: packages/lib/models/Setting.js:972
|
||||
msgid ""
|
||||
"Specify the port that should be used by the API server. If not set, a "
|
||||
"default will be used."
|
||||
msgstr ""
|
||||
"Määritä portti, jota API-palvelimen on käytettävä. Jos sitä ei ole "
|
||||
"Määritä portti, jota API palvelimen on käytettävä. Jos sitä ei ole "
|
||||
"määritetty, käytetään oletusarvoa."
|
||||
|
||||
#: packages/lib/models/Setting.js:977
|
||||
@@ -3933,10 +3924,10 @@ msgid ""
|
||||
"item with a factor of 2 will take twice as much space as an item with a "
|
||||
"factor of 1.Restart app to see changes."
|
||||
msgstr ""
|
||||
"Kerroin-ominaisuus määrittää, miten kohde kasvaa tai kutistuu niin, että se "
|
||||
"Kerroin ominaisuus määrittää, miten kohde kasvaa tai kutistuu niin, että se "
|
||||
"sopii säiliössä olevaan tilaan suhteessa muihin kohteisiin. Näin ollen "
|
||||
"kohde, jonka kerroin on 2, vie kaksi kertaa enemmän tilaa kuin kohde, jonka "
|
||||
"kerroin on 1.Restart-sovellus muutosten näkemistä varten."
|
||||
"kerroin on 1.Restart sovellus muutosten näkemistä varten."
|
||||
|
||||
#: packages/lib/models/Setting.js:1029
|
||||
msgid "Note list growth factor"
|
||||
@@ -4010,7 +4001,7 @@ msgstr ""
|
||||
#: packages/lib/models/Setting.js:1709
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr "Muistiinpanot ja asetukset tallennetaan: %s"
|
||||
msgstr "Muistiinpanot ja oletusasetukset tallennetaan: %s"
|
||||
|
||||
#: packages/lib/models/Tag.js:223
|
||||
#, javascript-format
|
||||
@@ -4213,28 +4204,28 @@ msgstr "Muistiinpano \"%s\" on palautettu muistikirjaan \"%s\"."
|
||||
#: packages/lib/services/interop/InteropService.js:48
|
||||
#: packages/lib/services/interop/InteropService.js:57
|
||||
msgid "Joplin Export File"
|
||||
msgstr "Joplin Vie tiedosto"
|
||||
msgstr "Joplin vie tiedosto"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:50
|
||||
#: packages/lib/services/interop/InteropService.js:58
|
||||
msgid "Joplin Export Directory"
|
||||
msgstr "Joplin Vie hakemisto"
|
||||
msgstr "Joplin vie hakemisto"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:51
|
||||
msgid "Evernote Export File (as Markdown)"
|
||||
msgstr "Evernote Vie tiedosto (kuten Markdown)"
|
||||
msgstr "Evernote vie tiedosto (kuten Markdown)"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:52
|
||||
msgid "Evernote Export File (as HTML)"
|
||||
msgstr "Evernote Vie tiedosto (kuten HTML)"
|
||||
msgstr "Evernote vie tiedosto (kuten HTML)"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:60
|
||||
msgid "HTML File"
|
||||
msgstr "HTML Tiedosto"
|
||||
msgstr "HTML tiedosto"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:61
|
||||
msgid "HTML Directory"
|
||||
msgstr "HTML Hakemisto"
|
||||
msgstr "HTML hakemisto"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:127
|
||||
#, javascript-format
|
||||
@@ -4324,19 +4315,19 @@ msgid ""
|
||||
"The default admin password is insecure and has not been changed! [Change it "
|
||||
"now](%s)"
|
||||
msgstr ""
|
||||
"Järjestelmänvalvojan oletussalasana on epävarma, eikä sitä ole muutettu "
|
||||
"[Vaihda se nyt] (%s)"
|
||||
"Järjestelmänvalvojan oletussalasana on turvaton, eikä sitä ole muutettu "
|
||||
"[Vaihda se nyt](%s)"
|
||||
|
||||
#: packages/server/dist/models/UserModel.js:199
|
||||
#: packages/server/dist/models/UserModel.js:204
|
||||
#, fuzzy
|
||||
msgid "attachment"
|
||||
msgstr "Liitteet"
|
||||
msgstr "liite"
|
||||
|
||||
#: packages/server/dist/models/UserModel.js:199
|
||||
#, javascript-format
|
||||
msgid "Cannot save %s \"%s\" because it is larger than the allowed limit (%s)"
|
||||
msgstr ""
|
||||
"Tallennus ei onnistu %s \"%s\" koska se on suurempi kuin sallittu raja (%s)"
|
||||
|
||||
#: packages/server/dist/models/UserModel.js:204
|
||||
#, javascript-format
|
||||
@@ -4344,6 +4335,8 @@ msgid ""
|
||||
"Cannot save %s \"%s\" because it would go over the total allowed size (%s) "
|
||||
"for this account"
|
||||
msgstr ""
|
||||
"Kohdetta %s \"%s\" ei voida tallentaa, koska se ylittäisi tämän tilin "
|
||||
"sallitun kokonaiskoon (%s)"
|
||||
|
||||
#, javascript-format
|
||||
#~ msgid "%s %s (%s)"
|
||||
@@ -4370,8 +4363,8 @@ msgstr ""
|
||||
#~ msgid "Templates"
|
||||
#~ msgstr "Mallit"
|
||||
|
||||
#~ msgid "Share Notes"
|
||||
#~ msgstr "Jaa muistiinpanoja"
|
||||
#~ msgid "Full Release Notes"
|
||||
#~ msgstr "Täydelliset julkaisutiedot"
|
||||
|
||||
#~ msgid "Joplin Server Directory"
|
||||
#~ msgstr "Joplin palvelimen hakemisto"
|
||||
@@ -4379,25 +4372,11 @@ msgstr ""
|
||||
#~ msgid "Joplin Server username"
|
||||
#~ msgstr "Joplin palvelimen käyttäjänimi"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "marked text"
|
||||
#~ msgstr "korostettu teksti"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Mark"
|
||||
#~ msgstr "Merkintä"
|
||||
|
||||
#~ msgid "Full Release Notes"
|
||||
#~ msgstr "Täydelliset julkaisutiedot"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid ""
|
||||
#~ "If the font is incorrect or empty, it will default to a generic monospace "
|
||||
#~ "font."
|
||||
#~ msgstr ""
|
||||
#~ "Tämän on oltava *monospace* fontti, muuten se ei toimi oikein. Jos "
|
||||
#~ "kirjasin on väärä tai tyhjä, se on oletusarvoisesti yleinen monospace "
|
||||
#~ "fontti."
|
||||
#~ "Jos fontti on väärä tai tyhjä, se on oletuksena yleinen monospace fontti."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "This should be a *monospace* font or some elements will render "
|
||||
|
||||
Generated
+283
-1
@@ -6,7 +6,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/tools",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"execa": "^4.1.0",
|
||||
@@ -31,6 +31,7 @@
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/node": "^14.14.6",
|
||||
"gulp": "^4.0.2",
|
||||
"sass": "^1.39.2",
|
||||
"sqlite3": "^5.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
@@ -4197,6 +4198,18 @@
|
||||
"integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
@@ -4654,6 +4667,159 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.39.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.39.2.tgz",
|
||||
"integrity": "sha512-4/6Vn2RPc+qNwSclUSKvssh7dqK1Ih3FfHBW16I/GfH47b3scbYeOw65UIrYG7PkweFiKbpJjgkf5CV8EMmvzw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/chokidar": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
||||
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@@ -9271,6 +9437,12 @@
|
||||
"integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
@@ -9642,6 +9814,116 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.39.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.39.2.tgz",
|
||||
"integrity": "sha512-4/6Vn2RPc+qNwSclUSKvssh7dqK1Ih3FfHBW16I/GfH47b3scbYeOw65UIrYG7PkweFiKbpJjgkf5CV8EMmvzw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
||||
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/node": "^14.14.6",
|
||||
"gulp": "^4.0.2",
|
||||
"sass": "^1.39.2",
|
||||
"sqlite3": "^5.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
|
||||
@@ -79,6 +79,14 @@
|
||||
{
|
||||
"name": "cuongtransc",
|
||||
"id": "808091"
|
||||
},
|
||||
{
|
||||
"name": "iamwillbar",
|
||||
"id": "3266447"
|
||||
},
|
||||
{
|
||||
"name": "marcdw1289",
|
||||
"id": "42319182"
|
||||
}
|
||||
],
|
||||
"orgs": [
|
||||
|
||||
@@ -147,7 +147,6 @@ export function execCommandVerbose(commandName: string, args: string[] = []) {
|
||||
interface ExecCommandOptions {
|
||||
showInput?: boolean;
|
||||
showOutput?: boolean;
|
||||
showError?: boolean;
|
||||
quiet?: boolean;
|
||||
}
|
||||
|
||||
@@ -161,7 +160,6 @@ export async function execCommand2(command: string | string[], options: ExecComm
|
||||
options = {
|
||||
showInput: true,
|
||||
showOutput: true,
|
||||
showError: true,
|
||||
quiet: false,
|
||||
...options,
|
||||
};
|
||||
@@ -169,7 +167,6 @@ export async function execCommand2(command: string | string[], options: ExecComm
|
||||
if (options.quiet) {
|
||||
options.showInput = false;
|
||||
options.showOutput = false;
|
||||
options.showError = false;
|
||||
}
|
||||
|
||||
if (options.showInput) {
|
||||
@@ -185,7 +182,6 @@ export async function execCommand2(command: string | string[], options: ExecComm
|
||||
args.splice(0, 1);
|
||||
const promise = execa(executableName, args);
|
||||
if (options.showOutput) promise.stdout.pipe(process.stdout);
|
||||
if (options.showError) promise.stderr.pipe(process.stderr);
|
||||
const result = await promise;
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Joplin Server Changelog
|
||||
|
||||
## [server-v2.4.7-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.4.7-beta) (Pre-release) - 2021-09-15T15:58:46Z
|
||||
|
||||
- Improved: Improve flag logic (c229821)
|
||||
- Fixed: Fixed handling of brute force limiter by getting correct user IP (3ce947e)
|
||||
|
||||
## [server-v2.4.6-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.4.6-beta) (Pre-release) - 2021-09-14T15:02:21Z
|
||||
|
||||
- New: Add link to Stripe subscription page to manage payment details (4e7fe66)
|
||||
- New: Add transaction info to debug deadlock issues (01b653f)
|
||||
|
||||
## [server-v2.4.3-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.4.3-beta) (Pre-release) - 2021-09-02T17:49:11Z
|
||||
|
||||
- New: Added Help page for Joplin Cloud (6520a48)
|
||||
|
||||
@@ -8,7 +8,7 @@ In some cases however, the extra markup format that appears in notes can be seen
|
||||
|
||||
However **there is a catch**: in Joplin, notes, even when edited with this Rich Text editor, are **still Markdown** under the hood. This is generally a good thing, because it means you can switch at any time between Markdown and Rich Text editor, and the note is still readable. It is also good if you sync with the mobile application, which doesn't have a rich text editor. The catch is that since Markdown is used under the hood, it means the rich text editor has a number of limitations it inherits from that format:
|
||||
|
||||
- For a start, **most Markdown plugins will not be compatible**. If you open a Markdown note that makes use of such plugin in the Rich Text editor, it is likely you will lose the plugin special formatting. The only supported plugins are the "fenced" plugins - those that wrap a section of text in triple backticks (for example, KaTeX, Mermaid, etc. are working). You can see on the Markdown config screen which plugins that are compatible or not.
|
||||
- For a start, **most Markdown plugins will not be compatible**. If you open a Markdown note that makes use of such plugin in the Rich Text editor, it is likely you will lose the plugin special formatting. The only supported plugins are the "fenced" plugins - those that wrap a section of text in triple backticks (for example, KaTeX, Mermaid, etc. are working). You can see a plugin's compatibility on the Markdown config screen.
|
||||
|
||||
- It is not possible to have multiple new lines in a row. Because in Markdown, multiple new lines would be collapsed into one when rendered.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user