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:
commit
3fa13828cd
@ -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
3
.gitignore
vendored
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 (српски језик),
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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 });
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 };
|
||||
},
|
||||
|
||||
|
@ -109,6 +109,11 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.rdtPicker {
|
||||
min-width: 250px;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.smalltalk {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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;
|
||||
|
@ -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'] },
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
||||
// ...
|
||||
|
||||
|
@ -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 (српски језик),
|
||||
|
Loading…
Reference in New Issue
Block a user