1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Merge branch 'dev' of github.com:laurent22/joplin into dev

This commit is contained in:
Laurent Cozic 2021-03-26 17:56:18 +01:00
commit 3fa13828cd
25 changed files with 425 additions and 1205 deletions

View File

@ -232,6 +232,9 @@ packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js.map
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.d.ts
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js.map
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.d.ts
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map

3
.gitignore vendored
View File

@ -219,6 +219,9 @@ packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js.map
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.d.ts
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js.map
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.d.ts
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map

View File

@ -410,7 +410,7 @@ https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/pull_request_guidel
<p>Pull requests must be based on an issue that existed <strong>before GSoC was started</strong>, or based on an issue created by a moderator.</p>
</li>
<li>
<p>Each contributor <strong>may only create one pull request at a time</strong>. In some rare cases, once your pull request has been merged, you may be allowed to post a second one, in which case we will let you know. We have this rule in place due to our limited resources - if everyone was allowed to post multiple pull requests we will not be able to review them properly. It is also better for you because you only need to care about one PR - so spend time making sure it is as good as it can be - make sure it works well, has test units, documentation and screenshots (if relevant).</p>
<p>Each contributor <strong>may only create one pull request at a time</strong>. Once your pull request has been merged, you can post a second one. We have this rule in place due to our limited resources - if everyone was allowed to post multiple pull requests we will not be able to review them properly. It is also better for you because you only need to care about one PR - so spend time making sure it is as good as it can be - make sure it works well, has test units, documentation and screenshots (if relevant).</p>
</li>
<li>
<p>If the pull request has serious issues, or would require a significant rewrite to be acceptable, we might closed it and you will not be allowed to open a new one. So <strong>please be careful when posting a PR</strong>.</p>

View File

@ -418,7 +418,7 @@ https://github.com/laurent22/joplin/blob/dev/readme/terminal.md
<tbody>
<tr>
<td>macOS, Linux, or Windows (via <a href="https://docs.microsoft.com/en-us/windows/wsl/faq">WSL</a>)</td>
<td><strong>Important:</strong> First, <a href="https://nodejs.org/en/download/package-manager/">install Node 10+</a>.<br/><br/><code>NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin</code><br/><code>sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin</code><br><br>By default, the application binary will be installed under <code>~/.joplin-bin</code>. You may change this directory if needed. Alternatively, if your npm permissions are setup as described <a href="https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory">here</a> (Option 2) then simply running <code>npm -g install joplin</code> would work.</td>
<td><strong>Important:</strong> First, <a href="https://nodejs.org/en/download/package-manager/">install Node 12+</a>.<br/><br/><code>NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin</code><br/><code>sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin</code><br><br>By default, the application binary will be installed under <code>~/.joplin-bin</code>. You may change this directory if needed. Alternatively, if your npm permissions are setup as described <a href="https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory">here</a> (Option 2) then simply running <code>npm -g install joplin</code> would work.</td>
</tr>
</tbody>
</table>
@ -817,7 +817,7 @@ Possible keys/values:
fa (Persian), pl_PL (Polski),
pt_PT (Português),
pt_BR (Português (Brasil)), ro (Română),
sl_SI (Slovenian), sv (Svenska),
sl_SI (Slovenian), sv (Svenska),
th_TH (Thai), vi (Tiếng Việt),
tr_TR (Türkçe), el_GR (Ελληνικά),
ru_RU (Русский), sr_RS (српски језик),

View File

@ -18,22 +18,23 @@ describe('JoplinSettings', () => {
test('should listen to setting change event', async () => {
const service = new newPluginService() as PluginService;
const pluginScript = newPluginScript(`
const pluginScript = newPluginScript(`
joplin.plugins.register({
onStart: async function() {
await joplin.settings.registerSetting('myCustomSetting1', {
value: 1,
type: 1,
public: true,
label: 'My Custom Setting 1',
});
await joplin.settings.registerSetting('myCustomSetting2', {
value: 2,
type: 1,
public: true,
label: 'My Custom Setting 2',
});
await joplin.settings.registerSettings({
'myCustomSetting1': {
value: 1,
type: 1,
public: true,
label: 'My Custom Setting 1',
},
'myCustomSetting2': {
value: 2,
type: 1,
public: true,
label: 'My Custom Setting 2',
}
})
joplin.settings.onChange((event) => {
joplin.data.post(['folders'], null, { title: JSON.stringify(event.keys) });
@ -66,4 +67,35 @@ describe('JoplinSettings', () => {
await service.destroy();
});
test('should allow registering multiple settings', async () => {
const service = new newPluginService() as PluginService;
const pluginScript = newPluginScript(`
joplin.plugins.register({
onStart: async function() {
await joplin.settings.registerSettings({
'myCustomSetting1': {
value: 1,
type: 1,
public: true,
label: 'My Custom Setting 1',
},
'myCustomSetting2': {
value: 2,
type: 1,
public: true,
label: 'My Custom Setting 2',
}
})
},
});
`);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
await service.runPlugin(plugin);
expect(Setting.value('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting1')).toBe(1);
expect(Setting.value('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting2')).toBe(2);
await service.destroy();
});
});

View File

@ -42,13 +42,13 @@ joplin.plugins.register({
onStart: async function() {
const panels = joplin.views.panels;
const view = await (panels as any).create();
const view = await panels.create("panel_1");
await panels.setHtml(view, 'Loading...');
await panels.addScript(view, './webview.js');
await panels.addScript(view, './webview.css');
panels.onMessage(view, (message:any) => {
await panels.onMessage(view, (message:any) => {
if (message.name === 'scrollToHash') {
joplin.commands.execute('scrollToHash', message.hash)
}
@ -88,7 +88,7 @@ joplin.plugins.register({
updateTocView();
});
joplin.workspace.onNoteContentChange(() => {
joplin.workspace.onNoteChange(() => {
updateTocView();
});
@ -97,8 +97,8 @@ joplin.plugins.register({
label: 'Toggle TOC',
iconName: 'fas fa-drum',
execute: async () => {
const isVisible = await (panels as any).visible(view);
(panels as any).show(view, !isVisible);
const isVisible = await panels.visible(view);
await panels.show(view, !isVisible);
},
});

View File

@ -478,7 +478,7 @@ class Application extends BaseApplication {
updateEditorFont() {
const fontFamilies = [];
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
fontFamilies.push('monospace');
fontFamilies.push('Avenir, Arial, sans-serif');
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155

View File

@ -0,0 +1,91 @@
import useOnInstallHandler from './useOnInstallHandler';
import { renderHook } from '@testing-library/react-hooks';
import PluginService, { defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
import { ItemEvent } from './PluginBox';
jest.mock('@joplin/lib/services/plugins/PluginService');
const pluginServiceInstance = {
updatePluginFromRepo: jest.fn(),
installPluginFromRepo: jest.fn(),
};
const pluginId = 'test.plugin';
const setInstallingPluginIds = jest.fn();
const repoApi = jest.fn();
const onPluginSettingsChange = jest.fn();
const itemEvent = ({
item: { manifest: { id: pluginId } },
} as ItemEvent);
const callHook = (isUpdate: boolean, pluginEnabled = true, pluginInstalledViaGUI = true) => () => useOnInstallHandler(
setInstallingPluginIds,
{
[pluginId]: pluginInstalledViaGUI ? {
enabled: pluginEnabled,
deleted: false,
hasBeenUpdated: false,
} : undefined,
},
repoApi,
onPluginSettingsChange,
isUpdate
);
describe('useOnInstallHandler', () => {
beforeAll(() => {
(PluginService.instance as jest.Mock).mockReturnValue(pluginServiceInstance);
(defaultPluginSetting as jest.Mock).mockImplementation(
jest.requireActual('@joplin/lib/services/plugins/PluginService').defaultPluginSetting
);
});
beforeEach(() => {
jest.clearAllMocks();
});
test('should report that the plugin is being updated', async () => {
const { result: { current: onUpdate } } = renderHook(callHook(true));
await onUpdate(itemEvent);
expect(setInstallingPluginIds).toHaveBeenCalledTimes(2);
expect(setInstallingPluginIds.mock.calls[0][0]({})).toMatchObject({ [pluginId]: true });
expect(setInstallingPluginIds.mock.calls[1][0]({})).toMatchObject({ [pluginId]: false });
});
test('should update the plugin when there is an update', async () => {
const { result: { current: onUpdate } } = renderHook(callHook(true));
await onUpdate(itemEvent);
expect(pluginServiceInstance.updatePluginFromRepo).toHaveBeenCalledWith(undefined, pluginId);
});
test('should install the plugin when it is not yet installed', async () => {
const { result: { current: onInstall } } = renderHook(callHook(false));
await onInstall(itemEvent);
expect(pluginServiceInstance.installPluginFromRepo).toHaveBeenCalledWith(undefined, pluginId);
});
test('should preserve the enabled flag when plugin is updated', async () => {
const { result: { current: onUpdate } } = renderHook(callHook(true, false));
await onUpdate(itemEvent);
const newSettings = onPluginSettingsChange.mock.calls[0][0].value;
expect(newSettings[pluginId].enabled).toBe(false);
});
test('should indicate it when plugin has been updated', async () => {
const { result: { current: onUpdate } } = renderHook(callHook(true));
await onUpdate(itemEvent);
const newSettings = onPluginSettingsChange.mock.calls[0][0].value;
expect(newSettings[pluginId].hasBeenUpdated).toBe(true);
});
test('should not fail when plugin was not installed through the GUI', async () => {
const { result: { current: onUpdate } } = renderHook(callHook(true, true, false));
await onUpdate(itemEvent);
});
});

View File

@ -39,7 +39,12 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
if (!installError) {
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
draft[pluginId] = defaultPluginSetting();
if (isUpdate) draft[pluginId].hasBeenUpdated = true;
if (isUpdate) {
if (pluginSettings[pluginId]) {
draft[pluginId].enabled = pluginSettings[pluginId].enabled;
}
draft[pluginId].hasBeenUpdated = true;
}
});
onPluginSettingsChange({ value: newSettings });

View File

@ -377,6 +377,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
`.CodeMirror-selected {
background: #6b6b6b !important;
}` : '';
const monospaceFonts = [];
if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
monospaceFonts.push('monospace');
const element = document.createElement('style');
element.setAttribute('id', 'codemirrorStyle');
@ -412,6 +415,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
padding-right: 10px !important;
}
/* This enforces monospace for certain elements (code, tables, etc.) */
.cm-jn-monospace {
font-family: ${monospaceFonts.join(', ')} !important;
}
.cm-header-1 {
font-size: 1.5em;
}

View File

@ -1,7 +1,16 @@
import 'codemirror/addon/mode/multiplex';
import 'codemirror/mode/stex/stex';
import MarkdownUtils from '@joplin/lib/markdownUtils';
import Setting from '@joplin/lib/models/Setting';
interface JoplinModeState {
outer: any;
openCharacter: string;
inTable: boolean;
inner: any;
}
// Joplin markdown is a the same as markdown mode, but it has configured defaults
// and support for katex math blocks
export default function useJoplinMode(CodeMirror: any) {
@ -34,25 +43,29 @@ export default function useJoplinMode(CodeMirror: any) {
}
return {
startState: function(): { outer: any; openCharacter: string; inner: any } {
startState: function(): JoplinModeState {
return {
outer: CodeMirror.startState(markdownMode),
openCharacter: '',
inTable: false,
inner: CodeMirror.startState(stex),
};
},
copyState: function(state: any) {
copyState: function(state: JoplinModeState) {
return {
outer: CodeMirror.copyState(markdownMode, state.outer),
openCharacter: state.openCharacter,
inTable: state.inTable,
inner: CodeMirror.copyState(stex, state.inner),
};
},
token: function(stream: any, state: any) {
token: function(stream: any, state: JoplinModeState) {
let currentMode = markdownMode;
let currentState = state.outer;
// //////// KATEX //////////
let tokenLabel = 'katex-marker-open';
let nextTokenPos = stream.string.length;
let closing = false;
@ -86,34 +99,75 @@ export default function useJoplinMode(CodeMirror: any) {
return tokenLabel;
}
// //////// End KATEX //////////
// //////// Markdown //////////
// If we found a token in this stream but haven;t reached it yet, then we will
// pass all the characters leading up to our token to markdown mode
const oldString = stream.string;
stream.string = oldString.slice(0, nextTokenPos);
const token = currentMode.token(stream, currentState);
let token = currentMode.token(stream, currentState);
stream.string = oldString;
// //////// End Markdown //////////
// //////// Monospace //////////
let isMonospace = false;
// After being passed to the markdown mode we can check if the
// code state variables are set
// Code Block
if (state.outer.code || (state.outer.thisLine && state.outer.thisLine.fencedCodeEnd)) {
isMonospace = true;
}
// Indented Code
if (state.outer.indentedCode) {
isMonospace = true;
}
// Task lists
if (state.outer.taskList || state.outer.taskOpen || state.outer.taskClosed) {
isMonospace = true;
}
// Any line that contains a | is potentially a table row
if (stream.string.match(/\|/g)) {
// Check if the current and following line together make a valid
// markdown table header
if (MarkdownUtils.matchingTableDivider(stream.string, stream.lookAhead(1))) {
state.inTable = true;
}
// Treat all lines that start with | as a table row
if (state.inTable || stream.string[0] === '|') {
isMonospace = true;
}
} else {
state.inTable = false;
}
if (isMonospace) { token = `${token} jn-monospace`; }
// //////// End Monospace //////////
return token;
},
indent: function(state: any, textAfter: string, line: any) {
indent: function(state: JoplinModeState, textAfter: string, line: any) {
const mode = state.openCharacter ? stex : markdownMode;
if (!mode.indent) return CodeMirror.Pass;
return mode.indent(state.openCharacter ? state.inner : state.outer, textAfter, line);
},
blankLine: function(state: any) {
blankLine: function(state: JoplinModeState) {
const mode = state.openCharacter ? stex : markdownMode;
if (mode.blankLine) {
mode.blankLine(state.openCharacter ? state.inner : state.outer);
}
state.inTable = false;
},
electricChars: markdownMode.electricChars,
innerMode: function(state: any) {
innerMode: function(state: JoplinModeState) {
return state.openCharacter ? { state: state.inner, mode: stex } : { state: state.outer, mode: markdownMode };
},

View File

@ -109,6 +109,11 @@ a {
}
}
.rdtPicker {
min-width: 250px;
width: auto !important;
}
.smalltalk {
font-family: sans-serif;
}

View File

@ -1,4 +1,4 @@
import { useRef, useMemo, useCallback } from 'react';
import { useRef, useCallback } from 'react';
import Setting from '@joplin/lib/models/Setting';
import useSource from './hooks/useSource';
@ -27,13 +27,13 @@ interface Props {
onLoadEnd?: Function;
}
const webViewStyle = {
backgroundColor: 'transparent',
};
export default function NoteBodyViewer(props: Props) {
const theme = themeStyle(props.themeId);
const webViewStyle: any = useMemo(() => {
return { backgroundColor: theme.backgroundColor };
}, [theme.backgroundColor]);
const dialogBoxRef = useRef(null);
const { source, injectedJs } = useSource(

View File

@ -1,135 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const EncryptionService_1 = require("./services/EncryptionService");
const shim_1 = require("./shim");
const ResourceService_1 = require("./services/ResourceService");
class BaseSyncTarget {
constructor(db, options = null) {
this.synchronizer_ = null;
this.initState_ = null;
this.logger_ = null;
this.db_ = db;
this.options_ = options;
}
static supportsConfigCheck() {
return false;
}
option(name, defaultValue = null) {
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
}
logger() {
return this.logger_;
}
setLogger(v) {
this.logger_ = v;
}
db() {
return this.db_;
}
// If [] is returned it means all platforms are supported
static unsupportedPlatforms() {
return [];
}
isAuthenticated() {
return __awaiter(this, void 0, void 0, function* () {
return false;
});
}
authRouteName() {
return null;
}
static id() {
throw new Error('id() not implemented');
}
// Note: it cannot be called just "name()" because that's a reserved keyword and
// it would throw an obscure error in React Native.
static targetName() {
throw new Error('targetName() not implemented');
}
static label() {
throw new Error('label() not implemented');
}
initSynchronizer() {
return __awaiter(this, void 0, void 0, function* () {
throw new Error('initSynchronizer() not implemented');
});
}
initFileApi() {
return __awaiter(this, void 0, void 0, function* () {
throw new Error('initFileApi() not implemented');
});
}
fileApi() {
return __awaiter(this, void 0, void 0, function* () {
if (this.fileApi_)
return this.fileApi_;
this.fileApi_ = yield this.initFileApi();
return this.fileApi_;
});
}
// Usually each sync target should create and setup its own file API via initFileApi()
// but for testing purposes it might be convenient to provide it here so that multiple
// clients can share and sync to the same file api (see test-utils.js)
setFileApi(v) {
this.fileApi_ = v;
}
synchronizer() {
return __awaiter(this, void 0, void 0, function* () {
if (this.synchronizer_)
return this.synchronizer_;
if (this.initState_ == 'started') {
// Synchronizer is already being initialized, so wait here till it's done.
return new Promise((resolve, reject) => {
const iid = shim_1.default.setInterval(() => {
if (this.initState_ == 'ready') {
shim_1.default.clearInterval(iid);
resolve(this.synchronizer_);
}
if (this.initState_ == 'error') {
shim_1.default.clearInterval(iid);
reject(new Error('Could not initialise synchroniser'));
}
}, 1000);
});
}
else {
this.initState_ = 'started';
try {
this.synchronizer_ = yield this.initSynchronizer();
this.synchronizer_.setLogger(this.logger());
this.synchronizer_.setEncryptionService(EncryptionService_1.default.instance());
this.synchronizer_.setResourceService(ResourceService_1.default.instance());
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
this.initState_ = 'ready';
return this.synchronizer_;
}
catch (error) {
this.initState_ = 'error';
throw error;
}
}
});
}
syncStarted() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.synchronizer_)
return false;
if (!(yield this.isAuthenticated()))
return false;
const sync = yield this.synchronizer();
return sync.state() != 'idle';
});
}
}
exports.default = BaseSyncTarget;
BaseSyncTarget.dispatch = () => { };
//# sourceMappingURL=BaseSyncTarget.js.map

View File

@ -1,398 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const Logger_1 = require("./Logger");
const time_1 = require("./time");
const shim_1 = require("./shim");
const Mutex = require('async-mutex').Mutex;
class Database {
constructor(driver) {
this.debugMode_ = false;
this.sqlQueryLogEnabled_ = false;
this.logger_ = new Logger_1.default();
this.logExcludedQueryTypes_ = [];
this.batchTransactionMutex_ = new Mutex();
this.profilingEnabled_ = false;
this.queryId_ = 1;
this.driver_ = driver;
}
setLogExcludedQueryTypes(v) {
this.logExcludedQueryTypes_ = v;
}
// Converts the SQLite error to a regular JS error
// so that it prints a stacktrace when passed to
// console.error()
sqliteErrorToJsError(error, sql = null, params = null) {
return this.driver().sqliteErrorToJsError(error, sql, params);
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
driver() {
return this.driver_;
}
open(options) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.driver().open(options);
}
catch (error) {
throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`);
}
this.logger().info('Database was open successfully');
});
}
escapeField(field) {
if (field == '*')
return '*';
const p = field.split('.');
if (p.length == 1)
return `\`${field}\``;
if (p.length == 2)
return `${p[0]}.\`${p[1]}\``;
throw new Error(`Invalid field format: ${field}`);
}
escapeFields(fields) {
if (fields == '*')
return '*';
const output = [];
for (let i = 0; i < fields.length; i++) {
output.push(this.escapeField(fields[i]));
}
return output;
}
tryCall(callName, inputSql, inputParams) {
return __awaiter(this, void 0, void 0, function* () {
let sql = null;
let params = null;
if (typeof inputSql === 'object') {
params = inputSql.params;
sql = inputSql.sql;
}
else {
params = inputParams;
sql = inputSql;
}
let waitTime = 50;
let totalWaitTime = 0;
const callStartTime = Date.now();
let profilingTimeoutId = null;
while (true) {
try {
this.logQuery(sql, params);
const queryId = this.queryId_++;
if (this.profilingEnabled_) {
console.info(`SQL START ${queryId}`, sql, params);
profilingTimeoutId = shim_1.default.setInterval(() => {
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
}, 3000);
}
const result = yield this.driver()[callName](sql, params);
if (this.profilingEnabled_) {
shim_1.default.clearInterval(profilingTimeoutId);
profilingTimeoutId = null;
const elapsed = Date.now() - callStartTime;
if (elapsed > 10)
console.info(`SQL END ${queryId}`, elapsed, sql, params);
}
return result; // No exception was thrown
}
catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
if (totalWaitTime >= 20000)
throw this.sqliteErrorToJsError(error, sql, params);
// NOTE: don't put logger statements here because it might log to the database, which
// could result in an error being thrown again.
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
// this.logger().warn('Error was: ' + error.toString());
yield time_1.default.msleep(waitTime);
totalWaitTime += waitTime;
waitTime *= 1.5;
}
else {
throw this.sqliteErrorToJsError(error, sql, params);
}
}
finally {
if (profilingTimeoutId)
shim_1.default.clearInterval(profilingTimeoutId);
}
}
});
}
selectOne(sql, params = null) {
return __awaiter(this, void 0, void 0, function* () {
return this.tryCall('selectOne', sql, params);
});
}
loadExtension( /* path */) {
return __awaiter(this, void 0, void 0, function* () {
return; // Disabled for now as fuzzy search extension is not in use
// let result = null;
// try {
// result = await this.driver().loadExtension(path);
// return result;
// } catch (e) {
// throw new Error(`Could not load extension ${path}`);
// }
});
}
selectAll(sql, params = null) {
return __awaiter(this, void 0, void 0, function* () {
return this.tryCall('selectAll', sql, params);
});
}
selectAllFields(sql, params, field) {
return __awaiter(this, void 0, void 0, function* () {
const rows = yield this.tryCall('selectAll', sql, params);
const output = [];
for (let i = 0; i < rows.length; i++) {
const v = rows[i][field];
if (!v)
throw new Error(`No such field: ${field}. Query was: ${sql}`);
output.push(rows[i][field]);
}
return output;
});
}
exec(sql, params = null) {
return __awaiter(this, void 0, void 0, function* () {
return this.tryCall('exec', sql, params);
});
}
transactionExecBatch(queries) {
return __awaiter(this, void 0, void 0, function* () {
if (queries.length <= 0)
return;
if (queries.length == 1) {
const q = this.wrapQuery(queries[0]);
yield this.exec(q.sql, q.params);
return;
}
// There can be only one transaction running at a time so use a mutex
const release = yield this.batchTransactionMutex_.acquire();
try {
yield this.exec('BEGIN TRANSACTION');
for (let i = 0; i < queries.length; i++) {
const query = this.wrapQuery(queries[i]);
yield this.exec(query.sql, query.params);
}
yield this.exec('COMMIT');
}
catch (error) {
yield this.exec('ROLLBACK');
throw error;
}
finally {
release();
}
});
}
static enumId(type, s) {
if (type == 'settings') {
if (s == 'int')
return 1;
if (s == 'string')
return 2;
}
if (type == 'fieldType') {
if (s)
s = s.toUpperCase();
if (s == 'INTEGER')
s = 'INT';
if (!(`TYPE_${s}` in this))
throw new Error(`Unkonwn fieldType: ${s}`);
return this[`TYPE_${s}`];
}
if (type == 'syncTarget') {
if (s == 'memory')
return 1;
if (s == 'filesystem')
return 2;
if (s == 'onedrive')
return 3;
}
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
}
static enumName(type, id) {
if (type === 'fieldType') {
if (id === Database.TYPE_UNKNOWN)
return 'unknown';
if (id === Database.TYPE_INT)
return 'int';
if (id === Database.TYPE_TEXT)
return 'text';
if (id === Database.TYPE_NUMERIC)
return 'numeric';
throw new Error(`Invalid type id: ${id}`);
}
// Or maybe an error should be thrown
return undefined;
}
static formatValue(type, value) {
if (value === null || value === undefined)
return null;
if (type == this.TYPE_INT)
return Number(value);
if (type == this.TYPE_TEXT)
return value;
if (type == this.TYPE_NUMERIC)
return Number(value);
throw new Error(`Unknown type: ${type}`);
}
sqlStringToLines(sql) {
const output = [];
const lines = sql.split('\n');
let statement = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line == '')
continue;
if (line.substr(0, 2) == '--')
continue;
statement += line.trim();
if (line[line.length - 1] == ',')
statement += ' ';
if (line[line.length - 1] == ';') {
output.push(statement);
statement = '';
}
}
return output;
}
logQuery(sql, params = null) {
if (!this.sqlQueryLogEnabled_)
return;
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0)
return;
}
}
this.logger().debug(sql);
if (params !== null && params.length)
this.logger().debug(JSON.stringify(params));
}
static insertQuery(tableName, data) {
if (!data || !Object.keys(data).length)
throw new Error('Data is empty');
let keySql = '';
let valueSql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key))
continue;
if (key[key.length - 1] == '_')
continue;
if (keySql != '')
keySql += ', ';
if (valueSql != '')
valueSql += ', ';
keySql += `\`${key}\``;
valueSql += '?';
params.push(data[key]);
}
return {
sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`,
params: params,
};
}
static updateQuery(tableName, data, where) {
if (!data || !Object.keys(data).length)
throw new Error('Data is empty');
let sql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key))
continue;
if (key[key.length - 1] == '_')
continue;
if (sql != '')
sql += ', ';
sql += `\`${key}\`=?`;
params.push(data[key]);
}
if (typeof where != 'string') {
const s = [];
for (const n in where) {
if (!where.hasOwnProperty(n))
continue;
params.push(where[n]);
s.push(`\`${n}\`=?`);
}
where = s.join(' AND ');
}
return {
sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`,
params: params,
};
}
alterColumnQueries(tableName, fields) {
const fieldsNoType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n))
continue;
fieldsNoType.push(n);
}
const fieldsWithType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n))
continue;
fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`);
}
let sql = `
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_;
DROP TABLE _TABLE_NAME_;
CREATE TABLE _TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_;
DROP TABLE _BACKUP_TABLE_NAME_;
`;
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`));
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(','));
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
return sql.trim().split('\n');
}
wrapQueries(queries) {
const output = [];
for (let i = 0; i < queries.length; i++) {
output.push(this.wrapQuery(queries[i]));
}
return output;
}
wrapQuery(sql, params = null) {
if (!sql)
throw new Error(`Cannot wrap empty string: ${sql}`);
if (Array.isArray(sql)) {
return {
sql: sql[0],
params: sql.length >= 2 ? sql[1] : null,
};
}
else if (typeof sql === 'string') {
return { sql: sql, params: params };
}
else {
return sql; // Already wrapped
}
}
}
exports.default = Database;
Database.TYPE_UNKNOWN = 0;
Database.TYPE_INT = 1;
Database.TYPE_TEXT = 2;
Database.TYPE_NUMERIC = 3;
//# sourceMappingURL=database.js.map

View File

@ -1,429 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.basicDelta = exports.FileApi = void 0;
const Logger_1 = require("./Logger");
const shim_1 = require("./shim");
const BaseItem_1 = require("./models/BaseItem");
const time_1 = require("./time");
const { isHidden } = require('./path-utils');
const JoplinError = require('./JoplinError');
const ArrayUtils = require('./ArrayUtils');
const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
const logger = Logger_1.default.create('FileApi');
function requestCanBeRepeated(error) {
const errorCode = typeof error === 'object' && error.code ? error.code : null;
// The target is explicitely rejecting the item so repeating wouldn't make a difference.
if (errorCode === 'rejectedByTarget')
return false;
// We don't repeat failSafe errors because it's an indication of an issue at the
// server-level issue which usually cannot be fixed by repeating the request.
// Also we print the previous requests and responses to the log in this case,
// so not repeating means there will be less noise in the log.
if (errorCode === 'failSafe')
return false;
return true;
}
function tryAndRepeat(fn, count) {
return __awaiter(this, void 0, void 0, function* () {
let retryCount = 0;
// Don't use internal fetch retry mechanim since we
// are already retrying here.
const shimFetchMaxRetryPrevious = shim_1.default.fetchMaxRetrySet(0);
const defer = () => {
shim_1.default.fetchMaxRetrySet(shimFetchMaxRetryPrevious);
};
while (true) {
try {
const result = yield fn();
defer();
return result;
}
catch (error) {
if (retryCount >= count || !requestCanBeRepeated(error)) {
defer();
throw error;
}
retryCount++;
yield time_1.default.sleep(1 + retryCount * 3);
}
}
});
}
class FileApi {
constructor(baseDir, driver) {
this.logger_ = new Logger_1.default();
this.syncTargetId_ = null;
this.tempDirName_ = null;
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = 0;
this.remoteDateMutex_ = new Mutex();
this.initialized_ = false;
this.baseDir_ = baseDir;
this.driver_ = driver;
this.driver_.fileApi_ = this;
}
initialize() {
return __awaiter(this, void 0, void 0, function* () {
if (this.initialized_)
return;
this.initialized_ = true;
if (this.driver_.initialize)
return this.driver_.initialize(this.fullPath(''));
});
}
fetchRemoteDateOffset_() {
return __awaiter(this, void 0, void 0, function* () {
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
const startTime = Date.now();
yield this.put(tempFile, 'timeCheck');
// Normally it should be possible to read the file back immediately but
// just in case, read it in a loop.
const loopStartTime = Date.now();
let stat = null;
while (Date.now() - loopStartTime < 5000) {
stat = yield this.stat(tempFile);
if (stat)
break;
yield time_1.default.msleep(200);
}
if (!stat)
throw new Error('Timed out trying to get sync target clock time');
void this.delete(tempFile); // No need to await for this call
const endTime = Date.now();
const expectedTime = Math.round((endTime + startTime) / 2);
return stat.updated_time - expectedTime;
});
}
// Approximates the current time on the sync target. It caches the time offset to
// improve performance.
remoteDate() {
return __awaiter(this, void 0, void 0, function* () {
const shouldSyncTime = () => {
return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_;
};
if (shouldSyncTime()) {
const release = yield this.remoteDateMutex_.acquire();
try {
// Another call might have refreshed the time while we were waiting for the mutex,
// so check again if we need to refresh.
if (shouldSyncTime()) {
this.remoteDateOffset_ = yield this.fetchRemoteDateOffset_();
// The sync target clock should rarely change but the device one might,
// so we need to refresh relatively frequently.
this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000;
}
}
catch (error) {
logger.warn('Could not retrieve remote date - defaulting to device date:', error);
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000;
}
finally {
release();
}
}
return new Date(Date.now() + this.remoteDateOffset_);
});
}
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
// historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver,
// and it defaults to no repeating.
requestRepeatCount() {
if (this.requestRepeatCount_ !== null)
return this.requestRepeatCount_;
if (this.driver_.requestRepeatCount)
return this.driver_.requestRepeatCount();
return 0;
}
lastRequests() {
return this.driver_.lastRequests ? this.driver_.lastRequests() : [];
}
clearLastRequests() {
if (this.driver_.clearLastRequests)
this.driver_.clearLastRequests();
}
baseDir() {
return typeof this.baseDir_ === 'function' ? this.baseDir_() : this.baseDir_;
}
tempDirName() {
if (this.tempDirName_ === null)
throw Error('Temp dir not set!');
return this.tempDirName_;
}
setTempDirName(v) {
this.tempDirName_ = v;
}
fsDriver() {
return shim_1.default.fsDriver();
}
driver() {
return this.driver_;
}
setSyncTargetId(v) {
this.syncTargetId_ = v;
}
syncTargetId() {
if (this.syncTargetId_ === null)
throw new Error('syncTargetId has not been set!!');
return this.syncTargetId_;
}
setLogger(l) {
if (!l)
l = new Logger_1.default();
this.logger_ = l;
}
logger() {
return this.logger_;
}
fullPath(path) {
const output = [];
if (this.baseDir())
output.push(this.baseDir());
if (path)
output.push(path);
return output.join('/');
}
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
list(path = '', options = null) {
return __awaiter(this, void 0, void 0, function* () {
if (!options)
options = {};
if (!('includeHidden' in options))
options.includeHidden = false;
if (!('context' in options))
options.context = null;
if (!('includeDirs' in options))
options.includeDirs = true;
if (!('syncItemsOnly' in options))
options.syncItemsOnly = false;
logger.debug(`list ${this.baseDir()}`);
const result = yield tryAndRepeat(() => this.driver_.list(this.fullPath(path), options), this.requestRepeatCount());
if (!options.includeHidden) {
const temp = [];
for (let i = 0; i < result.items.length; i++) {
if (!isHidden(result.items[i].path))
temp.push(result.items[i]);
}
result.items = temp;
}
if (!options.includeDirs) {
result.items = result.items.filter((f) => !f.isDir);
}
if (options.syncItemsOnly) {
result.items = result.items.filter((f) => !f.isDir && BaseItem_1.default.isSystemPath(f.path));
}
return result;
});
}
// Deprectated
setTimestamp(path, timestampMs) {
logger.debug(`setTimestamp ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath(path), timestampMs), this.requestRepeatCount());
// return this.driver_.setTimestamp(this.fullPath(path), timestampMs);
}
mkdir(path) {
logger.debug(`mkdir ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath(path)), this.requestRepeatCount());
}
stat(path) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug(`stat ${this.fullPath(path)}`);
const output = yield tryAndRepeat(() => this.driver_.stat(this.fullPath(path)), this.requestRepeatCount());
if (!output)
return output;
output.path = path;
return output;
// return this.driver_.stat(this.fullPath(path)).then((output) => {
// if (!output) return output;
// output.path = path;
// return output;
// });
});
}
// Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'`
get(path, options = null) {
if (!options)
options = {};
if (!options.encoding)
options.encoding = 'utf8';
logger.debug(`get ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.get(this.fullPath(path), options), this.requestRepeatCount());
}
put(path, content, options = null) {
return __awaiter(this, void 0, void 0, function* () {
logger.debug(`put ${this.fullPath(path)}`, options);
if (options && options.source === 'file') {
if (!(yield this.fsDriver().exists(options.path)))
throw new JoplinError(`File not found: ${options.path}`, 'fileNotFound');
}
return tryAndRepeat(() => this.driver_.put(this.fullPath(path), content, options), this.requestRepeatCount());
});
}
delete(path) {
logger.debug(`delete ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.delete(this.fullPath(path)), this.requestRepeatCount());
}
// Deprectated
move(oldPath, newPath) {
logger.debug(`move ${this.fullPath(oldPath)} => ${this.fullPath(newPath)}`);
return tryAndRepeat(() => this.driver_.move(this.fullPath(oldPath), this.fullPath(newPath)), this.requestRepeatCount());
}
// Deprectated
format() {
return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount());
}
clearRoot() {
return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount());
}
delta(path, options = null) {
logger.debug(`delta ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount());
}
}
exports.FileApi = FileApi;
function basicDeltaContextFromOptions_(options) {
const output = {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
statIdsCache: null,
deletedItemsProcessed: false,
};
if (!options || !options.context)
return output;
const d = new Date(options.context.timestamp);
output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null;
output.deletedItemsProcessed = options.context && 'deletedItemsProcessed' in options.context ? options.context.deletedItemsProcessed : false;
return output;
}
// This is the basic delta algorithm, which can be used in case the cloud service does not have
// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
// the file system do not.
function basicDelta(path, getDirStatFn, options) {
return __awaiter(this, void 0, void 0, function* () {
const outputLimit = 50;
const itemIds = yield options.allItemIdsHandler();
if (!Array.isArray(itemIds))
throw new Error('Delta API not supported - local IDs must be provided');
const logger = options && options.logger ? options.logger : new Logger_1.default();
const context = basicDeltaContextFromOptions_(options);
if (context.timestamp > Date.now()) {
logger.warn(`BasicDelta: Context timestamp is greater than current time: ${context.timestamp}`);
logger.warn('BasicDelta: Sync will continue but it is likely that nothing will be synced');
}
const newContext = {
timestamp: context.timestamp,
filesAtTimestamp: context.filesAtTimestamp.slice(),
statsCache: context.statsCache,
statIdsCache: context.statIdsCache,
deletedItemsProcessed: context.deletedItemsProcessed,
};
// Stats are cached until all items have been processed (until hasMore is false)
if (newContext.statsCache === null) {
newContext.statsCache = yield getDirStatFn(path);
newContext.statsCache.sort(function (a, b) {
return a.updated_time - b.updated_time;
});
newContext.statIdsCache = newContext.statsCache.filter((item) => BaseItem_1.default.isSystemPath(item.path)).map((item) => BaseItem_1.default.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
let output = [];
const updateReport = {
timestamp: context.timestamp,
older: 0,
newer: 0,
equal: 0,
};
// Find out which files have been changed since the last time. Note that we keep
// both the timestamp of the most recent change, *and* the items that exactly match
// this timestamp. This to handle cases where an item is modified while this delta
// function is running. For example:
// t0: Item 1 is changed
// t0: Sync items - run delta function
// t0: While delta() is running, modify Item 2
// Since item 2 was modified within the same millisecond, it would be skipped in the
// next sync if we relied exclusively on a timestamp.
for (let i = 0; i < newContext.statsCache.length; i++) {
const stat = newContext.statsCache[i];
if (stat.isDir)
continue;
if (stat.updated_time < context.timestamp) {
updateReport.older++;
continue;
}
// Special case for items that exactly match the timestamp
if (stat.updated_time === context.timestamp) {
if (context.filesAtTimestamp.indexOf(stat.path) >= 0) {
updateReport.equal++;
continue;
}
}
if (stat.updated_time > newContext.timestamp) {
newContext.timestamp = stat.updated_time;
newContext.filesAtTimestamp = [];
updateReport.newer++;
}
newContext.filesAtTimestamp.push(stat.path);
output.push(stat);
if (output.length >= outputLimit)
break;
}
logger.info(`BasicDelta: Report: ${JSON.stringify(updateReport)}`);
if (!newContext.deletedItemsProcessed) {
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
// Note that when deleted items are processed it might result in the output having
// more items than outputLimit. This is acceptable since delete operations are cheap.
const deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
deletedItems.push({
path: BaseItem_1.default.systemPath(itemId),
isDeleted: true,
});
}
}
const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0;
// If more than 90% of the notes are going to be deleted, it's most likely a
// configuration error or bug. For example, if the user moves their Nextcloud
// directory, or if a network drive gets disconnected and returns an empty dir
// instead of an error. In that case, we don't wipe out the user data, unless
// they have switched off the fail-safe.
if (options.wipeOutFailSafe && percentDeleted >= 0.90)
throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe');
output = output.concat(deletedItems);
}
newContext.deletedItemsProcessed = true;
const hasMore = output.length >= outputLimit;
if (!hasMore) {
// Clear temporary info from context. It's especially important to remove deletedItemsProcessed
// so that they are processed again on the next sync.
newContext.statsCache = null;
newContext.statIdsCache = null;
delete newContext.deletedItemsProcessed;
}
return {
hasMore: hasMore,
context: newContext,
items: output,
};
});
}
exports.basicDelta = basicDelta;
//# sourceMappingURL=file-api.js.map

View File

@ -137,6 +137,30 @@ const markdownUtils = {
return output.join('\n');
},
countTableColumns(line: string) {
if (!line) return 0;
const trimmed = line.trim();
let pipes = (line.match(/\|/g) || []).length;
if (trimmed[0] === '|') { pipes -= 1; }
if (trimmed[trimmed.length - 1] === '|') { pipes -= 1; }
return pipes + 1;
},
matchingTableDivider(header: string, divider: string) {
if (!header || !divider) return false;
const invalidChars = divider.match(/[^\s\-:|]/g);
if (invalidChars) { return false; }
const columns = markdownUtils.countTableColumns(header);
const cols = markdownUtils.countTableColumns(divider);
return cols > 0 && (cols >= columns);
},
titleFromBody(body: string) {
if (!body) return '';
const mdLinkRegex = /!?\[([^\]]+?)\]\(.+?\)/g;

View File

@ -867,10 +867,21 @@ class Setting extends BaseModel {
section: 'appearance',
label: () => _('Editor font family'),
description: () =>
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
'is incorrect or empty, it will default to a generic monospace font.'),
_('If the font is incorrect or empty, it will default to a generic monospace font.'),
storage: SettingStorage.File,
},
'style.editor.monospaceFontFamily': {
value: '',
type: SettingItemType.String,
public: true,
appTypes: ['desktop'],
section: 'appearance',
label: () => _('Editor monospace font family'),
description: () =>
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
'is incorrect or empty, it will default to a generic monospace font.'),
storage: SettingStorage.File,
},
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: ['desktop'] },

View File

@ -39,32 +39,45 @@ export default class JoplinSettings {
}
/**
* Registers a new setting. Note that registering a setting item is dynamic and will be gone next time Joplin starts.
* Registers new settings.
* Note that registering a setting item is dynamic and will be gone next time Joplin starts.
* What it means is that you need to register the setting every time the plugin starts (for example in the onStart event).
* The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some
* reason the plugin fails to start at some point.
*/
public async registerSettings(settings: Record<string, SettingItem>) {
for (const [key, setting] of Object.entries(settings)) {
const internalSettingItem: InternalSettingItem = {
key: key,
value: setting.value,
type: setting.type,
public: setting.public,
label: () => setting.label,
description: (_appType: string) => setting.description,
};
if ('isEnum' in setting) internalSettingItem.isEnum = setting.isEnum;
if ('section' in setting) internalSettingItem.section = this.namespacedKey(setting.section);
if ('options' in setting) internalSettingItem.options = () => setting.options;
if ('appTypes' in setting) internalSettingItem.appTypes = setting.appTypes;
if ('secure' in setting) internalSettingItem.secure = setting.secure;
if ('advanced' in setting) internalSettingItem.advanced = setting.advanced;
if ('minimum' in setting) internalSettingItem.minimum = setting.minimum;
if ('maximum' in setting) internalSettingItem.maximum = setting.maximum;
if ('step' in setting) internalSettingItem.step = setting.step;
await Setting.registerSetting(this.namespacedKey(key), internalSettingItem);
}
}
/**
* @deprecated Use joplin.settings.registerSettings()
*
* Registers a new setting.
*/
public async registerSetting(key: string, settingItem: SettingItem) {
const internalSettingItem: InternalSettingItem = {
key: key,
value: settingItem.value,
type: settingItem.type,
public: settingItem.public,
label: () => settingItem.label,
description: (_appType: string) => settingItem.description,
};
if ('isEnum' in settingItem) internalSettingItem.isEnum = settingItem.isEnum;
if ('section' in settingItem) internalSettingItem.section = this.namespacedKey(settingItem.section);
if ('options' in settingItem) internalSettingItem.options = () => settingItem.options;
if ('appTypes' in settingItem) internalSettingItem.appTypes = settingItem.appTypes;
if ('secure' in settingItem) internalSettingItem.secure = settingItem.secure;
if ('advanced' in settingItem) internalSettingItem.advanced = settingItem.advanced;
if ('minimum' in settingItem) internalSettingItem.minimum = settingItem.minimum;
if ('maximum' in settingItem) internalSettingItem.maximum = settingItem.maximum;
if ('step' in settingItem) internalSettingItem.step = settingItem.step;
return Setting.registerSetting(this.namespacedKey(key), internalSettingItem);
this.plugin_.deprecationNotice('1.8', 'joplin.settings.registerSetting() is deprecated in favour of joplin.settings.registerSettings()');
await this.registerSettings({ [key]: settingItem });
}
/**

View File

@ -481,7 +481,7 @@ function shimInit(sharp = null, keytar = null, React = null) {
shim.httpAgent_ = null;
shim.httpAgent = url => {
if (shim.isLinux() && !shim.httpAgent_) {
if (!shim.httpAgent_) {
const AgentSettings = {
keepAlive: true,
maxSockets: 1,

View File

@ -1,61 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const tool_utils_1 = require("./tool-utils");
const sqlts = require('@rmp135/sql-ts').default;
const fs = require('fs-extra');
function main() {
return __awaiter(this, void 0, void 0, function* () {
// Run the CLI app once so as to generate the database file
process.chdir(`${tool_utils_1.rootDir}/packages/app-cli`);
yield tool_utils_1.execCommand2('npm start -- version');
const sqlTsConfig = {
'client': 'sqlite3',
'connection': {
'filename': `${require('os').homedir()}/.config/joplindev-desktop/database.sqlite`,
},
'tableNameCasing': 'pascal',
'singularTableNames': true,
'useNullAsDefault': true,
'excludedTables': [
'main.notes_fts',
'main.notes_fts_segments',
'main.notes_fts_segdir',
'main.notes_fts_docsize',
'main.notes_fts_stat',
],
};
const definitions = yield sqlts.toObject(sqlTsConfig);
definitions.tables = definitions.tables.map((t) => {
t.columns.push({
nullable: false,
name: 'type_',
type: 'int',
optional: true,
isEnum: false,
propertyName: 'type_',
propertyType: 'number',
});
return t;
});
const tsString = sqlts.fromObject(definitions, sqlTsConfig)
.replace(/": /g, '"?: ');
const header = `// AUTO-GENERATED BY ${__filename.substr(tool_utils_1.rootDir.length + 1)}`;
const targetFile = `${tool_utils_1.rootDir}/packages/lib/services/database/types.ts`;
console.info(`Writing type definitions to ${targetFile}...`);
yield fs.writeFile(targetFile, `${header}\n\n${tsString}`, 'utf8');
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
//# sourceMappingURL=generate-database-types.js.map

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Ettore Atalan <atalanttore@users.noreply.github.com>\n"
"Last-Translator: pomeloy <45542782+pomeloy@users.noreply.github.com>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
@ -412,21 +412,23 @@ msgstr "Neue(s) %s wird erstellt ..."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:284
#, fuzzy
msgid "Click to add tags..."
msgstr "Auf Aktualisierungen prüfen ..."
msgstr "Klicke, um Schlagwörter hinzuzufügen ..."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:337
msgid ""
"This Rich Text editor has a number of limitations and it is recommended to "
"be aware of them before using it."
msgstr ""
"Dieser Rich Text Editor hat eine Reihe von Unzulänglichkeiten, die bei der "
"Nutzung beachtet werden sollten."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:341
msgid "Read more about it"
msgstr ""
msgstr "Mehr erfahren"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:346
msgid "Dismiss"
msgstr ""
msgstr "Ausblenden"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:382
msgid "The following attachments are being watched for changes:"
@ -492,19 +494,19 @@ msgstr "Notiz löschen?"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
msgid "Undo"
msgstr ""
msgstr "Rückgängig"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
msgid "Redo"
msgstr ""
msgstr "Wiederholen"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:100
msgid "Indent less"
msgstr ""
msgstr "Ausrücken"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:104
msgid "Indent more"
msgstr ""
msgstr "Einrücken"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:108
#, fuzzy
@ -518,11 +520,11 @@ msgstr "Die ausgewählte Notiz bearbeiten"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:116
msgid "Swap line up"
msgstr ""
msgstr "Zeile nach oben verschieben"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:120
msgid "Swap line down"
msgstr ""
msgstr "Zeile nach unten verschieben"
#: packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js:16
msgid "Note title"
@ -1401,7 +1403,7 @@ msgstr "Erweiterte Einstellungen anzeigen"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:406
msgid "Path:"
msgstr ""
msgstr "Pfad:"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:411
msgid "Browse..."
@ -1409,7 +1411,7 @@ msgstr "Durchsuchen ..."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:413
msgid "Arguments:"
msgstr ""
msgstr "Kommandozeilenargumente:"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:461
msgid "The application must be restarted for these changes to take effect."
@ -1433,7 +1435,7 @@ msgstr "Hole es jetzt:"
#: packages/app-desktop/gui/ConfigScreen/Sidebar.js:89
#: packages/lib/models/Setting.js:1524
msgid "Plugins"
msgstr "Zusatzprogramme"
msgstr "Erweiterungen"
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.js:27
msgid "Apply"
@ -1442,34 +1444,34 @@ msgstr "Anwenden"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:113
#, javascript-format
msgid "Delete plugin \"%s\"?"
msgstr "Plugin „%s“ löschen?"
msgstr "Erweiterung „%s“ löschen?"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:155
#, fuzzy
msgid "Browse all plugins"
msgstr "Plugin installieren"
msgstr "Erweiterungen durchsuchen"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:159
msgid "Install from file"
msgstr ""
msgstr "Aus Datei installieren"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:193
msgid "You do not have any installed plugin."
msgstr ""
msgstr "Es sind keine Erweiterungen installiert."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:208
#, fuzzy
msgid "Plugin tools"
msgstr "Zusatzprogramme"
msgstr "Erweiterungen"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:209
msgid "Manage your plugins"
msgstr ""
msgstr "Erweiterungen verwalten"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js:68
#, fuzzy
msgid "No results"
msgstr "Keine Anhänge!"
msgstr "Keine Ergebnisse"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js:80
#, fuzzy
@ -1479,36 +1481,36 @@ msgstr "Suchen ..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:127
#, fuzzy
msgid "Install"
msgstr "Plugin installieren"
msgstr "Erweiterung installieren"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:129
#, fuzzy
msgid "Installing..."
msgstr "Wird abgebrochen ..."
msgstr "Wird installiert ..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:131
#, fuzzy
msgid "Installed"
msgstr "Plugin installieren"
msgstr "Erweiterung installiert"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:137
#, fuzzy
msgid "Update"
msgstr "Aktualisiert"
msgstr "Aktualisieren"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:139
#, fuzzy
msgid "Updating..."
msgstr "Zuschauend ..."
msgstr "Wird aktualisiert ..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:151
msgid "Please upgrade Joplin to use this plugin"
msgstr ""
msgstr "Bitte aktualisiere Joplin, um diese Erweiterung zu nutzen"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:165
#, javascript-format
msgid "(%s)"
msgstr ""
msgstr "(%s)"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js:49
#, fuzzy, javascript-format
@ -2640,7 +2642,7 @@ msgid ""
"The editor command (may include arguments) that will be used to open a note. "
"If none is provided it will try to auto-detect the default editor."
msgstr ""
"Der Editor-Befehl (kann Argumente enthalten), der zum Öffnen einer Notiz "
"Der Editor-Befehl (kann Kommandozeilenargumente enthalten), der zum Öffnen einer Notiz "
"verwendet wird. Wenn keiner angegeben wird, wird versucht, den Standard-"
"Editor automatisch zu erkennen."
@ -2835,14 +2837,14 @@ msgid ""
"formatting. It is indicated below which plugins are compatible or not with "
"the WYSIWYG editor."
msgstr ""
"Diese Zusatzprogramme erweitern den Markdown-Renderer um zusätzliche "
"Diese Erweiterungen erweitern den Markdown-Renderer um zusätzliche "
"Funktionen. Bitte beachte, dass diese Funktionen zwar nützlich sein können, "
"es sich dabei jedoch nicht um Standard-Markdown handelt und die meisten von "
"ihnen daher nur in Joplin funktionieren. Außerdem sind einige von ihnen "
"*inkompatibel* mit dem WYSIWYG-Editor. Wenn du eine Notiz, die eines dieser "
"Zusatzprogramme verwendet, in diesem Editor öffnest, verlierst du die "
"Formatierung des Zusatzprogramms. Es ist unten angegeben, welche "
"Zusatzprogramme mit dem WYSIWYG-Editor kompatibel sind oder nicht."
"*inkompatibel* mit dem WYSIWYG-Editor. Wenn du eine Notiz, die eine dieser "
"Erweiterungen verwendet, in diesem Editor öffnest, verlierst du die "
"Formatierung der Erweiterung. Es ist unten angegeben, welche "
"Erweiterungen mit dem WYSIWYG-Editor kompatibel sind oder nicht."
#: packages/lib/models/Setting.js:1543
#, javascript-format

View File

@ -15,6 +15,8 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.2\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: packages/app-desktop/bridge.js:106 packages/app-desktop/bridge.js:110
#: packages/app-desktop/bridge.js:126 packages/app-desktop/bridge.js:134
@ -60,7 +62,7 @@ msgstr "Annulla"
msgid ""
"The app is now going to close. Please relaunch it to complete the process."
msgstr ""
"Ora l'applicazione si chiuderà. Per favore rilancia per completare il "
"Ora l'applicazione si chiuderà. Per favore riavviala per completare il "
"processo."
#: packages/app-desktop/plugins/GotoAnything.js:431
@ -76,7 +78,7 @@ msgstr ""
#: packages/app-desktop/plugins/GotoAnything.js:456
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:20
msgid "Goto Anything..."
msgstr "Goto..."
msgstr "Cerca ovunque..."
#: packages/app-desktop/plugins/GotoAnything.js:463
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:28
@ -86,7 +88,7 @@ msgstr "Comandi"
#: packages/app-desktop/InteropServiceHelper.js:154
#, javascript-format
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "Esportazione da \"%s\" in formato \"%s\". Si prega di attendere..."
msgstr "Esportazione da \"%s\" in formato \"%s\". Per favore attendere..."
#: packages/app-desktop/InteropServiceHelper.js:174
#, javascript-format
@ -256,7 +258,8 @@ msgstr "Focus"
#: packages/app-desktop/gui/StatusScreen/StatusScreen.js:26
msgid "Please select where the sync status should be exported to"
msgstr ""
"Prego selezionare dove lo stato della sincronizzazione deve essere esportato"
"Per favore selezionare dove lo stato della sincronizzazione deve essere "
"esportato"
#: packages/app-desktop/gui/StatusScreen/StatusScreen.js:61
#: packages/app-mobile/components/screens/status.js:112
@ -407,23 +410,24 @@ msgid "Creating new %s..."
msgstr "Creare nuovo %s..."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:284
#, fuzzy
msgid "Click to add tags..."
msgstr "Controlla aggiornamenti..."
msgstr "Clicca per aggiungere etichetta..."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:337
msgid ""
"This Rich Text editor has a number of limitations and it is recommended to "
"be aware of them before using it."
msgstr ""
"Questo editor di testo RTF ha un certo numero di limitazioni e si raccomanda "
"di conoscerle prima di utilizzarlo."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:341
msgid "Read more about it"
msgstr ""
msgstr "Per saperne di più"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:346
msgid "Dismiss"
msgstr ""
msgstr "Non visualizzare più"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:382
msgid "The following attachments are being watched for changes:"
@ -483,43 +487,40 @@ msgid "Horizontal Rule"
msgstr "Riga orizzontale"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:88
#, fuzzy
msgid "Delete line"
msgstr "Eliminare la nota?"
msgstr "Elimina riga"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
msgid "Undo"
msgstr ""
msgstr "Annulla"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
msgid "Redo"
msgstr ""
msgstr "Ripristina"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:100
msgid "Indent less"
msgstr ""
msgstr "Meno rientro"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:104
msgid "Indent more"
msgstr ""
msgstr "Più rientro"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:108
#, fuzzy
msgid "Toggle comment"
msgstr "Attiva lista delle note"
msgstr "Attiva / disattiva commento"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:112
#, fuzzy
msgid "Sort selected lines"
msgstr "Modifica la nota selezionata"
msgstr "Ordina alfabeticamente le righe selezionate"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:116
msgid "Swap line up"
msgstr ""
msgstr "Sposta la riga sopra"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:120
msgid "Swap line down"
msgstr ""
msgstr "Sposta la riga sotto"
#: packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js:16
msgid "Note title"
@ -1387,7 +1388,7 @@ msgstr "Mostra opzioni avanzate"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:406
msgid "Path:"
msgstr ""
msgstr "Percorso:"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:411
msgid "Browse..."
@ -1395,12 +1396,12 @@ msgstr "Naviga..."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:413
msgid "Arguments:"
msgstr ""
msgstr "Argomenti:"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:461
msgid "The application must be restarted for these changes to take effect."
msgstr ""
"L'applicazione deve essere riavviata perché le modificano siano attive."
"L'applicazione deve essere riavviata affinché le modifiche siano attive."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:473
#: packages/app-desktop/gui/NoteList/NoteList.js:163
@ -1430,75 +1431,66 @@ msgid "Delete plugin \"%s\"?"
msgstr "Eliminare il plugin \"%s\"?"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:155
#, fuzzy
msgid "Browse all plugins"
msgstr "Installa plugin"
msgstr "Sfoglia tutti i plugins"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:159
msgid "Install from file"
msgstr ""
msgstr "Installa da file"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:193
msgid "You do not have any installed plugin."
msgstr ""
msgstr "Non hai installato nessun plugin."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:208
#, fuzzy
msgid "Plugin tools"
msgstr "Plugins"
msgstr "Strumenti plugin"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:209
msgid "Manage your plugins"
msgstr ""
msgstr "Gestisci plugins"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js:68
#, fuzzy
msgid "No results"
msgstr "Nessuna risorsa!"
msgstr "Nessun risultato"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js:80
#, fuzzy
msgid "Search for plugins..."
msgstr "Cerca..."
msgstr "Cerca plugins..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:127
#, fuzzy
msgid "Install"
msgstr "Installa plugin"
msgstr "Installa"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:129
#, fuzzy
msgid "Installing..."
msgstr "Annullamento..."
msgstr "Installazione..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:131
#, fuzzy
msgid "Installed"
msgstr "Installa plugin"
msgstr "Installato"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:137
#, fuzzy
msgid "Update"
msgstr "Aggiornato"
msgstr "Aggiorna"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:139
#, fuzzy
msgid "Updating..."
msgstr "Osservare..."
msgstr "Aggiornamento..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:151
msgid "Please upgrade Joplin to use this plugin"
msgstr ""
msgstr "Aggiorna Joplin per utilizzare questo plugin"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:165
#, javascript-format
msgid "(%s)"
msgstr ""
msgstr "(%s)"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js:49
#, fuzzy, javascript-format
#, javascript-format
msgid "Could not install plugin: %s"
msgstr "Impossibile esportare le note: %s"
msgstr "Non è possibile installare il plugin: %s"
#: packages/app-desktop/gui/PromptDialog.min.js:249
msgid "Clear"
@ -1975,7 +1967,7 @@ msgstr "Sito web Joplin"
#: packages/app-mobile/components/screens/config.js:523
msgid "Privacy Policy"
msgstr ""
msgstr "Politica sulla Privacy"
#: packages/app-mobile/components/screens/Note.js:98
msgid "This note has been modified:"
@ -2104,12 +2096,14 @@ msgid ""
"The default admin password is insecure and has not been changed! [Change it "
"now](%s)"
msgstr ""
"La password d'amministratore predefinita non è sicura e non è stata "
"modificata! [Modificala ora](%s)"
#: packages/lib/onedrive-api-node-utils.js:46
#, javascript-format
msgid "All potential ports are in use - please report the issue at %s"
msgstr ""
"Tutte le potenziali porte sono in uso - prego riportare il problema a %s"
"Tutte le potenziali porte sono in uso - per favore riportare il problema a %s"
#: packages/lib/onedrive-api-node-utils.js:86
msgid ""
@ -2145,9 +2139,8 @@ msgid "Dropbox"
msgstr "Dropbox"
#: packages/lib/SyncTargetJoplinServer.js:30
#, fuzzy
msgid "Joplin Server"
msgstr "Sito web Joplin"
msgstr "Joplin Server"
#: packages/lib/shim-init-node.js:212
#, javascript-format
@ -2289,21 +2282,19 @@ msgstr "Secret AWS"
#: packages/lib/models/Setting.js:293
msgid "Joplin Server URL"
msgstr ""
msgstr "URL Joplin Server"
#: packages/lib/models/Setting.js:308
#, fuzzy
msgid "Joplin Server Directory"
msgstr "Cartella di esportazione di Joplin"
msgstr "Cartella di Joplin Server"
#: packages/lib/models/Setting.js:319
msgid "Joplin Server username"
msgstr ""
msgstr "Nome Utente Joplin Server"
#: packages/lib/models/Setting.js:330
#, fuzzy
msgid "Joplin Server password"
msgstr "Inserisci password principale:"
msgstr "Password Joplin Server"
#: packages/lib/models/Setting.js:342
msgid "Attachment download behaviour"
@ -2432,9 +2423,8 @@ msgid "Enable typographer support"
msgstr "Attiva supporto tipografico"
#: packages/lib/models/Setting.js:614
#, fuzzy
msgid "Enable Linkify"
msgstr "Attiva cronologia della nota"
msgstr "Abilita Linkify"
#: packages/lib/models/Setting.js:615
msgid "Enable math expressions"
@ -2449,17 +2439,16 @@ msgid "Enable Mermaid diagrams support"
msgstr "Attiva supporto diagrammi Mermaid"
#: packages/lib/models/Setting.js:618
#, fuzzy
msgid "Enable audio player"
msgstr "Abilita emoji markdown"
msgstr "Abilita riproduttore audio"
#: packages/lib/models/Setting.js:619
msgid "Enable video player"
msgstr ""
msgstr "Abilita riproduttore video"
#: packages/lib/models/Setting.js:620
msgid "Enable PDF viewer"
msgstr ""
msgstr "Abilita visualizzatore PDF"
#: packages/lib/models/Setting.js:621
msgid "Enable ==mark== syntax"
@ -2902,7 +2891,7 @@ msgid ""
"Error. Please check that URL, username, password, etc. are correct and that "
"the sync target is accessible. The reported error was:"
msgstr ""
"Errore. Prego controllare che URL, nome utente, password, etc. siano "
"Errore. Per favore controllare che URL, nome utente, password, etc. siano "
"corretti e che la destinazione di sincronizzazione sia accessibile. L’errore "
"riportato era:"
@ -2923,7 +2912,7 @@ msgstr ""
"\n"
"%s\n"
"\n"
"Riprovare prego."
"Riprovare per favore."
#: packages/lib/components/shared/encryption-config-shared.js:33
msgid ""
@ -3026,20 +3015,20 @@ msgstr "In corso"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
"Tipo elemento scaricato sconosciuto - prego aggiornare Joplin all’ultima "
"Elemento scaricato sconosciuto - cortesemente aggiornare Joplin all’ultima "
"versione"
#: packages/lib/JoplinServerApi.js:63
#, fuzzy, javascript-format
#, javascript-format
msgid ""
"Could not connect to Joplin Server. Please check the Synchronisation options "
"in the config screen. Full error was:\n"
"\n"
"%s"
msgstr ""
"Non è stato possibile connettersi alla Joplin Nextcloud app. Si prega di "
"controllare la configurazione nella schermata di Config. della "
"Sincronizzazione. Errore completo era:\n"
"Non è stato possibile connettersi al Joplin Server. Per favore controllare "
"le opzioni di sincronizzazione nella relativa schermata di configurazione. "
"Errore completo era:\n"
"\n"
"%s"
@ -3194,10 +3183,13 @@ msgstr "comando"
msgid "\"%s\" is missing the required \"%s\" property."
msgstr "A \"% s\" manca la proprietà \"% s\" richiesta."
# Non è chiaro cosa sia un accelerator senza contesto, non lo trovo nell'applicazione e non credo sia corretto tradurlo con acceleratore
# Non è chiaro cosa sia un accelerator senza contesto, non lo trovo nell'applicazione e non credo sia corretto tradurlo con acceleratore.
# Cercando online ho trovato che
# "accelerator" fa parte di opzioni
# relative ai comandi delle API dei
# Plugins.
#: packages/lib/services/KeymapService.js:278
#: packages/lib/services/KeymapService.js:285
#, fuzzy
msgid "accelerator"
msgstr "accelerator"
@ -3206,14 +3198,15 @@ msgstr "accelerator"
msgid "Invalid %s: %s."
msgstr "%s non valido: %s."
# Non è chiaro cosa sia un accelerator senza contesto, non lo trovo nell'applicazione e non credo sia corretto tradurlo con acceleratore
# Non è chiaro cosa sia un accelerator senza contesto, non lo trovo nell'applicazione e non credo sia corretto tradurlo con acceleratore.
# Cercando online ho trovato che "accelerator" fa parte di opzioni relative ai comandi delle API dei Plugins.
#: packages/lib/services/KeymapService.js:303
#, fuzzy, javascript-format
#, javascript-format
msgid ""
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
"unexpected behaviour."
msgstr ""
"L'accelerator \"%s\" è utilizzato per i comandi \"%s\" e \"%s\". Questo "
"Accelerator \"%s\" è utilizzato per i comandi \"%s\" e \"%s\". Questo "
"potrebbe portare a comportamenti inaspettati."
#: packages/lib/services/KeymapService.js:328
@ -3286,9 +3279,8 @@ msgid "Downloaded and encrypted"
msgstr "Scaricato e criptato"
#: packages/lib/services/ReportService.js:180
#, fuzzy
msgid "Created locally"
msgstr "Elementi locali creati: %d."
msgstr "Creato localmente"
#: packages/lib/services/ReportService.js:191
msgid "Attachments that could not be downloaded"
@ -3505,8 +3497,8 @@ msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Avvio decrittazione... Attendere prego, ci potrebbero volere diversi minuti "
"per la decriptazione."
"Avvio decrittazione... Attendere per favore, ci potrebbero volere diversi "
"minuti per la decriptazione."
#: packages/app-cli/app/command-e2ee.js:55
#, javascript-format
@ -3953,7 +3945,8 @@ msgstr "s"
#: packages/app-cli/app/app.js:170
msgid "Cancelling background synchronisation... Please wait."
msgstr "Annullamento della sincronizzazione in background... Attendere prego."
msgstr ""
"Annullamento della sincronizzazione in background... Attendere per favore."
#: packages/app-cli/app/app.js:255
#, javascript-format

View File

@ -66,7 +66,7 @@ joplin.plugins.register({
// This event will be triggered when the content of the note changes
// as you also want to update the TOC in this case.
await joplin.workspace.onNoteContentChange(() => {
await joplin.workspace.onNoteChange(() => {
updateTocView();
});
@ -171,7 +171,7 @@ joplin.plugins.register({
onStart: async function() {
// Create the panel object
const panel = await joplin.views.panels.create();
const panel = await joplin.views.panels.create('panel_1');
// Set some initial content while the TOC is being created
await joplin.views.panels.setHtml(panel, 'Loading...');
@ -228,7 +228,7 @@ Now run the plugin again and you should see the TOC dynamically updating as you
In order to better integrate the TOC to Joplin, you might want to style it using CSS. To do so, first add a `webview.css` file next to `index.ts`, then you will need to let Joplin know about this file. This is done using the `addScript()` function (which is also used to add JavaScript files as we'll see later), like so:
```typescript
const panel = await joplin.views.panels.create();
const panel = await joplin.views.panels.create('panel_1');
// Add the CSS file to the view, right after it has been created:
await joplin.views.panels.addScript(panel, './webview.css');
```
@ -261,8 +261,7 @@ The next step is to make the TOC interactive so that when the user clicks on a l
```typescript
// In index.ts
const panel = joplin.views.createWebviewPanel();
const panel = await joplin.views.panels.create('panel_1');
await joplin.views.panels.addScript(panel, './webview.css');
await joplin.views.panels.addScript(panel, './webview.js'); // Add the JS file
```
@ -314,7 +313,7 @@ Then from the plugin, in `src/index.ts`, you can listen to this message using th
```typescript
joplin.plugins.register({
onStart: async function() {
const panel = await joplin.views.panels.create();
const panel = await joplin.views.panels.create('panel_1');
// ...

View File

@ -12,7 +12,7 @@ The notes can be [synchronised](#synchronisation) with various targets including
Operating system | Method
-----------------|----------------
macOS, Linux, or Windows (via [WSL](https://docs.microsoft.com/en-us/windows/wsl/faq)) | **Important:** First, [install Node 10+](https://nodejs.org/en/download/package-manager/).<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
macOS, Linux, or Windows (via [WSL](https://docs.microsoft.com/en-us/windows/wsl/faq)) | **Important:** First, [install Node 12+](https://nodejs.org/en/download/package-manager/).<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
To start it, type `joplin`.
@ -364,7 +364,7 @@ The following commands are available in [command-line mode](#command-line-mode):
fa (Persian), pl_PL (Polski),
pt_PT (Português),
pt_BR (Português (Brasil)), ro (Română),
sl_SI (Slovenian), sv (Svenska),
sl_SI (Slovenian), sv (Svenska),
th_TH (Thai), vi (Tiếng Việt),
tr_TR (Türkçe), el_GR (Ελληνικά),
ru_RU (Русский), sr_RS (српски језик),