1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Laurent Cozic
4810c01aec Android release v1.8.1 2021-03-29 11:48:22 +02:00
Laurent Cozic
b3f950e67a Desktop release v1.8.1 2021-03-29 11:26:04 +02:00
Laurent Cozic
f43edc2736 Update package lock 2021-03-29 11:25:14 +02:00
Martin Cejp
ea65346f04 Linux: Joplin_install_and_update.sh: restore compatibility with older wget versions (#4752)
The `--show-progress` flag was introduced in wget 1.16 (2014). CentOS 7 only has wget 1.14.
2021-03-29 09:59:45 +01:00
Anakai Richards
a2a80bc2e3 Desktop, Mobile: Resolves #3887: Improved solarized dark theme (#4748) 2021-03-29 09:46:47 +01:00
Nishant Mittal
9cf5974c7b Desktop: Fixes #4602: Allow copying images from Joplin to external editors (#4724) 2021-03-29 09:40:50 +01:00
Roman Musin
66ad2259a9 Tools: Change windows build tools install command (#4737)
Based on issues some people were having, e.g. https://discourse.joplinapp.org/t/build-error-on-windows-linux-arch/16104/2
2021-03-29 09:36:39 +01:00
mbalint
c516ab405b Mobile: Resolves #720: Add "Sync only on Wi-Fi" option (#4729) 2021-03-29 09:35:39 +01:00
mrkaato
17295734fd All: Translation: Update fi_FI.po (#4745)
* Updated Translation in Finnish

Updated all and corrected a few errors in the previous translation.

* Update fi_FI.po

-tdd fixed -tnt
2021-03-28 13:35:51 -04:00
Laurent Cozic
3fa13828cd Merge branch 'dev' of github.com:laurent22/joplin into dev 2021-03-26 17:56:18 +01:00
iamtalwinder
99b55129f2 Doc: Update TOC plugin (#4708) 2021-03-26 09:10:06 +00:00
Roman Musin
49e6b5cf62 Desktop: Set keep-alive for WebDAV/Nextcloud sync (#4668) 2021-03-26 09:09:19 +00:00
Caleb John
81b3ddf0e7 Desktop: Add monospace enforcement for certain elements in Markdown editor (#4689) 2021-03-26 09:08:22 +00:00
Tamás Hetesi
e2db02887c Mobile: Fix flash of white when loading note bodies (#4629)
Refs #4450
2021-03-26 09:02:16 +00:00
Helmut K. C. Tessarek
0da3881aa6 add generated files to ignore files 2021-03-25 01:30:07 -04:00
mbalint
cd1f95c5ea Desktop: Fixes #4723: Plugin Update error when plugin was installed manually (#4725) 2021-03-24 14:15:49 +00:00
Helmut K. C. Tessarek
c119821c19 Doc: update node version on terminal page 2021-03-24 08:02:02 -04:00
Harshit Kathuria
656673ed57 Desktop: Fixes #4397: Fixed calendar styling (#4703) 2021-03-23 09:35:25 +00:00
mbalint
044477ba0d Desktop: Fixes #4683: Updating a disabled plugin enables it (#4711) 2021-03-23 09:01:34 +00:00
Jalaj
0985340a7f Desktop: Resolves #4614: Allow registering multiple settings in one call (#4627) 2021-03-23 09:01:02 +00:00
pomeloy
9e58af7232 All: Translation: Update de_DE.po (#4720) 2021-03-22 19:55:17 -04:00
Roman Musin
5b65186b4d Delete generated .js files (#4717) 2021-03-21 19:56:33 +00:00
Sandro
a0ec926ba2 All: Translation: Update it_IT.po (#4707) 2021-03-18 18:28:24 -04:00
43 changed files with 784 additions and 1320 deletions

View File

@@ -121,6 +121,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/registry.d.ts
packages/app-cli/tests/registry.js
packages/app-cli/tests/registry.js.map
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
packages/app-cli/tests/services/plugins/RepositoryApi.js
packages/app-cli/tests/services/plugins/RepositoryApi.js.map
@@ -232,6 +235,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
@@ -397,6 +403,12 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.test.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.test.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map

12
.gitignore vendored
View File

@@ -108,6 +108,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/registry.d.ts
packages/app-cli/tests/registry.js
packages/app-cli/tests/registry.js.map
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
packages/app-cli/tests/services/plugins/RepositoryApi.js
packages/app-cli/tests/services/plugins/RepositoryApi.js.map
@@ -219,6 +222,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
@@ -384,6 +390,12 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.test.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/getCopyableContent.test.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map

View File

@@ -22,7 +22,7 @@ There are also a few forks of existing packages under the "fork-*" name.
- Install node 10+ - https://nodejs.org/en/
- macOS: Install Cocoapods - `brew install cocoapods`
- Windows: Install Windows Build Tools - `npm install -g windows-build-tools`
- Windows: Install Windows Build Tools - `npm install -g windows-build-tools --vs2015`
- Linux: Install dependencies - `sudo apt install libnss3 libsecret-1-dev python rsync`
## Building

View File

@@ -126,8 +126,8 @@ fi
#-----------------------------------------------------
print 'Downloading Joplin...'
TEMP_DIR=$(mktemp -d)
wget -qnv --show-progress -O ${TEMP_DIR}/Joplin.AppImage https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage
wget -qnv --show-progress -O ${TEMP_DIR}/joplin.png https://joplinapp.org/images/Icon512.png
wget -O ${TEMP_DIR}/Joplin.AppImage https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage
wget -O ${TEMP_DIR}/joplin.png https://joplinapp.org/images/Icon512.png
#-----------------------------------------------------
print 'Installing Joplin...'

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
import Setting from '@joplin/lib/models/Setting';
import { reg } from '@joplin/lib/registry';
const sync = {
start: jest.fn().mockReturnValue({}),
};
describe('Registry', function() {
let originalSyncTarget: typeof reg.syncTarget;
beforeAll(() => {
Setting.setConstant('env', 'prod');
originalSyncTarget = reg.syncTarget;
reg.syncTarget = () => ({
isAuthenticated: () => true,
synchronizer: () => sync,
});
});
afterAll(() => {
Setting.setConstant('env', 'dev');
reg.syncTarget = originalSyncTarget;
});
beforeEach(() => {
jest.useFakeTimers();
Setting.setValue('sync.interval', 300);
});
afterEach(() => {
Setting.setValue('sync.interval', 0);
reg.setupRecurrentSync();
});
describe('when on mobile data', () => {
beforeEach(() => {
Setting.setValue('sync.mobileWifiOnly', true);
reg.setIsOnMobileData(true);
});
it('should not sync automatically', () => {
reg.setupRecurrentSync();
jest.runOnlyPendingTimers();
expect(sync.start).toHaveBeenCalledTimes(0);
});
it('should sync if do wifi check is false', done => {
void reg.scheduleSync(1, null, false)
.then(() =>{
expect(sync.start).toHaveBeenCalled();
done();
});
jest.runOnlyPendingTimers();
});
it('should sync if "sync only over wifi" is disabled in settings', () => {
Setting.setValue('sync.mobileWifiOnly', false);
reg.setupRecurrentSync();
jest.runOnlyPendingTimers();
expect(sync.start).toHaveBeenCalled();
});
});
describe('when not on mobile data', () => {
beforeEach(() => {
Setting.setValue('sync.mobileWifiOnly', true);
reg.setIsOnMobileData(false);
});
it('should sync automatically', () => {
reg.setupRecurrentSync();
jest.runOnlyPendingTimers();
expect(sync.start).toHaveBeenCalled();
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import usePluginServiceRegistration from '../../utils/usePluginServiceRegistrati
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import { _, closestSupportedLocale } from '@joplin/lib/locale';
import useContextMenu from './utils/useContextMenu';
import getCopyableContent from './utils/getCopyableContent';
import shim from '@joplin/lib/shim';
const { MarkupToHtml } = require('@joplin/renderer');
@@ -1037,6 +1038,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
}
}
async function onCopy(event: any) {
const copiedContent = editor.selection.getContent();
const copyableContent = getCopyableContent(copiedContent);
clipboard.writeHTML(copyableContent);
event.preventDefault();
}
function onKeyDown(event: any) {
// It seems "paste as text" is handled automatically by
// on Windows so the code below so we need to run the below
@@ -1059,6 +1067,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
editor.on('keydown', onKeyDown);
editor.on('keypress', onKeypress);
editor.on('paste', onPaste);
editor.on('copy', onCopy);
// `compositionend` means that a user has finished entering a Chinese
// (or other languages that require IME) character.
editor.on('compositionend', onChangeHandler);
@@ -1074,6 +1083,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
editor.off('keydown', onKeyDown);
editor.off('keypress', onKeypress);
editor.off('paste', onPaste);
editor.off('copy', onCopy);
editor.off('compositionend', onChangeHandler);
editor.off('cut', onChangeHandler);
editor.off('joplinChange', onChangeHandler);

View File

@@ -0,0 +1,50 @@
import getCopyableContent from './getCopyableContent';
describe('getCopyableContent', () => {
test('should remove parameters from local images', () => {
const localImage = 'file:///home/some/path/test.jpg';
const content = `<div><img src="${localImage}?a=1&b=2"></div>`;
const copyableContent = getCopyableContent(content);
expect(copyableContent).toEqual(`<div><img src="${localImage}"></div>`);
});
test('should be able to process mutiple images', () => {
const localImage1 = 'file:///home/some/path/test1.jpg';
const localImage2 = 'file:///home/some/path/test2.jpg';
const localImage3 = 'file:///home/some/path/test3.jpg';
const content = `
<div>
<img src="${localImage1}?a=1&b=2">
<img src="${localImage2}">
<img src="${localImage3}?t=1">
</div>`;
const copyableContent = getCopyableContent(content);
const expectedContent = `
<div>
<img src="${localImage1}">
<img src="${localImage2}">
<img src="${localImage3}">
</div>`;
expect(copyableContent).toEqual(expectedContent);
});
test('should not change parameters for images on the internet', () => {
const image1 = 'http://www.somelink.com/image1.jpg';
const image2 = 'https://www.somelink.com/image2.jpg';
const content = `
<div>
<img src="${image1}">
<img src="${image2}?h=12&w=15">
</div>`;
const copyableContent = getCopyableContent(content);
expect(copyableContent).toEqual(content);
});
});

View File

@@ -0,0 +1,20 @@
import HtmlUtils from '@joplin/lib/htmlUtils';
export default function(htmlContent: string) {
// We need to remove extra url params from the image URLs while copying
// because some offline edtors do not show the image if there is
// an extra parameter in it's path.
// Related to - https://github.com/laurent22/joplin/issues/4602
const removeParametersFromUrl = (url: string) => {
const imageSrc = new URL(url);
// Remove parameters if it's a local image.
if (imageSrc.protocol === 'file:') {
imageSrc.search = '';
}
return imageSrc.toString();
};
return HtmlUtils.replaceImageUrls(htmlContent, removeParametersFromUrl);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.7.11",
"version": "1.8.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.8.0",
"version": "1.8.1",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

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

View File

@@ -141,8 +141,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097626
versionName "1.8.0"
versionCode 2097627
versionName "1.8.1"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- RN-NOTIFICATION -->
<uses-permission android:name="android.permission.VIBRATE" />

View File

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

View File

@@ -345,6 +345,14 @@ class SideMenuContentComponent extends Component {
);
}
if (this.props.syncOnlyOverWifi && this.props.isOnMobileData) {
items.push(
<Text key="net_info" style={this.styles().syncStatus}>
{ _('Mobile data - auto-sync disabled') }
</Text>
);
}
return <View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>{items}</View>;
}
@@ -404,6 +412,8 @@ const SideMenuContent = connect(state => {
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
isOnMobileData: state.isOnMobileData,
syncOnlyOverWifi: state.settings['sync.mobileWifiOnly'],
};
})(SideMenuContentComponent);

View File

@@ -254,6 +254,8 @@ PODS:
- React-Core
- react-native-image-resizer (1.3.0):
- React-Core
- react-native-netinfo (6.0.0):
- React-Core
- react-native-slider (3.0.3):
- React
- react-native-sqlite-storage (5.0.0):
@@ -338,7 +340,7 @@ PODS:
- React
- RNSecureRandom (1.0.0-rc.0):
- React
- RNShare (4.0.2):
- RNShare (5.1.3):
- React-Core
- RNVectorIcons (7.1.0):
- React
@@ -389,6 +391,7 @@ DEPENDENCIES:
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-image-resizer (from `../node_modules/react-native-image-resizer`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
- react-native-version-info (from `../node_modules/react-native-version-info`)
@@ -473,6 +476,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-image-picker"
react-native-image-resizer:
:path: "../node_modules/react-native-image-resizer"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-sqlite-storage:
@@ -557,6 +562,7 @@ SPEC CHECKSUMS:
react-native-geolocation: cbd9d6bd06bac411eed2671810f454d4908484a8
react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11
react-native-image-resizer: b53bf95ad880100e20262687e41f76fdbc9df255
react-native-netinfo: 34f4d7a42f49157f3b45c14217d256bce7dc9682
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
react-native-sqlite-storage: ce71689c5a73b79390a1ab213555ae80979a5dc7
react-native-version-info: 64f0f0bf3da6316298f9cd6085d50ba3a992d0c7
@@ -579,7 +585,7 @@ SPEC CHECKSUMS:
RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNSecureRandom: 1f19ad1492f7ed416b8fc79e92216a1f73f13a4c
RNShare: 7a7277f3c313652422d9de072ac50714dff5e8a4
RNShare: d98667ee0277d9cd068041e5f316a5fe67aa8536
RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59
Yoga: 7d13633d129fd179e01b8953d38d47be90db185a
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

View File

@@ -1091,6 +1091,11 @@
"resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-2.0.2.tgz",
"integrity": "sha512-tTNXRCgnhJBu79mulQwzabXRpDqfh/uaDqfHVpvF0nX4NTpolpy6mvTRiFg7eWFPGRArsnZz1EYp6rHfJWGgEA=="
},
"@react-native-community/netinfo": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-6.0.0.tgz",
"integrity": "sha512-Z9M8VGcF2IZVOo2x+oUStvpCW/8HjIRi4+iQCu5n+PhC7OqCQX58KYAzdBr///alIfRXiu6oMb+lK+rXQH1FvQ=="
},
"@react-native-community/push-notification-ios": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@react-native-community/push-notification-ios/-/push-notification-ios-1.6.0.tgz",

View File

@@ -18,6 +18,7 @@
"@react-native-community/clipboard": "^1.5.0",
"@react-native-community/datetimepicker": "^3.0.3",
"@react-native-community/geolocation": "^2.0.2",
"@react-native-community/netinfo": "^6.0.0",
"@react-native-community/push-notification-ios": "^1.6.0",
"@react-native-community/slider": "^3.0.3",
"buffer": "^5.0.8",

View File

@@ -29,6 +29,7 @@ import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
import NetInfo from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default;
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
const SafeAreaView = require('./components/SafeAreaView');
@@ -118,7 +119,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
if (action.type == 'NAV_GO') Keyboard.dismiss();
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
SearchEngine.instance().scheduleSyncTables();
}
@@ -151,7 +152,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
// Schedule a sync operation so that items that need to be encrypted
// are sent to sync target.
void reg.scheduleSync();
void reg.scheduleSync(null, null, true);
}
if (action.type == 'NAV_GO' && action.routeName == 'Notes') {
@@ -194,6 +195,7 @@ const appDefaultState = Object.assign({}, defaultState, {
route: DEFAULT_ROUTE,
noteSelectionEnabled: false,
noteSideMenuOptions: null,
isOnMobileData: false,
});
const appReducer = (state = appDefaultState, action: any) => {
@@ -359,6 +361,12 @@ const appReducer = (state = appDefaultState, action: any) => {
newState.noteSideMenuOptions = action.options;
break;
case 'MOBILE_DATA_WARNING_UPDATE':
newState = Object.assign({}, state);
newState.isOnMobileData = action.isOnMobileData;
break;
}
} catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
@@ -583,7 +591,9 @@ async function initialize(dispatch: Function) {
// When the app starts we want the full sync to
// start almost immediately to get the latest data.
void reg.scheduleSync(1000).then(() => {
// doWifiConnectionCheck set to true so initial sync
// doesn't happen on mobile data
void reg.scheduleSync(1000, null, true).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
@@ -646,6 +656,22 @@ class AppComponent extends React.Component {
state: 'initializing',
});
try {
// This will be called right after adding the event listener
// so there's no need to check netinfo on startup
this.unsubscribeNetInfoHandler_ = NetInfo.addEventListener(({ type, details }) => {
const isMobile = details.isConnectionExpensive || type === 'cellular';
reg.setIsOnMobileData(isMobile);
this.props.dispatch({
type: 'MOBILE_DATA_WARNING_UPDATE',
isOnMobileData: isMobile,
});
});
} catch (error) {
reg.logger().warn('Something went wrong while checking network info');
reg.logger().info(error);
}
await initialize(this.props.dispatch);
this.props.dispatch({
@@ -679,6 +705,7 @@ class AppComponent extends React.Component {
componentWillUnmount() {
AppState.removeEventListener('change', this.onAppStateChange_);
if (this.unsubscribeNetInfoHandler_) this.unsubscribeNetInfoHandler_();
}
componentDidUpdate(prevProps: any) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -867,10 +867,21 @@ class Setting extends BaseModel {
section: 'appearance',
label: () => _('Editor font family'),
description: () =>
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
'is incorrect or empty, it will default to a generic monospace font.'),
_('If the font is incorrect or empty, it will default to a generic monospace font.'),
storage: SettingStorage.File,
},
'style.editor.monospaceFontFamily': {
value: '',
type: SettingItemType.String,
public: true,
appTypes: ['desktop'],
section: 'appearance',
label: () => _('Editor monospace font family'),
description: () =>
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
'is incorrect or empty, it will default to a generic monospace font.'),
storage: SettingStorage.File,
},
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: ['desktop'] },
@@ -938,6 +949,15 @@ class Setting extends BaseModel {
},
storage: SettingStorage.File,
},
'sync.mobileWifiOnly': {
value: false,
type: SettingItemType.Bool,
section: 'sync',
public: true,
label: () => _('Synchronise only over WiFi connection'),
storage: SettingStorage.File,
appTypes: ['mobile'],
},
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: ['desktop'] },
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] },
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] },

View File

@@ -15,6 +15,7 @@ class Registry {
private scheduleSyncId_: any;
private recurrentSyncId_: any;
private db_: any;
private isOnMobileData_ = false;
logger() {
if (!this.logger_) {
@@ -38,6 +39,12 @@ class Registry {
this.showErrorMessageBoxHandler_(message);
}
// If isOnMobileData is true, the doWifiConnectionCheck is not set
// and the sync.mobileWifiOnly setting is true it will cancel the sync.
setIsOnMobileData(isOnMobileData: boolean) {
this.isOnMobileData_ = isOnMobileData;
}
resetSyncTarget(syncTargetId: number = null) {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
delete this.syncTargets_[syncTargetId];
@@ -74,7 +81,7 @@ class Registry {
}
};
scheduleSync = async (delay: number = null, syncOptions: any = null) => {
scheduleSync = async (delay: number = null, syncOptions: any = null, doWifiConnectionCheck: boolean = false) => {
this.schedSyncCalls_.push(true);
try {
@@ -104,6 +111,12 @@ class Registry {
this.scheduleSyncId_ = null;
this.logger().info('Preparing scheduled sync');
if (doWifiConnectionCheck && Setting.value('sync.mobileWifiOnly') && this.isOnMobileData_) {
this.logger().info('Sync cancelled because we\'re on mobile data');
promiseResolve();
return;
}
const syncTargetId = Setting.value('sync.target');
if (!(await this.syncTarget(syncTargetId).isAuthenticated())) {
@@ -191,7 +204,7 @@ class Registry {
this.recurrentSyncId_ = shim.setInterval(() => {
this.logger().info('Running background sync on timer...');
void this.scheduleSync(0);
void this.scheduleSync(0, null, true);
}, 1000 * Setting.value('sync.interval'));
}
} finally {

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ const theme: Theme = {
backgroundColor: '#002b36',
backgroundColorTransparent: 'rgba(0, 43, 54, 0.9)',
oddBackgroundColor: '#073642',
color: '#93a1a1', // For regular text
color: '#839496', // For regular text
colorError: '#dc322f',
colorWarn: '#cb4b16',
colorFaded: '#657b83', // For less important text;
@@ -18,9 +18,16 @@ const theme: Theme = {
backgroundColor2: '#073642',
color2: '#eee8d5',
selectedColor2: '#6c71c4',
selectedColor2: '#586e75',
colorError2: '#cb4b16',
backgroundColor3: '#012732',
backgroundColorHover3: '#2aa19870',
color3: '#93a1a1',
backgroundColor4: '#073642',
color4: '#93a1a1',
raisedBackgroundColor: '#073642',
raisedColor: '#839496',
@@ -29,7 +36,7 @@ const theme: Theme = {
tableBackgroundColor: '#002b36',
codeBackgroundColor: '#002b36',
codeBorderColor: '#696969',
codeColor: '#fdf6e3',
codeColor: '#839496',
codeMirrorTheme: 'solarized dark',
codeThemeCss: 'atom-one-dark-reasonable.css',

View File

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

View File

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

View File

@@ -7,7 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: mrkaato\n"
"Language-Team: \n"
"Language: fi_FI\n"
"MIME-Version: 1.0\n"
@@ -211,7 +213,7 @@ msgstr "Kaikki muistiinpanot"
#: packages/app-mobile/components/side-menu-content.js:338
#: packages/lib/commands/synchronize.js:18
msgid "Synchronise"
msgstr "Synkronoida"
msgstr "Synkronoi"
#: packages/app-desktop/gui/Sidebar/Sidebar.js:455
#: packages/app-mobile/components/side-menu-content.js:364
@@ -403,23 +405,24 @@ msgid "Creating new %s..."
msgstr "Luodaan uusi %s..."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:284
#, fuzzy
msgid "Click to add tags..."
msgstr "Tarkista päivitykset..."
msgstr "Klikkaa lisätäksesi tunnisteita..."
#: 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 ""
"Tällä Rich Text -editorilla on useita rajoituksia, on suositeltavaa olla "
"tietoinen niistä ennen sen käyttöä."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:341
msgid "Read more about it"
msgstr ""
msgstr "Lue lisää siitä"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:346
msgid "Dismiss"
msgstr ""
msgstr "Hylkää"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:382
msgid "The following attachments are being watched for changes:"
@@ -477,42 +480,40 @@ msgid "Horizontal Rule"
msgstr "Vaakaviiva"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:88
#, fuzzy
msgid "Delete line"
msgstr "Poistetaanko merkintä?"
msgstr "Poista rivi"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
msgid "Undo"
msgstr ""
msgstr "Kumoa"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
msgid "Redo"
msgstr ""
msgstr "Tee uudelleen"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:100
msgid "Indent less"
msgstr ""
msgstr "Sisennä vähemmän"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:104
msgid "Indent more"
msgstr ""
msgstr "Sisennä lisää"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:108
#, fuzzy
msgid "Toggle comment"
msgstr "Muistiinpanoluettelon vaihtaminen"
msgstr "Vaihda kommentti"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:112
msgid "Sort selected lines"
msgstr ""
msgstr "Lajittele valitut rivit"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:116
msgid "Swap line up"
msgstr ""
msgstr "Vaihda viiva ylös"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:120
msgid "Swap line down"
msgstr ""
msgstr "Vaihda viiva alas"
#: packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js:16
msgid "Note title"
@@ -648,9 +649,8 @@ msgid "Options"
msgstr "Vaihtoehdot"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:37
#, fuzzy
msgid "Invalid"
msgstr "Virheellinen vastaus %s: %s."
msgstr "Virheellinen"
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:49
msgid "Press the shortcut"
@@ -723,7 +723,7 @@ msgstr "Lisätietoja"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:559
msgid "Use the arrows to move the layout items. Press \"Escape\" to exit."
msgstr ""
msgstr "Siirrä asettelukohteita nuolilla. Paina \"Esc\" poistuaksesi."
#: packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js:18
msgid "Statistics..."
@@ -792,7 +792,7 @@ msgstr "Vaihda editoria"
#: packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js:16
msgid "Change application layout"
msgstr ""
msgstr "Sovelluksen asettelun muuttaminen"
#: packages/app-desktop/gui/MainScreen/commands/renameTag.js:30
msgid "Rename tag:"
@@ -809,7 +809,7 @@ msgstr "Muistikirjan otsikko:"
#: packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js:19
#: packages/lib/services/spellChecker/SpellCheckerService.js:180
msgid "Spell checker"
msgstr ""
msgstr "Oikeinkirjoituksen tarkistus"
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js:16
msgid "Share note..."
@@ -1033,7 +1033,7 @@ msgstr "Loitonna"
#: packages/app-desktop/gui/MenuBar.js:608
msgid "&Go"
msgstr ""
msgstr "&Mene"
#: packages/app-desktop/gui/MenuBar.js:620
msgid "&Note"
@@ -1234,13 +1234,12 @@ msgstr ""
"Tärkeää: sinun tarvitsee suorittaa tämä kerran vain yhdellä laitteella."
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:198
#, fuzzy
msgid "Re-encryption"
msgstr "Salaus"
msgstr "Uudelleen salaus"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:213
msgid "Ignore"
msgstr ""
msgstr "Ohita"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:244
#: packages/app-mobile/components/screens/encryption-config.js:215
@@ -1375,7 +1374,7 @@ msgstr "Näytä lisäasetukset"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:406
msgid "Path:"
msgstr ""
msgstr "Polku:"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:411
msgid "Browse..."
@@ -1383,11 +1382,12 @@ msgstr "Selaa..."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:413
msgid "Arguments:"
msgstr ""
msgstr "Väitteet:"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:461
msgid "The application must be restarted for these changes to take effect."
msgstr ""
"Sovellus on käynnistettävä uudelleen, jotta nämä muutokset tulevat voimaan."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:473
#: packages/app-desktop/gui/NoteList/NoteList.js:163
@@ -1396,12 +1396,11 @@ msgstr "Hae se nyt"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:473
msgid "Later"
msgstr ""
msgstr "Myöhemmin"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:526
#, fuzzy
msgid "Restart now"
msgstr "Hae se nyt:"
msgstr "Käynnistä uudelleen nyt"
#: packages/app-desktop/gui/ConfigScreen/Sidebar.js:89
#: packages/lib/models/Setting.js:1524
@@ -1413,77 +1412,71 @@ msgid "Apply"
msgstr "Käytä"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:113
#, fuzzy, javascript-format
#, javascript-format
msgid "Delete plugin \"%s\"?"
msgstr "Poista muistiinpano \"%s\"?"
msgstr "Poista laajennus \"%s\"?"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:155
msgid "Browse all plugins"
msgstr ""
msgstr "Selaa kaikkia laajennuksia"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:159
msgid "Install from file"
msgstr ""
msgstr "Asenna tiedostosta"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:193
msgid "You do not have any installed plugin."
msgstr ""
msgstr "Sinulla ei ole asennettua laajennusta."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:208
#, fuzzy
msgid "Plugin tools"
msgstr "Laajennukset"
msgstr "Laajennustyökalut"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js:209
msgid "Manage your plugins"
msgstr ""
msgstr "Hallitse laajennuksia"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js:68
#, fuzzy
msgid "No results"
msgstr "Ei resursseja!"
msgstr "Ei tuloksia"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js:80
#, fuzzy
msgid "Search for plugins..."
msgstr "Etsi..."
msgstr "Etsi laajennuksia..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:127
msgid "Install"
msgstr ""
msgstr "Asenna"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:129
#, fuzzy
msgid "Installing..."
msgstr "Peruutetaan..."
msgstr "Asentaa..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:131
msgid "Installed"
msgstr ""
msgstr "Asennettu"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:137
#, fuzzy
msgid "Update"
msgstr "Päivitetty"
msgstr "Päivitä"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:139
#, fuzzy
msgid "Updating..."
msgstr "Katsomassa..."
msgstr "Päivittää..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js:151
msgid "Please upgrade Joplin to use this plugin"
msgstr ""
msgstr "Päivitä Joplin käyttääksesi tätä laajennusta"
#: 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 "Muistiinpanoja ei voitu viedä: %s"
msgstr "Laajennuksen asentaminen epäonnistui: %s"
#: packages/app-desktop/gui/PromptDialog.min.js:249
msgid "Clear"
@@ -1958,7 +1951,7 @@ msgstr "Joplin verkkosivusto"
#: packages/app-mobile/components/screens/config.js:523
msgid "Privacy Policy"
msgstr ""
msgstr "Tietosuojakäytäntö"
#: packages/app-mobile/components/screens/Note.js:98
msgid "This note has been modified:"
@@ -2087,11 +2080,14 @@ msgid ""
"The default admin password is insecure and has not been changed! [Change it "
"now](%s)"
msgstr ""
"Järjestelmänvalvojan oletussalasana on epävarma, eikä sitä ole muutettu "
"[Vaihda se nyt] (%s)"
#: packages/lib/onedrive-api-node-utils.js:46
#, javascript-format
msgid "All potential ports are in use - please report the issue at %s"
msgstr ""
"Kaikki mahdolliset portit ovat käytössä - ilmoita ongelmasta osoitteessa %s"
#: packages/lib/onedrive-api-node-utils.js:86
msgid ""
@@ -2125,9 +2121,8 @@ msgid "Dropbox"
msgstr "Dropbox"
#: packages/lib/SyncTargetJoplinServer.js:30
#, fuzzy
msgid "Joplin Server"
msgstr "Joplin verkkosivusto"
msgstr "Joplin Palvelin"
#: packages/lib/shim-init-node.js:212
#, javascript-format
@@ -2221,7 +2216,8 @@ msgid ""
"parameters which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"Kohde, johon synkronoidaan. Jokaisella synkronointikohteella voi olla muita "
"parametreja, joiden nimi on `sync.NUM.NAME` (kaikki dokumentoitu jäljempänä)."
"parametreja, jotka on nimetty `sync.NUM.NAME` (kaikki dokumentoitu "
"jäljempänä)."
#: packages/lib/models/Setting.js:160
msgid "Directory to synchronise with (absolute path)"
@@ -2269,21 +2265,19 @@ msgstr "AWS secret"
#: packages/lib/models/Setting.js:293
msgid "Joplin Server URL"
msgstr ""
msgstr "Joplin palvelimen URL-osoite"
#: packages/lib/models/Setting.js:308
#, fuzzy
msgid "Joplin Server Directory"
msgstr "Joplin Vie hakemisto"
msgstr "Joplin palvelimen hakemisto"
#: packages/lib/models/Setting.js:319
msgid "Joplin Server username"
msgstr ""
msgstr "Joplin palvelimen käyttäjänimi"
#: packages/lib/models/Setting.js:330
#, fuzzy
msgid "Joplin Server password"
msgstr "Syötä pääsalasana:"
msgstr "Joplin palvelimen salasana"
#: packages/lib/models/Setting.js:342
msgid "Attachment download behaviour"
@@ -2412,9 +2406,8 @@ msgid "Enable typographer support"
msgstr "Ota typografiatuki käyttöön"
#: packages/lib/models/Setting.js:614
#, fuzzy
msgid "Enable Linkify"
msgstr "Muistiinpanohistorian ottaminen käyttöön"
msgstr "Ota käyttöön Linkify"
#: packages/lib/models/Setting.js:615
msgid "Enable math expressions"
@@ -2429,17 +2422,16 @@ msgid "Enable Mermaid diagrams support"
msgstr "Ota Mermaid kaavioiden tuki käyttöön"
#: packages/lib/models/Setting.js:618
#, fuzzy
msgid "Enable audio player"
msgstr "Ota merkinnän hymiö käyttöön"
msgstr "Ota käyttöön audiosoitin"
#: packages/lib/models/Setting.js:619
msgid "Enable video player"
msgstr ""
msgstr "Ota käyttöön videosoitin"
#: packages/lib/models/Setting.js:620
msgid "Enable PDF viewer"
msgstr ""
msgstr "Ota käyttöön PDF katseluohjelma"
#: packages/lib/models/Setting.js:621
msgid "Enable ==mark== syntax"
@@ -2530,9 +2522,8 @@ msgstr ""
"on väärä tai tyhjä, se on oletusarvoisesti yleinen monospace fontti."
#: packages/lib/models/Setting.js:717
#, fuzzy
msgid "Custom stylesheet for rendered Markdown"
msgstr "Mukautettu tyylitaulukko Joplinin sovellustyyleille"
msgstr "Mukautettu tyylitaulukko mukautetuille Markdown tyyleille"
#: packages/lib/models/Setting.js:733
msgid "Custom stylesheet for Joplin-wide app styles"
@@ -3001,18 +2992,18 @@ msgstr "Käynnissä"
#: packages/lib/Synchronizer.js:844
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgstr "Tuntematon kohdetyyppi ladattu - päivitä Joplin uusimpaan versioon"
#: 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 ""
"Yhteyden muodostaminen Joplinin Nextcloud -sovellukseen epäonnistui. "
"Tarkista kokoonpano Synkronointiasetukset-näytössä. Koko virhe oli:\n"
"Yhteyden muodostaminen Joplinin palvelimelle epäonnistui. Tarkista "
"vaihtoehdot Synkronointiasetukset-näytössä. Koko virhe oli:\n"
"\n"
"%s"
@@ -3251,9 +3242,8 @@ msgid "Downloaded and encrypted"
msgstr "Ladattu ja salattu"
#: packages/lib/services/ReportService.js:180
#, fuzzy
msgid "Created locally"
msgstr "Luodut paikalliset kohteet: %d."
msgstr "Luotu paikallisesti"
#: packages/lib/services/ReportService.js:191
msgid "Attachments that could not be downloaded"
@@ -3308,20 +3298,19 @@ msgstr "Päällä %s: %s"
#: packages/lib/services/spellChecker/SpellCheckerService.js:107
msgid "No suggestions"
msgstr ""
msgstr "Ei ehdotuksia"
#: packages/lib/services/spellChecker/SpellCheckerService.js:114
msgid "Add to dictionary"
msgstr ""
msgstr "Lisää sanakirjaan"
#: packages/lib/services/spellChecker/SpellCheckerService.js:153
msgid "Use spell checker"
msgstr ""
msgstr "Käytä oikolukua"
#: packages/lib/services/spellChecker/SpellCheckerService.js:173
#, fuzzy
msgid "Change language"
msgstr "Kieli"
msgstr "Vaihda kiel"
#: packages/app-cli/app/command-cp.js:13
msgid ""
@@ -3360,9 +3349,10 @@ msgid ""
"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the "
"to-dos, while `-tnt` would display notes and to-dos."
msgstr ""
"Näyttää vain tietyn tyyppiset kohteet. Voi olla `n` muistiinpanoja, `t` "
"tehtäviä, tai `nt` muistiinpanoja ja tehtäviä (esim. `-tt` näyttäisi vain "
"tehtävät, kun taas `-tnt` näyttää muistiinpanoja ja tehtäviä."
"Näyttää vain tietyn tyyppisen kohteen (kohteet). Voi olla `n` "
"muistiinpanoja, `t` tehtäviä, tai `nt` muistiinpanoja ja tehtäviä (esim. `-"
"tt` näyttäisi vain tehtävät, kun taas `-tnt` näyttää muistiinpanoja ja "
"tehtäviä."
#: packages/app-cli/app/command-ls.js:31
msgid "Either \"text\" or \"json\""
@@ -3583,9 +3573,9 @@ msgid "Downloading resources..."
msgstr "Ladataan resursseja..."
#: packages/app-cli/app/command-sync.js:242
#, fuzzy, javascript-format
#, javascript-format
msgid "Sync target must be upgraded! Run `%s` to proceed."
msgstr "Synkronointitavoite on päivitettävä. Jatka painamalla tätä banneria."
msgstr "Synkronointikohde on päivitettävä! Suorita `%s` jatkaaksesi."
#: packages/app-cli/app/command-sync.js:260
msgid "Cancelling... Please wait."
@@ -3643,11 +3633,11 @@ msgstr "Huomautusta ei ole: \"%s\". Luodaanko se?"
#: packages/app-cli/app/command-edit.js:75
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Aloitetaan muokkauksen tekeminen. Palaa kehotteeseen sulkemalla editori."
#: packages/app-cli/app/command-edit.js:97
#, fuzzy
msgid "Note has been saved."
msgstr "Muistikirjaa ei ole luokiteltu."
msgstr "Huomautus on tallennettu."
#: packages/app-cli/app/app-gui.js:452
msgid "To delete a tag, untag the associated notes."
@@ -4005,7 +3995,6 @@ msgstr "Huomautus ei ole tehtävä: \"%s\""
#~ msgid "Notebook properties"
#~ msgstr "Muistikirjan ominaisuudet"
#, javascript-format
#~ msgid "This file could not be opened: %s"
#~ msgstr "Tätä tiedostoa ei voitu avata: %s"
@@ -4030,7 +4019,6 @@ msgstr "Huomautus ei ole tehtävä: \"%s\""
#~ msgid "Move to notebook..."
#~ msgstr "Siirry muistikirjaan..."
#, javascript-format
#~ msgid "Move %d notes to notebook \"%s\"?"
#~ msgstr "Siirrä %d muistiinpanot muistikirjaan \"%s\"?"
@@ -4047,11 +4035,9 @@ msgstr "Huomautus ei ole tehtävä: \"%s\""
#~ msgid "Errors only"
#~ msgstr "Vain virheet"
#, javascript-format
#~ msgid "Database v%s"
#~ msgstr "Tietokanta v%s"
#, javascript-format
#~ msgid "FTS enabled: %d"
#~ msgstr "Koko tekstin haku (FTS) Käytössä: %d"
@@ -4064,11 +4050,9 @@ msgstr "Huomautus ei ole tehtävä: \"%s\""
#~ msgid "Add or remove tags"
#~ msgstr "Lisää tai poista tunnisteita"
#, javascript-format
#~ msgid "Unknown log level: %s"
#~ msgstr "Tuntematon kirjaus taso: %s"
#, javascript-format
#~ msgid "Unknown level ID: %s"
#~ msgstr "Tuntematon tason tunnus: %s"
@@ -4090,7 +4074,6 @@ msgstr "Huomautus ei ole tehtävä: \"%s\""
#~ msgid "Json Export Directory"
#~ msgstr "Json Vie hakemisto"
#, javascript-format
#~ msgid ""
#~ "This item is currently encrypted: %s \"%s\". Please wait for all items to "
#~ "be decrypted and try again."

View File

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

View File

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

View File

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