1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-23 23:33:01 +02:00

Compare commits

..

45 Commits

Author SHA1 Message Date
Laurent Cozic
f6cd988939 disable edition if no multi profiles 2022-04-11 16:36:23 +01:00
Laurent Cozic
a99eec7cdf 1 based 2022-04-11 16:24:41 +01:00
Laurent Cozic
7ef09dfa77 enter to valid 2022-04-11 16:19:27 +01:00
Laurent Cozic
b11573a2a7 Disable welcome notes 2022-04-11 12:30:54 +01:00
Laurent Cozic
f4034b1ff0 Merge branch 'dev' into multi_profiles 2022-04-11 12:22:48 +01:00
Henry Heino
58bf93a112 iOS: Fixes #6318: Remove white border around Beta Editor (#6326) 2022-04-11 11:57:49 +01:00
Henry Heino
5962b0813e Mobile: Fixes #6324: Support inserting attachments from Beta Editor (#6325) 2022-04-11 11:56:45 +01:00
Ayush Srivastava
cffea3ea1e Mobile: Fixes #3564: "Move Note" dropdown menu can be very narrow (#6306) 2022-04-11 11:53:20 +01:00
Laurent Cozic
dfadacd7f4 save settings 2022-04-10 19:13:01 +01:00
Laurent Cozic
ecc7b17708 loading settings 2022-04-10 18:19:56 +01:00
Laurent Cozic
ee6ab55649 setting loading 2022-04-10 16:50:11 +01:00
Laurent Cozic
b0d64e2f51 Merge branch 'dev' into multi_profiles 2022-04-10 15:23:19 +01:00
Kenichi Kobayashi
f6e21e0180 Desktop: Fixes #6074: Scroll jumps when typing if heavy scripts or many large elements are used (#6383) 2022-04-10 11:31:17 +01:00
reportxx
e02422070e Update Swedish translation (#6382) 2022-04-10 11:30:03 +01:00
Tolulope Malomo
727d64b646 Android: Fixes #6026: Long path in "Export profile" prevents tapping OK button (#6359) 2022-04-10 11:22:30 +01:00
Henry Heino
23e54a60d9 Android: Fixes #5987: Cursor hard to see in dark mode (#6307) 2022-04-10 10:58:11 +01:00
ScriptInfra
0d4978223e Update README.md (#6295) 2022-04-10 10:53:44 +01:00
Mayank Bondre
0b32a29cce Plugins: Resolves #5867: Add support for "categories" manifest field (#6109) 2022-04-10 10:52:31 +01:00
Laurent Cozic
f322d40910 tests 2022-04-09 18:29:20 +01:00
Laurent Cozic
557cb9a6c3 edit profile 2022-04-09 17:38:39 +01:00
Laurent Cozic
d5a55c7908 ui 2022-04-09 17:21:48 +01:00
Laurent Cozic
7308bbd3ca switch logic 2022-04-09 16:42:49 +01:00
Laurent Cozic
c1e8f9befd Multi profile support 2022-04-09 15:37:14 +01:00
Laurent Cozic
a0d77d10ba Tools: Allow setting website build environment from config file 2022-04-09 14:43:59 +01:00
Joplin Bot
bdd9c6cf35 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-04-09 12:19:45 +00:00
Laurent Cozic
f2bfa30e04 Api: Fixed updating resource content 2022-04-09 11:58:08 +01:00
Laurent Cozic
8077117e65 Doc: Ignore latest post in updateNews script 2022-04-08 13:16:43 +01:00
Laurent Cozic
7e8927398a Doc: Fixed typo 2022-04-07 19:19:53 +01:00
Laurent Cozic
09dcee876c Doc: Fixed env 2022-04-07 19:04:36 +01:00
Laurent Cozic
23b56f4f70 Tools: Fixed script name 2022-04-07 16:00:00 +01:00
Laurent Cozic
b3d09ce776 Doc: Add Joplin Cloud Teams offer to website 2022-04-07 15:35:15 +01:00
Laurent Cozic
84d40b805e Tools: Added tool to automatically post news from local Markdown folder to forum 2022-04-07 15:15:48 +01:00
Laurent Cozic
c097a82b7b Doc: Fixed news title 2022-04-07 11:07:48 +01:00
Joplin Bot
dfa22b560e Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-04-06 06:17:47 +00:00
Joplin Bot
7d31a3fe90 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-04-06 00:38:27 +00:00
妙呀
79aabc2d06 updata zh_CN.po (#6355) 2022-04-05 19:17:20 +01:00
rnbastos
70e82ca64f Update pt_BR.po (#6353) 2022-04-05 19:16:56 +01:00
Laurent Cozic
a0662412b2 Tools: Removed Windows build from CI for now - discontinued by GitHub 2022-04-05 17:30:27 +01:00
Laurent Cozic
220b48ef02 Doc: Add news about GSoC Contributor Proposals phase 2022-04-05 15:47:22 +01:00
Laurent Cozic
cb637e817b Server: Do not make checkboxes in published notes clickable 2022-04-05 15:42:06 +01:00
Laurent Cozic
27198a16a4 Chore: Make it easier to test note publishing on desktop 2022-04-05 15:37:57 +01:00
Laurent Cozic
1a5bff3bf4 Doc: Move info to Joplin Cloud FAQ 2022-04-05 15:16:48 +01:00
Laurent Cozic
571147acbb Tools: Fixed git changelog tool 2022-04-03 19:27:10 +01:00
Laurent Cozic
9d9420a35c Desktop: Support for Joplin Cloud recursive linked notes 2022-04-03 19:19:24 +01:00
kik0220
a79bc69604 Translation: Update ja_JP.po (#6345)
Co-authored-by: kik0220 <kik0220@gmail.com>
2022-03-31 10:50:29 +01:00
82 changed files with 1733 additions and 492 deletions

View File

@@ -148,6 +148,9 @@ packages/app-desktop/checkForUpdates.js.map
packages/app-desktop/commands/copyDevCommand.d.ts
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyDevCommand.js.map
packages/app-desktop/commands/editProfileConfig.d.ts
packages/app-desktop/commands/editProfileConfig.js
packages/app-desktop/commands/editProfileConfig.js.map
packages/app-desktop/commands/exportFolders.d.ts
packages/app-desktop/commands/exportFolders.js
packages/app-desktop/commands/exportFolders.js.map
@@ -175,6 +178,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
packages/app-desktop/commands/stopExternalEditing.d.ts
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js.map
packages/app-desktop/commands/switchProfile.d.ts
packages/app-desktop/commands/switchProfile.js
packages/app-desktop/commands/switchProfile.js.map
packages/app-desktop/commands/switchProfile1.d.ts
packages/app-desktop/commands/switchProfile1.js
packages/app-desktop/commands/switchProfile1.js.map
packages/app-desktop/commands/switchProfile2.d.ts
packages/app-desktop/commands/switchProfile2.js
packages/app-desktop/commands/switchProfile2.js.map
packages/app-desktop/commands/switchProfile3.d.ts
packages/app-desktop/commands/switchProfile3.js
packages/app-desktop/commands/switchProfile3.js.map
packages/app-desktop/commands/toggleExternalEditing.d.ts
packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleExternalEditing.js.map
@@ -259,6 +274,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/MainScreen.js.map
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
@@ -1246,6 +1264,9 @@ packages/lib/services/DecryptionWorker.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
packages/lib/services/ExternalEditWatcher/utils.d.ts
packages/lib/services/ExternalEditWatcher/utils.js
packages/lib/services/ExternalEditWatcher/utils.js.map
packages/lib/services/ItemChangeUtils.d.ts
packages/lib/services/ItemChangeUtils.js
packages/lib/services/ItemChangeUtils.js.map
@@ -1570,6 +1591,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
packages/lib/services/plugins/utils/validatePluginVersion.test.js
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
packages/lib/services/profileConfig/index.d.ts
packages/lib/services/profileConfig/index.js
packages/lib/services/profileConfig/index.js.map
packages/lib/services/profileConfig/index.test.d.ts
packages/lib/services/profileConfig/index.test.js
packages/lib/services/profileConfig/index.test.js.map
packages/lib/services/profileConfig/initProfile.d.ts
packages/lib/services/profileConfig/initProfile.js
packages/lib/services/profileConfig/initProfile.js.map
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/types.d.ts
packages/lib/services/profileConfig/types.js
packages/lib/services/profileConfig/types.js.map
packages/lib/services/rest/Api.d.ts
packages/lib/services/rest/Api.js
packages/lib/services/rest/Api.js.map
@@ -2023,6 +2062,9 @@ packages/tools/website/build.js.map
packages/tools/website/updateDownloadPage.d.ts
packages/tools/website/updateDownloadPage.js
packages/tools/website/updateDownloadPage.js.map
packages/tools/website/updateNews.d.ts
packages/tools/website/updateNews.js
packages/tools/website/updateNews.js.map
packages/tools/website/utils/frontMatter.d.ts
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/frontMatter.js.map

View File

@@ -5,7 +5,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-2016]
# Removed windows-2016 for now - discontinued by GitHub
os: [macos-latest, ubuntu-latest]
steps:
# Silence apt-get update errors (for example when a module doesn't

42
.gitignore vendored
View File

@@ -138,6 +138,9 @@ packages/app-desktop/checkForUpdates.js.map
packages/app-desktop/commands/copyDevCommand.d.ts
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyDevCommand.js.map
packages/app-desktop/commands/editProfileConfig.d.ts
packages/app-desktop/commands/editProfileConfig.js
packages/app-desktop/commands/editProfileConfig.js.map
packages/app-desktop/commands/exportFolders.d.ts
packages/app-desktop/commands/exportFolders.js
packages/app-desktop/commands/exportFolders.js.map
@@ -165,6 +168,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
packages/app-desktop/commands/stopExternalEditing.d.ts
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js.map
packages/app-desktop/commands/switchProfile.d.ts
packages/app-desktop/commands/switchProfile.js
packages/app-desktop/commands/switchProfile.js.map
packages/app-desktop/commands/switchProfile1.d.ts
packages/app-desktop/commands/switchProfile1.js
packages/app-desktop/commands/switchProfile1.js.map
packages/app-desktop/commands/switchProfile2.d.ts
packages/app-desktop/commands/switchProfile2.js
packages/app-desktop/commands/switchProfile2.js.map
packages/app-desktop/commands/switchProfile3.d.ts
packages/app-desktop/commands/switchProfile3.js
packages/app-desktop/commands/switchProfile3.js.map
packages/app-desktop/commands/toggleExternalEditing.d.ts
packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleExternalEditing.js.map
@@ -249,6 +264,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/MainScreen.js.map
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
@@ -1236,6 +1254,9 @@ packages/lib/services/DecryptionWorker.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
packages/lib/services/ExternalEditWatcher/utils.d.ts
packages/lib/services/ExternalEditWatcher/utils.js
packages/lib/services/ExternalEditWatcher/utils.js.map
packages/lib/services/ItemChangeUtils.d.ts
packages/lib/services/ItemChangeUtils.js
packages/lib/services/ItemChangeUtils.js.map
@@ -1560,6 +1581,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
packages/lib/services/plugins/utils/validatePluginVersion.test.js
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
packages/lib/services/profileConfig/index.d.ts
packages/lib/services/profileConfig/index.js
packages/lib/services/profileConfig/index.js.map
packages/lib/services/profileConfig/index.test.d.ts
packages/lib/services/profileConfig/index.test.js
packages/lib/services/profileConfig/index.test.js.map
packages/lib/services/profileConfig/initProfile.d.ts
packages/lib/services/profileConfig/initProfile.js
packages/lib/services/profileConfig/initProfile.js.map
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/types.d.ts
packages/lib/services/profileConfig/types.js
packages/lib/services/profileConfig/types.js.map
packages/lib/services/rest/Api.d.ts
packages/lib/services/rest/Api.js
packages/lib/services/rest/Api.js.map
@@ -2013,6 +2052,9 @@ packages/tools/website/build.js.map
packages/tools/website/updateDownloadPage.d.ts
packages/tools/website/updateDownloadPage.js
packages/tools/website/updateDownloadPage.js.map
packages/tools/website/updateNews.d.ts
packages/tools/website/updateNews.js
packages/tools/website/updateNews.js.map
packages/tools/website/utils/frontMatter.d.ts
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/frontMatter.js.map

View File

@@ -18,6 +18,7 @@
"buildCommandIndex": "gulp buildCommandIndex",
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
"buildTranslations": "node packages/tools/build-translation.js",
"buildWebsite": "node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",

View File

@@ -313,10 +313,14 @@ async function fetchAllNotes() {
lines.push('');
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
lines.push('');
lines.push('Or to **update** a resource:');
lines.push('To **update** the resource content, you can make a PUT request with the same arguments:');
lines.push('');
lines.push('\tcurl -X PUT -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my modified title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
lines.push('');
lines.push('Or if you only need to update the resource properties (title, etc.), without changing the content, you can make a regular PUT request:');
lines.push('');
lines.push('\tcurl -X PUT --data \'{"title": "My new title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
lines.push('');
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
lines.push('');
lines.push('**From a plugin** the syntax to create a resource is also a bit special:');

View File

@@ -242,9 +242,9 @@ describe('MdToHtml', function() {
{
const input = '# Head\nFruits\n- Apple\n';
const result = await mdToHtml.render(input, null, { bodyOnly: true, mapsToLine: true });
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0">Head</h1>\n' +
'<p class="maps-to-line" source-line="1">Fruits</p>\n' +
'<ul>\n<li class="maps-to-line" source-line="2">Apple</li>\n</ul>'
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0" source-line-end="1">Head</h1>\n' +
'<p class="maps-to-line" source-line="1" source-line-end="2">Fruits</p>\n' +
'<ul>\n<li class="maps-to-line" source-line="2" source-line-end="3">Apple</li>\n</ul>'
);
}
}));

View File

@@ -0,0 +1,19 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import Setting from '../../lib/models/Setting';
import { openFileWithExternalEditor } from '../../lib/services/ExternalEditWatcher/utils';
import bridge from '../services/bridge';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'editProfileConfig',
label: () => _('Edit profile configuration...'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await openFileWithExternalEditor(`${Setting.value('rootProfileDir')}/profiles.json`, bridge());
},
enabledCondition: 'hasMultiProfiles',
};
};

View File

@@ -1,5 +1,6 @@
// AUTO-GENERATED using `gulp buildCommandIndex`
import * as copyDevCommand from './copyDevCommand';
import * as editProfileConfig from './editProfileConfig';
import * as exportFolders from './exportFolders';
import * as exportNotes from './exportNotes';
import * as focusElement from './focusElement';
@@ -8,11 +9,16 @@ import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as startExternalEditing from './startExternalEditing';
import * as stopExternalEditing from './stopExternalEditing';
import * as switchProfile from './switchProfile';
import * as switchProfile1 from './switchProfile1';
import * as switchProfile2 from './switchProfile2';
import * as switchProfile3 from './switchProfile3';
import * as toggleExternalEditing from './toggleExternalEditing';
import * as toggleSafeMode from './toggleSafeMode';
const index:any[] = [
copyDevCommand,
editProfileConfig,
exportFolders,
exportNotes,
focusElement,
@@ -21,6 +27,10 @@ const index:any[] = [
restoreNoteRevision,
startExternalEditing,
stopExternalEditing,
switchProfile,
switchProfile1,
switchProfile2,
switchProfile3,
toggleExternalEditing,
toggleSafeMode,
];

View File

@@ -0,0 +1,26 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import Setting from '@joplin/lib/models/Setting';
import { saveProfileConfig } from '@joplin/lib/services/profileConfig';
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import bridge from '../services/bridge';
export const declaration: CommandDeclaration = {
name: 'switchProfile',
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, profileIndex: number) => {
const currentConfig = context.state.profileConfig;
if (currentConfig.currentProfile === profileIndex) return;
const newConfig: ProfileConfig = {
...currentConfig,
currentProfile: profileIndex,
};
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
bridge().restart();
},
};
};

View File

@@ -0,0 +1,15 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'switchProfile1',
label: () => _('Switch to profile %d', 1),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await CommandService.instance().execute('switchProfile', 0);
},
};
};

View File

@@ -0,0 +1,15 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'switchProfile2',
label: () => _('Switch to profile %d', 2),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await CommandService.instance().execute('switchProfile', 1);
},
};
};

View File

@@ -0,0 +1,15 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'switchProfile3',
label: () => _('Switch to profile %d', 3),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await CommandService.instance().execute('switchProfile', 2);
},
};
};

View File

@@ -0,0 +1,34 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { createNewProfile, saveProfileConfig } from '@joplin/lib/services/profileConfig';
import Setting from '@joplin/lib/models/Setting';
import bridge from '../../../services/bridge';
export const declaration: CommandDeclaration = {
name: 'addProfile',
label: () => _('Create new profile...'),
};
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
comp.setState({
promptOptions: {
label: _('Profile name:'),
buttons: ['create', 'cancel'],
value: '',
onClose: async (answer: string) => {
if (answer) {
const newConfig = await createNewProfile(context.state.profileConfig, answer);
newConfig.currentProfile = newConfig.profiles.length - 1;
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
bridge().restart();
}
comp.setState({ promptOptions: null });
},
},
});
},
};
};

View File

@@ -1,4 +1,5 @@
// AUTO-GENERATED using `gulp buildCommandIndex`
import * as addProfile from './addProfile';
import * as commandPalette from './commandPalette';
import * as editAlarm from './editAlarm';
import * as exportPdf from './exportPdf';
@@ -38,6 +39,7 @@ import * as toggleSideBar from './toggleSideBar';
import * as toggleVisiblePanes from './toggleVisiblePanes';
const index:any[] = [
addProfile,
commandPalette,
editAlarm,
exportPdf,

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { AppState } from '../app.reducer';
import InteropService from '@joplin/lib/services/interop/InteropService';
import { stateUtils } from '@joplin/lib/reducer';
@@ -18,9 +18,9 @@ import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
import bridge from '../services/bridge';
import checkForUpdates from '../checkForUpdates';
const { connect } = require('react-redux');
import { reg } from '@joplin/lib/registry';
import { ProfileConfig } from '../../lib/services/profileConfig/types';
const packageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
@@ -39,7 +39,7 @@ function pluginMenuItemsCommandNames(menuItems: MenuItem[]): string[] {
return output;
}
function pluginCommandNames(plugins: PluginStates): string[] {
function getPluginCommandNames(plugins: PluginStates): string[] {
let output: string[] = [];
for (const view of pluginUtils.viewsByType(plugins, 'menu')) {
@@ -70,6 +70,42 @@ function createPluginMenuTree(label: string, menuItems: MenuItem[], onMenuItemCl
return output;
}
const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: any) => {
return useMemo(() => {
const switchProfileMenuItems: any[] = [];
for (let i = 0; i < profileConfig.profiles.length; i++) {
const profile = profileConfig.profiles[i];
let menuItem: any = {};
const profileNum = i + 1;
if (menuItemDic[`switchProfile${profileNum}`]) {
menuItem = { ...menuItemDic[`switchProfile${profileNum}`] };
} else {
menuItem = {
label: profile.name,
click: () => {
void CommandService.instance().execute('switchProfile', i);
},
};
}
menuItem.label = profile.name;
menuItem.type = 'checkbox';
menuItem.checked = profileConfig.currentProfile === i;
switchProfileMenuItems.push(menuItem);
}
switchProfileMenuItems.push({ type: 'separator' });
switchProfileMenuItems.push(menuItemDic.addProfile);
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
return switchProfileMenuItems;
}, [profileConfig, menuItemDic]);
};
interface Props {
dispatch: Function;
menuItemProps: any;
@@ -90,6 +126,7 @@ interface Props {
plugins: PluginStates;
customCss: string;
locale: string;
profileConfig: ProfileConfig;
}
const commandNames: string[] = menuCommandNames();
@@ -241,6 +278,18 @@ function useMenu(props: Props) {
const onImportModuleClickRef = useRef(null);
onImportModuleClickRef.current = onImportModuleClick;
const pluginCommandNames = useMemo(() => props.pluginMenuItems.map((view: any) => view.commandName), [props.pluginMenuItems]);
const menuItemDic = useMemo(() => {
return menuUtils.commandsToMenuItems(
commandNames.concat(pluginCommandNames),
(commandName: string) => onMenuItemClickRef.current(commandName),
props.locale
);
}, [commandNames, pluginCommandNames, props.locale]);
const switchProfileMenuItems: any[] = useSwitchProfileMenuItems(props.profileConfig, menuItemDic);
useEffect(() => {
let timeoutId: any = null;
@@ -249,13 +298,6 @@ function useMenu(props: Props) {
const keymapService = KeymapService.instance();
const pluginCommandNames = props.pluginMenuItems.map((view: any) => view.commandName);
const menuItemDic = menuUtils.commandsToMenuItems(
commandNames.concat(pluginCommandNames),
(commandName: string) => onMenuItemClickRef.current(commandName),
props.locale
);
const quitMenuItem = {
label: _('Quit'),
accelerator: keymapService.getAccelerator('quit'),
@@ -385,6 +427,10 @@ function useMenu(props: Props) {
const newFolderItem = menuItemDic.newFolder;
const newSubFolderItem = menuItemDic.newSubFolder;
const printItem = menuItemDic.print;
const switchProfileItem = {
label: _('Switch profile'),
submenu: switchProfileMenuItems,
};
let toolsItems: any[] = [];
@@ -499,6 +545,8 @@ function useMenu(props: Props) {
platforms: ['darwin'],
},
shim.isMac() ? noItem : switchProfileItem,
shim.isMac() ? {
label: _('Hide %s', 'Joplin'),
platforms: ['darwin'],
@@ -545,6 +593,7 @@ function useMenu(props: Props) {
type: 'separator',
},
printItem,
switchProfileItem,
],
};
@@ -848,7 +897,21 @@ function useMenu(props: Props) {
clearTimeout(timeoutId);
timeoutId = null;
};
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime, props['spellChecker.language'], props['spellChecker.enabled'], props.plugins, props.customCss, props.locale]);
}, [
props.routeName,
props.pluginMenuItems,
props.pluginMenus,
keymapLastChangeTime,
modulesLastChangeTime,
props['spellChecker.language'],
props['spellChecker.enabled'],
props.plugins,
props.customCss,
props.locale,
props.profileConfig,
switchProfileMenuItems,
menuItemDic,
]);
useMenuStates(menu, props);
@@ -889,7 +952,7 @@ const mapStateToProps = (state: AppState) => {
const whenClauseContext = stateToWhenClauseContext(state);
return {
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(pluginCommandNames(state.pluginService.plugins)), whenClauseContext),
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext),
locale: state.settings.locale,
routeName: state.route.routeName,
selectedFolderId: state.selectedFolderId,
@@ -907,6 +970,7 @@ const mapStateToProps = (state: AppState) => {
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
plugins: state.pluginService.plugins,
customCss: state.customCss,
profileConfig: state.profileConfig,
};
};

View File

@@ -649,6 +649,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
// undefined. Maybe due to the error boundary that unmount components.
// Since we can't do much about it we just print an error.
if (webviewRef.current && webviewRef.current.wrappedInstance) {
// To keep consistency among CodeMirror's editing and scroll percents
// of Editor and Viewer.
const percent = getLineScrollPercent();
setEditorPercentScroll(percent);
options.percent = percent;
webviewRef.current.wrappedInstance.send('setHtml', renderedBody.html, options);
} else {
console.error('Trying to set HTML on an undefined webview ref');

View File

@@ -100,6 +100,7 @@ export interface EditorProps {
function Editor(props: EditorProps, ref: any) {
const [editor, setEditor] = useState(null);
const editorParent = useRef(null);
const lastEditTime = useRef(NaN);
// Codemirror plugins add new commands to codemirror (or change it's behavior)
// This command adds the smartListIndent function which will be bound to tab
@@ -120,6 +121,7 @@ function Editor(props: EditorProps, ref: any) {
const editor_change = useCallback((cm: any, change: any) => {
if (props.onChange && change.origin !== 'setValue') {
props.onChange(cm.getValue());
lastEditTime.current = Date.now();
}
}, [props.onChange]);
@@ -154,7 +156,8 @@ function Editor(props: EditorProps, ref: any) {
}, [props.onResize]);
const editor_update = useCallback((cm: any) => {
props.onUpdate(cm);
const edited = Date.now() - lastEditTime.current <= 100;
props.onUpdate(cm, edited);
}, [props.onUpdate]);
useEffect(() => {

View File

@@ -17,7 +17,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
const now = Date.now();
if (now >= ignoreNextEditorScrollTime_.current) ignoreNextEditorScrollEventCount_.current = 0;
if (ignoreNextEditorScrollEventCount_.current < 10) { // for safety
ignoreNextEditorScrollTime_.current = now + 200;
ignoreNextEditorScrollTime_.current = now + 1000;
ignoreNextEditorScrollEventCount_.current += 1;
}
};
@@ -157,8 +157,9 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
// When heights of lines are updated in CodeMirror, 'update' events are raised.
// If such an update event is raised, scroll position should be restored.
// See https://github.com/laurent22/joplin/issues/5981
const editor_update = useCallback((cm) => {
const editor_update = useCallback((cm: any, edited: boolean) => {
if (isCodeMirrorReady(cm)) {
if (edited) return;
const linesHeight = cm.heightAtLine(cm.lineCount()) - cm.heightAtLine(0);
if (lastLinesHeight_.current !== linesHeight) {
// To avoid cancelling intentional scroll position changes,

View File

@@ -231,6 +231,13 @@ class PromptDialog extends React.Component {
}
const buttonComps = [];
if (buttonTypes.indexOf('create') >= 0) {
buttonComps.push(
<button key="create" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'create')}>
{_('Create')}
</button>
);
}
if (buttonTypes.indexOf('ok') >= 0) {
buttonComps.push(
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
@@ -15,6 +15,7 @@ import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app.reducer';
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import SyncTargetRegistry from '../../lib/SyncTargetRegistry';
const { clipboard } = require('electron');
interface Props {
@@ -22,6 +23,7 @@ interface Props {
noteIds: Array<string>;
onClose: Function;
shares: StateShare[];
syncTargetId: number;
}
function styles_(props: Props) {
@@ -69,9 +71,10 @@ export function ShareNoteDialog(props: Props) {
console.info('Render ShareNoteDialog');
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
const [sharesState, setSharesState] = useState<string>('unknown');
// const [shares, setShares] = useState<SharesMap>({});
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
@@ -102,7 +105,7 @@ export function ShareNoteDialog(props: Props) {
clipboard.writeText(links.join('\n'));
};
const shareLinkButton_click = async () => {
const shareLinkButton_click = useCallback(async () => {
const service = ShareService.instance();
let hasSynced = false;
@@ -121,7 +124,7 @@ export function ShareNoteDialog(props: Props) {
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id);
const share = await service.shareNote(note.id, recursiveShare);
newShares.push(share);
}
@@ -149,17 +152,7 @@ export function ShareNoteDialog(props: Props) {
break;
}
};
// const removeNoteButton_click = (event: any) => {
// const newNotes = [];
// for (let i = 0; i < notes.length; i++) {
// const n = notes[i];
// if (n.id === event.noteId) continue;
// newNotes.push(n);
// }
// setNotes(newNotes);
// };
}, [recursiveShare, notes]);
const unshareNoteButton_click = async (event: any) => {
await ShareService.instance().unshareNote(event.noteId);
@@ -171,22 +164,6 @@ export function ShareNoteDialog(props: Props) {
<Button tooltip={_('Unpublish note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
);
// const removeButton = notes.length <= 1 ? null : (
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
// );
// const unshareButton = !shares[note.id] ? null : (
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
// </button>
// );
// const removeButton = notes.length <= 1 ? null : (
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
// </button>
// );
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
@@ -214,11 +191,26 @@ export function ShareNoteDialog(props: Props) {
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
}
function renderContent() {
const onRecursiveShareChange = useCallback(() => {
setRecursiveShare(v => !v);
}, []);
const renderRecursiveShareCheckbox = () => {
if (!syncTargetInfo.supportsRecursiveLinkedNotes) return null;
return (
<div style={styles.root}>
<div className="form-input-group form-input-group-checkbox">
<input id="recursiveShare" name="recursiveShare" type="checkbox" checked={!!recursiveShare} onChange={onRecursiveShareChange} /> <label htmlFor="recursiveShare">{_('Also publish linked notes')}</label>
</div>
);
};
const renderContent = () => {
return (
<div style={styles.root} className="form">
<DialogTitle title={_('Publish Notes')}/>
{renderNoteList(notes)}
{renderRecursiveShareCheckbox()}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{renderEncryptionWarningMessage()}
@@ -230,7 +222,7 @@ export function ShareNoteDialog(props: Props) {
/>
</div>
);
}
};
return (
<Dialog renderContent={renderContent}/>
@@ -240,6 +232,7 @@ export function ShareNoteDialog(props: Props) {
const mapStateToProps = (state: AppState) => {
return {
shares: state.shareService.shares.filter(s => !!s.note_id),
syncTargetId: state.settings['sync.target'],
};
};

View File

@@ -60,5 +60,10 @@ export default function() {
'gotoAnything',
'commandPalette',
'openMasterPasswordDialog',
'addProfile',
'editProfileConfig',
'switchProfile1',
'switchProfile2',
'switchProfile3',
];
}

View File

@@ -116,7 +116,7 @@
const now = Date.now();
if (now >= ignoreNextScrollTime_) ignoreNextScrollEventCount_ = 0;
if (ignoreNextScrollEventCount_ < 10) { // for safety
ignoreNextScrollTime_ = now + 200;
ignoreNextScrollTime_ = now + 1000;
ignoreNextScrollEventCount_ += 1;
}
};
@@ -293,7 +293,7 @@
return;
}
}
if (!heightChanged) return;
if (!heightChanged && cause !== 'dom-changed') return;
const restoreAndRefresh = () => {
scrollmap.refresh();
restorePercentScroll();
@@ -337,7 +337,11 @@
contentElement.innerHTML = html;
scrollmap.create(event.options.markupLineCount);
restorePercentScroll(); // First, a quick treatment is applied.
if (typeof event.options.percent !== 'number') {
restorePercentScroll(); // First, a quick treatment is applied.
} else {
setPercentScroll(event.options.percent);
}
addPluginAssets(event.options.pluginAssets);

View File

@@ -45,7 +45,8 @@ scrollmap.get_ = () => {
// Each map entry is total-ordered.
let last = 0;
for (let i = 0; i < elems.length; i++) {
const top = elems[i].getBoundingClientRect().top - offset;
const rect = elems[i].getBoundingClientRect();
const top = rect.top - offset;
const line = Number(elems[i].getAttribute('source-line'));
const percent = Math.max(0, Math.min(1, top / height));
if (map.line[last] < line && map.percent[last] < percent) {
@@ -53,12 +54,20 @@ scrollmap.get_ = () => {
map.percent.push(percent);
last += 1;
}
const bottom = rect.bottom - offset;
const lineEnd = Number(elems[i].getAttribute('source-line-end'));
const percentEnd = Math.max(0, Math.min(1, bottom / height));
if (map.line[last] < lineEnd && map.percent[last] < percentEnd) {
map.line.push(lineEnd);
map.percent.push(percentEnd);
last += 1;
}
}
const lineCount = scrollmap.lineCount_;
if (lineCount) {
map.lineCount = lineCount;
} else {
if (map.lineCount <= map.line[last]) map.lineCount = map.line[last] + 1;
if (map.lineCount < map.line[last]) map.lineCount = map.line[last];
}
if (map.percent[last] < 1) {
map.line.push(lineCount || 1e10);

View File

@@ -112,7 +112,15 @@ document.addEventListener('auxclick', event => event.preventDefault());
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => event.preventDefault());
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
event.preventDefault();
});
app().start(bridge().processArgv()).then((result) => {
if (!result || !result.action) {

View File

@@ -180,6 +180,22 @@ h2 {
margin-bottom: 10px;
}
.form > .form-input-group-checkbox {
display: flex;
flex-direction: row;
align-items: center;
}
.form > .form-input-group-checkbox > input {
display: flex;
margin-right: 6px;
}
.form > .form-input-group-checkbox > label {
display: flex;
margin-bottom: 0;
}
.bold {
font-weight: bold;
}

View File

@@ -38,10 +38,9 @@ class Dropdown extends React.Component {
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
const wrapperStyle = {
width: this.state.headerSize.width,
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
marginTop: listTop,
marginLeft: this.state.headerSize.x,
alignSelf: 'center',
};
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
@@ -87,6 +86,7 @@ class Dropdown extends React.Component {
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
const closeList = () => {
if (this.props.onClose()) this.props.onClose();
this.setState({ listVisible: false });
};
@@ -116,6 +116,7 @@ class Dropdown extends React.Component {
onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
if (this.props.onOpen()) this.props.onOpen();
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>

View File

@@ -19,6 +19,8 @@ interface CodeMirrorResult {
editor: EditorView;
undo: Function;
redo: Function;
select: (anchor: number, head: number)=> void;
insertText: (text: string)=> void;
}
function postMessage(name: string, data: any) {
@@ -36,25 +38,53 @@ function logMessage(...msg: any[]) {
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
const createTheme = (theme: any): Extension => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = { };
const baseSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
}
const baseTheme = EditorView.baseTheme({
'&': {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
},
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': baseContentStyle,
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
});
const appearanceTheme = EditorView.theme({}, { dark: theme.appearance === 'dark' });
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
@@ -152,6 +182,13 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
postMessage('onChange', { value: editor.state.doc.toString() });
schedulePostUndoRedoDepthChange(editor);
}
if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
const mainRange = viewUpdate.state.selection.main;
const selStart = mainRange.from;
const selEnd = mainRange.to;
postMessage('onSelectionChange', { selection: { start: selStart, end: selEnd } });
}
}),
],
doc: initialText,
@@ -169,5 +206,14 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
redo(editor);
schedulePostUndoRedoDepthChange(editor, true);
},
select: (anchor: number, head: number) => {
editor.dispatch(editor.state.update({
selection: { anchor, head },
scrollIntoView: true,
}));
},
insertText: (text: string) => {
editor.dispatch(editor.state.replaceSelection(text));
},
};
}

View File

@@ -2,7 +2,7 @@ import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import { themeStyle } from '@joplin/lib/theme';
const React = require('react');
const { forwardRef, useImperativeHandle, useEffect, useState, useCallback, useRef } = require('react');
const { forwardRef, useImperativeHandle, useEffect, useMemo, useState, useCallback, useRef } = require('react');
const { WebView } = require('react-native-webview');
const { editorFont } = require('../global-style');
@@ -15,14 +15,27 @@ export interface UndoRedoDepthChangeEvent {
redoDepth: number;
}
export interface Selection {
start: number;
end: number;
}
export interface SelectionChangeEvent {
selection: Selection;
}
type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
interface Props {
themeId: number;
initialText: string;
initialSelection?: Selection;
style: any;
onChange: ChangeEventHandler;
onSelectionChange: SelectionChangeEventHandler;
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
}
@@ -31,6 +44,7 @@ function fontFamilyFromSettings() {
return [f, 'sans-serif'].join(', ');
}
// Obsolete with CodeMirror 6. See ./CodeMirror.ts for styling.
// function useCss(themeId:number):string {
// const [css, setCss] = useState('');
@@ -169,6 +183,17 @@ function fontFamilyFromSettings() {
// return css;
// }
function useCss(themeId: number): string {
return useMemo(() => {
const theme = themeStyle(themeId);
return `
:root {
background-color: ${theme.backgroundColor};
}
`;
}, [themeId]);
}
function useHtml(css: string): string {
const [html, setHtml] = useState('');
@@ -211,11 +236,15 @@ function NoteEditor(props: Props, ref: any) {
const [source, setSource] = useState(undefined);
const webviewRef = useRef(null);
const setInitialSelectionJS = props.initialSelection ? `
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
` : '';
const injectedJavaScript = `
function postMessage(name, data) {
window.ReactNativeWebView.postMessage(JSON.stringify({
data,
name,
name,
}));
}
@@ -226,7 +255,7 @@ function NoteEditor(props: Props, ref: any) {
// This variable is not used within this script
// but is called using "injectJavaScript" from
// the wrapper component.
let cm = null;
window.cm = null;
try {
${shim.injectedJs('codeMirrorBundle')};
@@ -236,6 +265,7 @@ function NoteEditor(props: Props, ref: any) {
const initialText = ${JSON.stringify(props.initialText)};
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme);
${setInitialSelectionJS}
} catch (e) {
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
} finally {
@@ -243,8 +273,8 @@ function NoteEditor(props: Props, ref: any) {
}
`;
// const css = useCss(props.themeId);
const html = useHtml('');
const css = useCss(props.themeId);
const html = useHtml(css);
useImperativeHandle(ref, () => {
return {
@@ -254,6 +284,14 @@ function NoteEditor(props: Props, ref: any) {
redo: function() {
webviewRef.current.injectJavaScript('cm.redo(); true;');
},
select: (anchor: number, head: number) => {
webviewRef.current.injectJavaScript(
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)}); true;`
);
},
insertText: (text: string) => {
webviewRef.current.injectJavaScript(`cm.insertText(${JSON.stringify(text)}); true;`);
},
};
});
@@ -300,6 +338,10 @@ function NoteEditor(props: Props, ref: any) {
console.info('onUndoRedoDepthChange', event);
props.onUndoRedoDepthChange(event);
},
onSelectionChange: (event: SelectionChangeEvent) => {
props.onSelectionChange(event);
},
};
if (handlers[msg.name]) {

View File

@@ -29,8 +29,10 @@ class ScreenHeaderComponent extends React.PureComponent {
constructor() {
super();
this.styles_ = {};
this.state = { showUndoRedoButtons: true };
}
styles() {
const themeId = Setting.value('theme');
if (this.styles_[themeId]) return this.styles_[themeId];
@@ -256,7 +258,7 @@ class ScreenHeaderComponent extends React.PureComponent {
}
const renderTopButton = (options) => {
if (!options.visible) return null;
if (!options.visible || !this.state.showUndoRedoButtons) return null;
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
@@ -422,6 +424,16 @@ class ScreenHeaderComponent extends React.PureComponent {
color: theme.color,
fontSize: theme.fontSize,
}}
onOpen={() => {
this.setState({
showUndoRedoButtons: false,
});
}}
onClose={() => {
this.setState({
showUndoRedoButtons: true,
});
}}
onValueChange={async (folderId, itemIndex) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the

View File

@@ -530,9 +530,9 @@ class ConfigScreenComponent extends BaseScreenComponent {
if (this.state.profileExportStatus === 'prompt') {
const profileExportPrompt = (
<View style={this.styles().settingContainer} key="profileExport">
<Text style={this.styles().settingText}>Path:</Text>
<TextInput style={{ ...this.styles().textInput, paddingRight: 20 }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance}></TextInput>
<Button title="OK" onPress={this.exportProfileButtonPress2_}></Button>
<Text style={{ ...this.styles().settingText, flex: 0 }}>Path:</Text>
<TextInput style={{ ...this.styles().textInput, paddingRight: 20, width: '75%', marginRight: 'auto' }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance} />
<Button title="OK" onPress={this.exportProfileButtonPress2_} />
</View>
);

View File

@@ -488,7 +488,11 @@ class NoteScreenComponent extends BaseScreenComponent {
}
body_selectionChange(event: any) {
this.selection = event.nativeEvent.selection;
if (this.useEditorBeta()) {
this.selection = event.selection;
} else {
this.selection = event.nativeEvent.selection;
}
}
makeSaveAction() {
@@ -708,9 +712,17 @@ class NoteScreenComponent extends BaseScreenComponent {
const newNote = Object.assign({}, this.state.note);
if (this.state.mode == 'edit' && !!this.selection) {
const newText = `\n${resourceTag}\n`;
const prefix = newNote.body.substring(0, this.selection.start);
const suffix = newNote.body.substring(this.selection.end);
newNote.body = `${prefix}\n${resourceTag}\n${suffix}`;
newNote.body = `${prefix}${newText}${suffix}`;
if (this.useEditorBeta()) {
// The beta editor needs to be explicitly informed of changes
// to the note's body
this.editorRef.current.insertText(newText);
}
} else {
newNote.body += `\n${resourceTag}`;
}
@@ -879,11 +891,6 @@ class NoteScreenComponent extends BaseScreenComponent {
output.push({
title: _('Attach...'),
onPress: async () => {
if (this.state.mode === 'edit' && this.useEditorBeta()) {
alert('Attaching files from the beta editor is not yet supported. You may do so from the viewer mode instead.');
return;
}
const buttons = [];
// On iOS, it will show "local files", which means certain files saved from the browser
@@ -1125,7 +1132,9 @@ class NoteScreenComponent extends BaseScreenComponent {
ref={this.editorRef}
themeId={this.props.themeId}
initialText={note.body}
initialSelection={this.selection}
onChange={this.onBodyChange}
onSelectionChange={this.body_selectionChange}
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
style={this.styles().bodyTextInput}
/>;

View File

@@ -8,5 +8,6 @@
"author": "<%= pluginAuthor %>",
"homepage_url": "<%= pluginHomepageUrl %>",
"repository_url": "<%= pluginRepositoryUrl %>",
"keywords": []
"keywords": [],
"categories": []
}

View File

@@ -29,6 +29,7 @@ const userConfig = Object.assign({}, {
const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`;
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management'];
const manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
@@ -67,10 +68,19 @@ function currentGitInfo() {
}
}
function validateCategories(categories) {
if (!categories) return null;
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
categories.forEach(category => {
if (!allPossibleCategories.includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid Categories are: \n${allPossibleCategories}\n`);
});
}
function readManifest(manifestPath) {
const content = fs.readFileSync(manifestPath, 'utf8');
const output = JSON.parse(content);
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
validateCategories(output.categories);
return output;
}

View File

@@ -55,6 +55,8 @@ import SyncTargetNone from './SyncTargetNone';
import { setRSA } from './services/e2ee/ppk';
import RSA from './services/e2ee/RSA.node';
import Resource from './models/Resource';
import { ProfileConfig } from './services/profileConfig/types';
import initProfile from './services/profileConfig/initProfile';
const appLogger: LoggerWrapper = Logger.create('App');
@@ -70,6 +72,7 @@ export default class BaseApplication {
private eventEmitter_: any;
private scheduleAutoAddResourcesIID_: any = null;
private database_: any = null;
private profileConfig_: ProfileConfig = null;
protected showStackTraces_: boolean = false;
protected showPromptString_: boolean = false;
@@ -646,6 +649,12 @@ export default class BaseApplication {
public initRedux() {
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
setStore(this.store_);
this.store_.dispatch({
type: 'PROFILE_CONFIG_SET',
value: this.profileConfig_,
});
BaseModel.dispatch = this.store().dispatch;
FoldersScreenUtils.dispatch = this.store().dispatch;
// reg.dispatch = this.store().dispatch;
@@ -714,14 +723,16 @@ export default class BaseApplication {
// https://immerjs.github.io/immer/docs/freezing
setAutoFreeze(initArgs.env === 'dev');
const profileDir = this.determineProfileDir(initArgs);
const rootProfileDir = this.determineProfileDir(initArgs);
const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir);
this.profileConfig_ = profileConfig;
const resourceDirName = 'resources';
const resourceDir = `${profileDir}/${resourceDirName}`;
const tempDir = `${profileDir}/tmp`;
const cacheDir = `${profileDir}/cache`;
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDirName', resourceDirName);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
@@ -778,6 +789,7 @@ export default class BaseApplication {
appLogger.info(`Profile directory: ${profileDir}`);
appLogger.info(`Root profile directory: ${rootProfileDir}`);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
this.database_.setLogExcludedQueryTypes(['SELECT']);
@@ -820,7 +832,7 @@ export default class BaseApplication {
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplincloud.local:22300');
}
// For now always disable fuzzy search due to performance issues:
@@ -838,6 +850,7 @@ export default class BaseApplication {
}
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
if (isSubProfile) Setting.setValue('welcome.enabled', false);
if (!Setting.value('api.token')) {
void EncryptionService.instance()

View File

@@ -33,6 +33,10 @@ export default class BaseSyncTarget {
return true;
}
public static supportsRecursiveLinkedNotes(): boolean {
return false;
}
public option(name: string, defaultValue: any = null) {
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
}

View File

@@ -38,6 +38,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
return false;
}
public static supportsRecursiveLinkedNotes(): boolean {
return true;
}
public async isAuthenticated() {
return true;
}

View File

@@ -4,33 +4,16 @@ export interface SyncTargetInfo {
label: string;
supportsSelfHosted: boolean;
supportsConfigCheck: boolean;
supportsRecursiveLinkedNotes: boolean;
description: string;
classRef: any;
}
// const syncTargetOrder = [
// 'joplinCloud',
// 'dropbox',
// 'onedrive',
// ];
export default class SyncTargetRegistry {
private static reg_: Record<number, SyncTargetInfo> = {};
private static get reg() {
// if (!this.reg_[0]) {
// this.reg_[0] = {
// id: 0,
// name: SyncTargetNone.targetName(),
// label: SyncTargetNone.label(),
// classRef: SyncTargetNone,
// description: SyncTargetNone.description(),
// supportsSelfHosted: false,
// supportsConfigCheck: false,
// };
// }
return this.reg_;
}
@@ -47,6 +30,10 @@ export default class SyncTargetRegistry {
throw new Error(`Unknown name: ${name}`);
}
public static infoById(id: number): SyncTargetInfo {
return this.infoByName(this.idToName(id));
}
public static addClass(SyncTargetClass: any) {
this.reg[SyncTargetClass.id()] = {
id: SyncTargetClass.id(),
@@ -56,6 +43,7 @@ export default class SyncTargetRegistry {
description: SyncTargetClass.description(),
supportsSelfHosted: SyncTargetClass.supportsSelfHosted(),
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
supportsRecursiveLinkedNotes: SyncTargetClass.supportsRecursiveLinkedNotes(),
};
}

View File

@@ -1,12 +1,28 @@
import Setting, { SettingSectionSource } from '../models/Setting';
import Setting, { SettingSectionSource, SettingStorage } from '../models/Setting';
import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, msleep } from '../testing/test-utils';
import * as fs from 'fs-extra';
import { readFile, stat, mkdirp, writeFile, pathExists, readdir } from 'fs-extra';
import Logger from '../Logger';
import { defaultProfileConfig } from '../services/profileConfig/types';
import { createNewProfile, saveProfileConfig } from '../services/profileConfig';
import initProfile from '../services/profileConfig/initProfile';
async function loadSettingsFromFile(): Promise<any> {
return JSON.parse(await fs.readFile(Setting.settingFilePath, 'utf8'));
return JSON.parse(await readFile(Setting.settingFilePath, 'utf8'));
}
const switchToSubProfileSettings = async () => {
await Setting.reset();
const rootProfileDir = Setting.value('profileDir');
const profileConfigPath = `${rootProfileDir}/profiles.json`;
let profileConfig = defaultProfileConfig();
profileConfig = createNewProfile(profileConfig, 'Sub-profile');
profileConfig.currentProfile = 1;
await saveProfileConfig(profileConfigPath, profileConfig);
const { profileDir } = await initProfile(rootProfileDir);
await mkdirp(profileDir);
await Setting.load();
};
describe('models/Setting', function() {
beforeEach(async (done) => {
@@ -180,19 +196,19 @@ describe('models/Setting', function() {
{
// Double-check that timestamp is indeed changed when the content is
// changed.
const beforeStat = await fs.stat(Setting.settingFilePath);
const beforeStat = await stat(Setting.settingFilePath);
await msleep(1001);
Setting.setValue('sync.mobileWifiOnly', false);
await Setting.saveAll();
const afterStat = await fs.stat(Setting.settingFilePath);
const afterStat = await stat(Setting.settingFilePath);
expect(afterStat.mtime.getTime()).toBeGreaterThan(beforeStat.mtime.getTime());
}
{
const beforeStat = await fs.stat(Setting.settingFilePath);
const beforeStat = await stat(Setting.settingFilePath);
await msleep(1001);
Setting.setValue('sync.mobileWifiOnly', false);
const afterStat = await fs.stat(Setting.settingFilePath);
const afterStat = await stat(Setting.settingFilePath);
await Setting.saveAll();
expect(afterStat.mtime.getTime()).toBe(beforeStat.mtime.getTime());
}
@@ -200,7 +216,7 @@ describe('models/Setting', function() {
it('should handle invalid JSON', (async () => {
const badContent = '{ oopsIforgotTheQuotes: true}';
await fs.writeFile(Setting.settingFilePath, badContent, 'utf8');
await writeFile(Setting.settingFilePath, badContent, 'utf8');
await Setting.reset();
Logger.globalLogger.enabled = false;
@@ -208,12 +224,12 @@ describe('models/Setting', function() {
Logger.globalLogger.enabled = true;
// Invalid JSON file has been moved to .bak file
expect(await fs.pathExists(Setting.settingFilePath)).toBe(false);
expect(await pathExists(Setting.settingFilePath)).toBe(false);
const files = await fs.readdir(Setting.value('profileDir'));
const files = await readdir(Setting.value('profileDir'));
expect(files.length).toBe(1);
expect(files[0].endsWith('.bak')).toBe(true);
expect(await fs.readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
expect(await readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
}));
it('should allow applying default migrations', (async () => {
@@ -256,4 +272,67 @@ describe('models/Setting', function() {
expect(Setting.value('style.editor.contentMaxWidth')).toBe(600); // Changed
}));
it('should load sub-profile settings', async () => {
await Setting.reset();
Setting.setValue('locale', 'fr_FR'); // Global setting
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
Setting.setValue('sync.target', 9); // Local setting
await Setting.saveAll();
await switchToSubProfileSettings();
expect(Setting.value('locale')).toBe('fr_FR'); // Should come from the root profile
expect(Setting.value('theme')).toBe(Setting.THEME_DARK); // Should come from the root profile
expect(Setting.value('sync.target')).toBe(0); // Should come from the local profile
// Also check that the special loadOne() function works as expected
expect((await Setting.loadOne('locale')).value).toBe('fr_FR');
expect((await Setting.loadOne('theme')).value).toBe(Setting.THEME_DARK);
expect((await Setting.loadOne('sync.target')).value).toBe(undefined);
});
it('should save sub-profile settings', async () => {
await Setting.reset();
Setting.setValue('locale', 'fr_FR'); // Global setting
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
await Setting.saveAll();
await switchToSubProfileSettings();
Setting.setValue('locale', 'en_GB'); // Should be saved to global
Setting.setValue('sync.target', 8); // Should be saved to local
await Setting.saveAll();
await Setting.reset();
await Setting.load();
expect(Setting.value('locale')).toBe('en_GB');
expect(Setting.value('theme')).toBe(Setting.THEME_DARK);
expect(Setting.value('sync.target')).toBe(8);
// Double-check that actual file content is correct
const globalSettings = JSON.parse(await readFile(`${Setting.value('rootProfileDir')}/settings-1.json`, 'utf8'));
const localSettings = JSON.parse(await readFile(`${Setting.value('profileDir')}/settings-1.json`, 'utf8'));
expect(globalSettings).toEqual({
'$schema': 'https://joplinapp.org/schema/settings.json',
locale: 'en_GB',
theme: 2,
});
expect(localSettings).toEqual({
'$schema': 'https://joplinapp.org/schema/settings.json',
'sync.target': 8,
});
});
it('all global settings should be saved to file', async () => {
for (const [k, v] of Object.entries(Setting.metadata())) {
if (v.isGlobal && v.storage !== SettingStorage.File) throw new Error(`Setting "${k}" is global but storage is not "file"`);
}
});
});

View File

@@ -7,6 +7,8 @@ import SyncTargetRegistry from '../SyncTargetRegistry';
import time from '../time';
import FileHandler, { SettingValues } from './settings/FileHandler';
import Logger from '../Logger';
import mergeGlobalAndLocalSettings from '../services/profileConfig/mergeGlobalAndLocalSettings';
import splitGlobalAndLocalSettings from '../services/profileConfig/splitGlobalAndLocalSettings';
const { sprintf } = require('sprintf-js');
const ObjectUtils = require('../ObjectUtils');
const { toTitleCase } = require('../string-utils.js');
@@ -59,6 +61,18 @@ export interface SettingItem {
autoSave?: boolean;
storage?: SettingStorage;
hideLabel?: boolean;
// In a multi-profile context, all settings are by default local - they take
// their value from the current profile. This flag can be set to specify
// that the setting is global and that its value should come from the root
// profile. This flag only applies to sub-profiles.
//
// At the moment, all global settings must be saved to file (have the
// storage attribute set to "file") because it's simpler to load the root
// profile settings.json than load the whole SQLite database. This
// restriction is not an issue normally since all settings that are
// considered global are also the user-facing ones.
isGlobal?: boolean;
}
interface SettingItems {
@@ -112,6 +126,7 @@ export interface Constants {
resourceDirName: string;
resourceDir: string;
profileDir: string;
rootProfileDir: string;
tempDir: string;
pluginDataDir: string;
cacheDir: string;
@@ -119,6 +134,7 @@ export interface Constants {
flagOpenDevTools: boolean;
syncVersion: number;
startupDevPlugins: string[];
isSubProfile: boolean;
}
interface SettingSections {
@@ -243,6 +259,7 @@ class Setting extends BaseModel {
resourceDirName: '',
resourceDir: '',
profileDir: '',
rootProfileDir: '',
tempDir: '',
pluginDataDir: '',
cacheDir: '',
@@ -250,6 +267,7 @@ class Setting extends BaseModel {
flagOpenDevTools: false,
syncVersion: 3,
startupDevPlugins: [],
isSubProfile: false,
};
public static autoSaveEnabled = true;
@@ -264,6 +282,7 @@ class Setting extends BaseModel {
private static customSections_: SettingSections = {};
private static changedKeys_: string[] = [];
private static fileHandler_: FileHandler = null;
private static rootFileHandler_: FileHandler = null;
private static settingFilename_: string = 'settings.json';
static tableName() {
@@ -291,6 +310,10 @@ class Setting extends BaseModel {
return `${this.value('profileDir')}/${this.settingFilename_}`;
}
public static get rootSettingFilePath(): string {
return `${this.value('rootProfileDir')}/${this.settingFilename_}`;
}
public static get settingFilename(): string {
return this.settingFilename_;
}
@@ -306,6 +329,13 @@ class Setting extends BaseModel {
return this.fileHandler_;
}
public static get rootFileHandler(): FileHandler {
if (!this.rootFileHandler_) {
this.rootFileHandler_ = new FileHandler(this.rootSettingFilePath);
}
return this.rootFileHandler_;
}
static keychainService() {
if (!this.keychainService_) throw new Error('keychainService has not been set!!');
return this.keychainService_;
@@ -359,6 +389,7 @@ class Setting extends BaseModel {
public: false,
appTypes: [AppType.Desktop],
storage: SettingStorage.File,
isGlobal: true,
},
'sync.openSyncWizard': {
@@ -669,6 +700,7 @@ class Setting extends BaseModel {
};
},
storage: SettingStorage.File,
isGlobal: true,
},
'sync.3.auth': { value: '', type: SettingItemType.String, public: false },
@@ -687,7 +719,7 @@ class Setting extends BaseModel {
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
'sync.10.context': { value: '', type: SettingItemType.String, public: false },
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
// The active folder ID is guaranteed to be valid as long as there's at least one
// existing folder, so it is a good default in contexts where there's no currently
@@ -695,7 +727,7 @@ class Setting extends BaseModel {
// to the last folder that was selected.
activeFolderId: { value: '', type: SettingItemType.String, public: false },
richTextBannerDismissed: { value: false, type: SettingItemType.Bool, public: false },
richTextBannerDismissed: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },
firstStart: { value: true, type: SettingItemType.Bool, public: false },
locale: {
@@ -708,6 +740,7 @@ class Setting extends BaseModel {
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
},
storage: SettingStorage.File,
isGlobal: true,
},
dateFormat: {
value: Setting.DATE_FORMAT_1,
@@ -730,6 +763,7 @@ class Setting extends BaseModel {
return options;
},
storage: SettingStorage.File,
isGlobal: true,
},
timeFormat: {
value: Setting.TIME_FORMAT_1,
@@ -746,6 +780,7 @@ class Setting extends BaseModel {
return options;
},
storage: SettingStorage.File,
isGlobal: true,
},
theme: {
@@ -761,6 +796,7 @@ class Setting extends BaseModel {
section: 'appearance',
options: () => themeOptions(),
storage: SettingStorage.File,
isGlobal: true,
},
themeAutoDetect: {
@@ -771,6 +807,7 @@ class Setting extends BaseModel {
public: true,
label: () => _('Automatically switch theme to match system theme'),
storage: SettingStorage.File,
isGlobal: true,
},
preferredLightTheme: {
@@ -786,6 +823,7 @@ class Setting extends BaseModel {
section: 'appearance',
options: () => themeOptions(),
storage: SettingStorage.File,
isGlobal: true,
},
preferredDarkTheme: {
@@ -801,6 +839,7 @@ class Setting extends BaseModel {
section: 'appearance',
options: () => themeOptions(),
storage: SettingStorage.File,
isGlobal: true,
},
notificationPermission: {
@@ -809,7 +848,7 @@ class Setting extends BaseModel {
public: false,
},
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false, advanced: true, appTypes: [AppType.Desktop], label: () => _('Show note counts') },
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop], label: () => _('Show note counts') },
layoutButtonSequence: {
value: Setting.LAYOUT_ALL,
@@ -824,9 +863,10 @@ class Setting extends BaseModel {
[Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')),
}),
storage: SettingStorage.File,
isGlobal: true,
},
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') },
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') },
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') },
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') },
'notes.sortOrder.field': {
value: 'user_updated_time',
type: SettingItemType.String,
@@ -845,6 +885,7 @@ class Setting extends BaseModel {
return options;
},
storage: SettingStorage.File,
isGlobal: true,
},
'editor.autoMatchingBraces': {
value: true,
@@ -854,8 +895,9 @@ class Setting extends BaseModel {
appTypes: [AppType.Desktop],
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
storage: SettingStorage.File,
isGlobal: true,
},
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
// NOTE: A setting whose name starts with 'notes.sortOrder' is special,
// which implies changing the setting automatically triggers the reflesh of notes.
// See lib/BaseApplication.ts/generalMiddleware() for details.
@@ -868,6 +910,7 @@ class Setting extends BaseModel {
label: () => _('Show sort order buttons'),
// description: () => _('If true, sort order buttons (field + reverse) for notes are shown at the top of Note List.'),
appTypes: [AppType.Desktop],
isGlobal: true,
},
'notes.perFieldReversalEnabled': {
value: true,
@@ -931,8 +974,8 @@ class Setting extends BaseModel {
},
storage: SettingStorage.File,
},
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, public: true, label: () => _('Save geo-location with notes') },
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Save geo-location with notes') },
// 2020-10-29: For now disable the beta editor due to
// underlying bugs in the TextInput component which we cannot
@@ -947,6 +990,8 @@ class Setting extends BaseModel {
appTypes: [AppType.Mobile],
label: () => 'Opt-in to the editor beta',
description: () => 'This beta adds list continuation and syntax highlighting. If you find bugs, please report them in the Discourse forum.',
storage: SettingStorage.File,
isGlobal: true,
},
newTodoFocus: {
@@ -964,6 +1009,7 @@ class Setting extends BaseModel {
};
},
storage: SettingStorage.File,
isGlobal: true,
},
newNoteFocus: {
value: 'body',
@@ -980,6 +1026,7 @@ class Setting extends BaseModel {
};
},
storage: SettingStorage.File,
isGlobal: true,
},
'plugins.states': {
@@ -1005,31 +1052,31 @@ class Setting extends BaseModel {
},
// Deprecated - use markdown.plugin.*
'markdown.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
'markdown.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
'markdown.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
// Deprecated
'markdown.plugin.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
'markdown.plugin.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
'markdown.plugin.linkify': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
'markdown.plugin.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
'markdown.plugin.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
'markdown.plugin.linkify': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
'markdown.plugin.katex': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
'markdown.plugin.fountain': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
'markdown.plugin.mermaid': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
'markdown.plugin.katex': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
'markdown.plugin.fountain': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
'markdown.plugin.mermaid': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` },
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` },
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
'markdown.plugin.mark': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` },
'markdown.plugin.footnote': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
'markdown.plugin.toc': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
'markdown.plugin.sub': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` },
'markdown.plugin.sup': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` },
'markdown.plugin.deflist': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
'markdown.plugin.abbr': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
'markdown.plugin.emoji': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
'markdown.plugin.insert': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
'markdown.plugin.multitable': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` },
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` },
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, isGlobal: true, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
'markdown.plugin.mark': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` },
'markdown.plugin.footnote': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
'markdown.plugin.toc': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
'markdown.plugin.sub': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` },
'markdown.plugin.sup': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` },
'markdown.plugin.deflist': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
'markdown.plugin.abbr': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
'markdown.plugin.emoji': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
// Tray icon (called AppIndicator) doesn't work in Ubuntu
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
@@ -1046,9 +1093,10 @@ class Setting extends BaseModel {
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : _('This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.');
},
storage: SettingStorage.File,
isGlobal: true,
},
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') },
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') },
collapsedFolderIds: { value: [], type: SettingItemType.Array, public: false },
@@ -1072,9 +1120,9 @@ class Setting extends BaseModel {
},
// Deprecated in favour of windowContentZoomFactor
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
'style.editor.fontSize': { value: 15, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
'style.editor.fontSize': { value: 15, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
'style.editor.fontFamily':
(mobilePlatform) ?
({
@@ -1101,6 +1149,7 @@ class Setting extends BaseModel {
};
},
storage: SettingStorage.File,
isGlobal: true,
}) : {
value: '',
type: SettingItemType.String,
@@ -1111,6 +1160,7 @@ class Setting extends BaseModel {
description: () =>
_('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'),
storage: SettingStorage.File,
isGlobal: true,
},
'style.editor.monospaceFontFamily': {
value: '',
@@ -1122,9 +1172,10 @@ class Setting extends BaseModel {
description: () =>
_('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'),
storage: SettingStorage.File,
isGlobal: true,
},
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true,appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
@@ -1149,6 +1200,8 @@ class Setting extends BaseModel {
label: () => _('Custom stylesheet for rendered Markdown'),
section: 'appearance',
advanced: true,
storage: SettingStorage.File,
isGlobal: true,
},
'style.customCss.joplinApp': {
value: null,
@@ -1167,6 +1220,8 @@ class Setting extends BaseModel {
section: 'appearance',
advanced: true,
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
storage: SettingStorage.File,
isGlobal: true,
},
'sync.clearLocalSyncStateButton': {
@@ -1192,9 +1247,9 @@ class Setting extends BaseModel {
},
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },
'sync.interval': {
value: 300,
type: SettingItemType.Int,
@@ -1214,6 +1269,7 @@ class Setting extends BaseModel {
};
},
storage: SettingStorage.File,
isGlobal: true,
},
'sync.mobileWifiOnly': {
value: false,
@@ -1223,12 +1279,13 @@ class Setting extends BaseModel {
label: () => _('Synchronise only over WiFi connection'),
storage: SettingStorage.File,
appTypes: [AppType.Mobile],
isGlobal: true,
},
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('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.') },
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('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.') },
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isGlobal: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
return {
'A4': _('A4'),
'Letter': _('Letter'),
@@ -1238,7 +1295,7 @@ class Setting extends BaseModel {
'Legal': _('Legal'),
};
} },
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => {
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, isGlobal: true, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => {
return {
'portrait': _('Portrait'),
'landscape': _('Landscape'),
@@ -1261,6 +1318,7 @@ class Setting extends BaseModel {
return output;
},
storage: SettingStorage.File,
isGlobal: true,
},
'editor.spellcheckBeta': {
@@ -1270,6 +1328,8 @@ class Setting extends BaseModel {
appTypes: [AppType.Desktop],
label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)',
description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)',
storage: SettingStorage.File,
isGlobal: true,
},
'net.customCertificates': {
@@ -1324,8 +1384,8 @@ class Setting extends BaseModel {
storage: SettingStorage.File,
},
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File },
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, public: true, appTypes: [AppType.Cli], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File, isGlobal: true },
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
'resourceService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
'searchEngine.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
@@ -1357,8 +1417,8 @@ class Setting extends BaseModel {
'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] },
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] },
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
'spellChecker.language': { value: '', type: SettingItemType.String, storage: SettingStorage.File, public: false },
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false },
'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false },
windowContentZoomFactor: {
value: 100,
@@ -1369,6 +1429,7 @@ class Setting extends BaseModel {
maximum: 300,
step: 10,
storage: SettingStorage.File,
isGlobal: true,
},
'layout.folderList.factor': {
@@ -1384,6 +1445,7 @@ class Setting extends BaseModel {
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
'Restart app to see changes.'),
storage: SettingStorage.File,
isGlobal: true,
},
'layout.noteList.factor': {
value: 1,
@@ -1398,6 +1460,7 @@ class Setting extends BaseModel {
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
'Restart app to see changes.'),
storage: SettingStorage.File,
isGlobal: true,
},
'layout.note.factor': {
value: 2,
@@ -1412,6 +1475,7 @@ class Setting extends BaseModel {
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
'Restart app to see changes.'),
storage: SettingStorage.File,
isGlobal: true,
},
'syncInfoCache': {
@@ -1452,9 +1516,17 @@ class Setting extends BaseModel {
this.metadata_ = Object.assign(this.metadata_, this.customMetadata_);
if (this.value('env') === Env.Dev) this.validateMetadata(this.metadata_);
return this.metadata_;
}
private static validateMetadata(md: SettingItems) {
for (const [k, v] of Object.entries(md)) {
if (v.isGlobal && v.storage !== SettingStorage.File) throw new Error(`Setting "${k}" is global but storage is not "file"`);
}
}
public static skipDefaultMigrations() {
logger.info('Skipping all default migrations...');
@@ -1594,10 +1666,17 @@ class Setting extends BaseModel {
// Low-level method to load a setting directly from the database. Should not be used in most cases.
public static async loadOne(key: string): Promise<CacheItem | null> {
if (this.keyStorage(key) === SettingStorage.File) {
const fromFile = await this.fileHandler.load();
let fileSettings = await this.fileHandler.load();
const md = this.settingMetadata(key);
if (md.isGlobal) {
const rootFileSettings = await this.rootFileHandler.load();
fileSettings = mergeGlobalAndLocalSettings(rootFileSettings, fileSettings);
}
return {
key,
value: fromFile[key],
value: fileSettings[key],
};
}
@@ -1664,11 +1743,17 @@ class Setting extends BaseModel {
const itemsFromFile: CacheItem[] = [];
if (this.canUseFileStorage()) {
const fromFile = await this.fileHandler.load();
for (const k of Object.keys(fromFile)) {
let fileSettings = await this.fileHandler.load();
if (this.value('isSubProfile')) {
const rootFileSettings = await this.rootFileHandler.load();
fileSettings = mergeGlobalAndLocalSettings(rootFileSettings, fileSettings);
}
for (const k of Object.keys(fileSettings)) {
itemsFromFile.push({
key: k,
value: fromFile[k],
value: fileSettings[k],
});
}
}
@@ -2018,7 +2103,15 @@ class Setting extends BaseModel {
await BaseModel.db().transactionExecBatch(queries);
if (this.canUseFileStorage()) await this.fileHandler.save(valuesForFile);
if (this.canUseFileStorage()) {
if (this.value('isSubProfile')) {
const { globalSettings, localSettings } = splitGlobalAndLocalSettings(valuesForFile);
await this.rootFileHandler.save(globalSettings);
await this.fileHandler.save(localSettings);
} else {
await this.fileHandler.save(valuesForFile);
}
}
logger.debug('Settings have been saved.');
}

View File

@@ -37,6 +37,7 @@
"@joplin/renderer": "~2.7",
"@joplin/turndown": "^4.0.61",
"@joplin/turndown-plugin-gfm": "^1.0.43",
"@types/nanoid": "^3.0.0",
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"base64-stream": "^1.0.0",

View File

@@ -5,6 +5,7 @@ import Note from './models/Note';
import Folder from './models/Folder';
import BaseModel from './BaseModel';
import { Store } from 'redux';
import { ProfileConfig } from './services/profileConfig/types';
const ArrayUtils = require('./ArrayUtils.js');
const { ALL_NOTES_FILTER_ID } = require('./reserved-ids');
const { createSelectorCreator, defaultMemoize } = require('reselect');
@@ -92,6 +93,7 @@ export interface State {
isInsertingNotes: boolean;
hasEncryptedItems: boolean;
needApiAuth: boolean;
profileConfig: ProfileConfig;
// Extra reducer keys go here:
pluginService: PluginServiceState;
@@ -162,6 +164,7 @@ export const defaultState: State = {
isInsertingNotes: false,
hasEncryptedItems: false,
needApiAuth: false,
profileConfig: null,
pluginService: pluginServiceDefaultState,
shareService: shareServiceDefaultState,
@@ -1138,6 +1141,10 @@ const reducer = produce((draft: Draft<State> = defaultState, action: any) => {
draft.needApiAuth = action.value;
break;
case 'PROFILE_CONFIG_SET':
draft.profileConfig = action.value;
break;
}
} catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;

View File

@@ -1,14 +1,12 @@
import Logger from '../Logger';
import Setting from '../models/Setting';
import shim from '../shim';
import { fileExtension, basename, toSystemSlashes } from '../path-utils';
import { basename, toSystemSlashes } from '../path-utils';
import time from '../time';
import { NoteEntity } from './database/types';
import Note from '../models/Note';
import { openFileWithExternalEditor } from './ExternalEditWatcher/utils';
const EventEmitter = require('events');
const { splitCommandString } = require('../string-utils');
const spawn = require('child_process').spawn;
const chokidar = require('chokidar');
const { ErrorNotFound } = require('./rest/utils/errors');
@@ -213,68 +211,6 @@ export default class ExternalEditWatcher {
return false;
}
textEditorCommand() {
const editorCommand = Setting.value('editor');
if (!editorCommand) return null;
const s = splitCommandString(editorCommand, { handleEscape: false });
const path = s.splice(0, 1);
if (!path.length) throw new Error(`Invalid editor command: ${editorCommand}`);
return {
path: path[0],
args: s,
};
}
async spawnCommand(path: string, args: string[], options: any) {
return new Promise((resolve, reject) => {
// App bundles need to be opened using the `open` command.
// Additional args can be specified after --args, and the
// -n flag is needed to ensure that the app is always launched
// with the arguments. Without it, if the app is already opened,
// it will just bring it to the foreground without opening the file.
// So the full command is:
//
// open -n /path/to/editor.app --args -app-flag -bla /path/to/file.md
//
if (shim.isMac() && fileExtension(path) === 'app') {
args = args.slice();
args.splice(0, 0, '--args');
args.splice(0, 0, path);
args.splice(0, 0, '-n');
path = 'open';
}
const wrapError = (error: any) => {
if (!error) return error;
const msg = error.message ? [error.message] : [];
msg.push(`Command was: "${path}" ${args.join(' ')}`);
error.message = msg.join('\n\n');
return error;
};
try {
const subProcess = spawn(path, args, options);
const iid = shim.setInterval(() => {
if (subProcess && subProcess.pid) {
this.logger().debug(`Started editor with PID ${subProcess.pid}`);
shim.clearInterval(iid);
resolve(null);
}
}, 100);
subProcess.on('error', (error: any) => {
shim.clearInterval(iid);
reject(wrapError(error));
});
} catch (error) {
throw wrapError(error);
}
});
}
async openAndWatch(note: NoteEntity) {
if (!note || !note.id) {
this.logger().warn('ExternalEditWatcher: Cannot open note: ', note);
@@ -285,13 +221,7 @@ export default class ExternalEditWatcher {
if (!filePath) return;
this.watch(filePath);
const cmd = this.textEditorCommand();
if (!cmd) {
this.bridge_().openExternal(`file://${filePath}`);
} else {
cmd.args.push(filePath);
await this.spawnCommand(cmd.path, cmd.args, { detached: true });
}
await openFileWithExternalEditor(filePath, this.bridge_());
this.dispatch({
type: 'NOTE_FILE_WATCHER_ADD',

View File

@@ -0,0 +1,82 @@
/* eslint-disable import/prefer-default-export */
const { splitCommandString } = require('../../string-utils');
import { spawn } from 'child_process';
import Logger from '../../Logger';
import Setting from '../../models/Setting';
import { fileExtension } from '../../path-utils';
import shim from '../../shim';
const logger = Logger.create('ExternalEditWatcher/utils');
const spawnCommand = async (path: string, args: string[], options: any) => {
return new Promise((resolve, reject) => {
// App bundles need to be opened using the `open` command.
// Additional args can be specified after --args, and the
// -n flag is needed to ensure that the app is always launched
// with the arguments. Without it, if the app is already opened,
// it will just bring it to the foreground without opening the file.
// So the full command is:
//
// open -n /path/to/editor.app --args -app-flag -bla /path/to/file.md
//
if (shim.isMac() && fileExtension(path) === 'app') {
args = args.slice();
args.splice(0, 0, '--args');
args.splice(0, 0, path);
args.splice(0, 0, '-n');
path = 'open';
}
const wrapError = (error: any) => {
if (!error) return error;
const msg = error.message ? [error.message] : [];
msg.push(`Command was: "${path}" ${args.join(' ')}`);
error.message = msg.join('\n\n');
return error;
};
try {
const subProcess = spawn(path, args, options);
const iid = shim.setInterval(() => {
if (subProcess && subProcess.pid) {
logger.debug(`Started editor with PID ${subProcess.pid}`);
shim.clearInterval(iid);
resolve(null);
}
}, 100);
subProcess.on('error', (error: any) => {
shim.clearInterval(iid);
reject(wrapError(error));
});
} catch (error) {
throw wrapError(error);
}
});
};
const textEditorCommand = () => {
const editorCommand = Setting.value('editor');
if (!editorCommand) return null;
const s = splitCommandString(editorCommand, { handleEscape: false });
const path = s.splice(0, 1);
if (!path.length) throw new Error(`Invalid editor command: ${editorCommand}`);
return {
path: path[0],
args: s,
};
};
export const openFileWithExternalEditor = async (filePath: string, bridge: any) => {
const cmd = textEditorCommand();
if (!cmd) {
bridge.openExternal(`file://${filePath}`);
} else {
cmd.args.push(filePath);
await spawnCommand(cmd.path, cmd.args, { detached: true });
}
};

View File

@@ -55,6 +55,9 @@ const defaultKeymapItems = {
{ accelerator: 'Option+Cmd+A', command: 'editor.sortSelectedLines' },
{ accelerator: 'Option+Up', command: 'editor.swapLineUp' },
{ accelerator: 'Option+Down', command: 'editor.swapLineDown' },
{ accelerator: 'Option+Cmd+1', command: 'switchProfile1' },
{ accelerator: 'Option+Cmd+2', command: 'switchProfile2' },
{ accelerator: 'Option+Cmd+3', command: 'switchProfile3' },
],
default: [
{ accelerator: 'Ctrl+N', command: 'newNote' },
@@ -97,6 +100,9 @@ const defaultKeymapItems = {
{ accelerator: 'Ctrl+Alt+S', command: 'editor.sortSelectedLines' },
{ accelerator: 'Alt+Up', command: 'editor.swapLineUp' },
{ accelerator: 'Alt+Down', command: 'editor.swapLineDown' },
{ accelerator: 'Ctrl+Alt+1', command: 'switchProfile1' },
{ accelerator: 'Ctrl+Alt+2', command: 'switchProfile2' },
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
],
};

View File

@@ -6,13 +6,15 @@ import propsHaveChanged from './propsHaveChanged';
const { createSelectorCreator, defaultMemoize } = require('reselect');
const { createCachedSelector } = require('re-reselect');
interface MenuItem {
id: string;
label: string;
click: Function;
export interface MenuItem {
id?: string;
label?: string;
click?: Function;
role?: any;
type?: string;
accelerator?: string;
enabled: boolean;
checked?: boolean;
enabled?: boolean;
}
interface MenuItems {

View File

@@ -30,6 +30,7 @@ export interface WhenClauseContext {
folderIsShared: boolean;
folderIsShareRoot: boolean;
joplinServerConnected: boolean;
hasMultiProfiles: boolean;
}
export default function stateToWhenClauseContext(state: State, options: WhenClauseContextOptions = null): WhenClauseContext {
@@ -82,5 +83,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
folderIsShared: commandFolder ? !!commandFolder.share_id : false,
joplinServerConnected: [9, 10].includes(state.settings['sync.target']),
hasMultiProfiles: state.profileConfig && state.profileConfig.profiles.length > 1,
};
}

View File

@@ -8,6 +8,9 @@
//
// If the userContentBaseUrl is an empty string, the baseUrl is returned instead.
export default function(userId: string, baseUrl: string, userContentBaseUrl: string) {
// Special case for development, because it's difficult to get wildcard domains working locally.
if (userContentBaseUrl === 'http://joplincloud.local:22300') return 'http://joplincloud.local:22300';
if (userContentBaseUrl && baseUrl !== userContentBaseUrl) {
if (!userId) throw new Error('User ID must be specified');
const url = new URL(userContentBaseUrl);

View File

@@ -45,6 +45,7 @@ export default function manifestFromObject(o: any): PluginManifest {
homepage_url: getString('homepage_url', false),
repository_url: getString('repository_url', false),
keywords: getStrings('keywords', false),
categories: getStrings('categories', false),
permissions: permissions,
_recommended: getBoolean('_recommended', false, false),

View File

@@ -13,6 +13,7 @@ export interface PluginManifest {
homepage_url?: string;
repository_url?: string;
keywords?: string[];
categories?: string[];
permissions?: PluginPermission[];
// Private keys

View File

@@ -0,0 +1,85 @@
import { writeFile } from 'fs-extra';
import { createNewProfile, getProfileFullPath, loadProfileConfig, saveProfileConfig } from '.';
import { tempFilePath } from '../../testing/test-utils';
import { defaultProfile, defaultProfileConfig, ProfileConfig } from './types';
describe('profileConfig/index', () => {
it('should load a default profile config', async () => {
const filePath = tempFilePath('json');
const config = await loadProfileConfig(filePath);
expect(config).toEqual(defaultProfileConfig());
});
it('should load a profile config', async () => {
const filePath = tempFilePath('json');
const config = {
profiles: [
{
name: 'Testing',
path: '.',
},
],
};
await writeFile(filePath, JSON.stringify(config), 'utf8');
const loadedConfig = await loadProfileConfig(filePath);
const expected: ProfileConfig = {
version: 1,
currentProfile: 0,
profiles: [
{
name: 'Testing',
path: '.',
},
],
};
expect(loadedConfig).toEqual(expected);
});
it('should load a save a config', async () => {
const filePath = tempFilePath('json');
const config = defaultProfileConfig();
await saveProfileConfig(filePath, config);
const loadedConfig = await loadProfileConfig(filePath);
expect(config).toEqual(loadedConfig);
});
it('should get a profile full path', async () => {
const profile1 = {
...defaultProfile(),
path: 'profile-abcd',
};
const profile2 = {
...defaultProfile(),
path: '.',
};
const profile3 = {
...defaultProfile(),
path: 'profiles/pro/',
};
expect(getProfileFullPath(profile1, '/test/root')).toBe('/test/root/profile-abcd');
expect(getProfileFullPath(profile2, '/test/root')).toBe('/test/root');
expect(getProfileFullPath(profile3, '/test/root')).toBe('/test/root/profiles/pro');
});
it('should create a new profile', async () => {
let config = defaultProfileConfig();
config = createNewProfile(config, 'new profile 1');
config = createNewProfile(config, 'new profile 2');
expect(config.profiles.length).toBe(3);
expect(config.profiles[1].name).toBe('new profile 1');
expect(config.profiles[2].name).toBe('new profile 2');
expect(config.profiles[1].path).not.toBe(config.profiles[2].path);
});
});

View File

@@ -0,0 +1,64 @@
import { rtrimSlashes, trimSlashes } from '../../path-utils';
import shim from '../../shim';
import { defaultProfile, defaultProfileConfig, Profile, ProfileConfig } from './types';
import { customAlphabet } from 'nanoid/non-secure';
export const loadProfileConfig = async (profileConfigPath: string): Promise<ProfileConfig> => {
if (!(await shim.fsDriver().exists(profileConfigPath))) {
return defaultProfileConfig();
}
try {
const configContent = await shim.fsDriver().readFile(profileConfigPath, 'utf8');
const parsed = JSON.parse(configContent) as ProfileConfig;
if (!parsed.profiles || !parsed.profiles.length) throw new Error(`Profile config should contain at least one profile: ${profileConfigPath}`);
const output: ProfileConfig = {
...defaultProfileConfig(),
...parsed,
};
for (let i = 0; i < output.profiles.length; i++) {
output.profiles[i] = {
...defaultProfile(),
...output.profiles[i],
};
}
if (output.currentProfile < 0 || output.currentProfile >= output.profiles.length) throw new Error(`Profile index out of range: ${output.currentProfile}`);
return output;
} catch (error) {
error.message = `Could not parse profile configuration: ${profileConfigPath}: ${error.message}`;
throw error;
}
};
export const saveProfileConfig = async (profileConfigPath: string, config: ProfileConfig) => {
await shim.fsDriver().writeFile(profileConfigPath, JSON.stringify(config, null, '\t'), 'utf8');
};
export const getCurrentProfile = (config: ProfileConfig): Profile => {
return { ...config.profiles[config.currentProfile] };
};
export const getProfileFullPath = (profile: Profile, rootProfilePath: string): string => {
let p = trimSlashes(profile.path);
if (p === '.') p = '';
return rtrimSlashes(`${rtrimSlashes(rootProfilePath)}/${p}`);
};
const profileIdGenerator = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8);
export const createNewProfile = (config: ProfileConfig, profileName: string) => {
const newConfig = {
...config,
profiles: config.profiles.slice(),
};
newConfig.profiles.push({
name: profileName,
path: `profile-${profileIdGenerator()}`,
});
return newConfig;
};

View File

@@ -0,0 +1,16 @@
import { getCurrentProfile, getProfileFullPath, loadProfileConfig } from '.';
import Setting from '../../models/Setting';
export default async (rootProfileDir: string) => {
const profileConfig = await loadProfileConfig(`${rootProfileDir}/profiles.json`);
const profileDir = getProfileFullPath(getCurrentProfile(profileConfig), rootProfileDir);
const isSubProfile = profileConfig.currentProfile !== 0;
Setting.setConstant('isSubProfile', isSubProfile);
Setting.setConstant('rootProfileDir', rootProfileDir);
Setting.setConstant('profileDir', profileDir);
return {
profileConfig,
profileDir,
isSubProfile,
};
};

View File

@@ -0,0 +1,22 @@
import Setting from '../../models/Setting';
export default (rootSettings: Record<string, any>, subProfileSettings: Record<string, any>) => {
const output: Record<string, any> = { ...subProfileSettings };
for (const k of Object.keys(output)) {
const md = Setting.settingMetadata(k);
if (md.isGlobal) {
delete output[k];
if (k in rootSettings) output[k] = rootSettings[k];
}
}
for (const k of Object.keys(rootSettings)) {
const md = Setting.settingMetadata(k);
if (md.isGlobal) {
output[k] = rootSettings[k];
}
}
return output;
};

View File

@@ -0,0 +1,19 @@
import Setting from '../../models/Setting';
import { SettingValues } from '../../models/settings/FileHandler';
export default (settings: SettingValues) => {
const globalSettings: SettingValues = {};
const localSettings: SettingValues = {};
for (const [k, v] of Object.entries(settings)) {
const md = Setting.settingMetadata(k);
if (md.isGlobal) {
globalSettings[k] = v;
} else {
localSettings[k] = v;
}
}
return { globalSettings, localSettings };
};

View File

@@ -0,0 +1,27 @@
export interface Profile {
name: string;
path: string;
}
export interface ProfileConfig {
version: number;
currentProfile: number;
profiles: Profile[];
}
export const defaultProfile = (): Profile => {
return {
name: 'Default',
path: '.',
};
};
export const defaultProfileConfig = (): ProfileConfig => {
return {
version: 1,
currentProfile: 0,
profiles: [defaultProfile()],
};
};
export type ProfileSwitchClickHandler = (profileIndex: number)=> void;

View File

@@ -9,6 +9,7 @@ import Tag from '../../models/Tag';
import NoteTag from '../../models/NoteTag';
import ResourceService from '../../services/ResourceService';
import SearchEngine from '../../services/searchengine/SearchEngine';
import { ResourceEntity } from '../database/types';
const createFolderForPagination = async (num: number, time: number) => {
await Folder.save({
@@ -354,7 +355,7 @@ describe('services_rest_Api', function() {
},
]);
const resourceV1 = (await Resource.all())[0];
const resourceV1: ResourceEntity = (await Resource.all())[0];
await msleep(1);
@@ -366,7 +367,7 @@ describe('services_rest_Api', function() {
},
]);
const resourceV2 = (await Resource.all())[0];
const resourceV2: ResourceEntity = (await Resource.all())[0];
expect(resourceV2.title).toBe('resource mod');
expect(resourceV2.mime).toBe('image/png');
@@ -378,6 +379,25 @@ describe('services_rest_Api', function() {
expect(resourceV2.size).toBe((await shim.fsDriver().stat(Resource.fullPath(resourceV2))).size);
}));
it('should update resource properties', (async () => {
await api.route(RequestMethod.POST, 'resources', null, JSON.stringify({
title: 'resource',
}), [{ path: `${supportDir}/photo.jpg` }]);
const resourceV1: ResourceEntity = (await Resource.all())[0];
await msleep(1);
await api.route(RequestMethod.PUT, `resources/${resourceV1.id}`, null, JSON.stringify({
title: 'my new title',
}));
const resourceV2: ResourceEntity = (await Resource.all())[0];
expect(resourceV2.title).toBe('my new title');
expect(resourceV2.mime).toBe(resourceV1.mime);
}));
it('should delete resources', (async () => {
const f = await Folder.save({ title: 'mon carnet' });

View File

@@ -50,7 +50,17 @@ export default async function(request: Request, id: string = null, link: string
if (request.method === RequestMethod.POST || request.method === RequestMethod.PUT) {
const isUpdate = request.method === RequestMethod.PUT;
if (!request.files.length) throw new ErrorBadRequest('Resource cannot be created without a file');
if (!request.files.length) {
if (request.method === RequestMethod.PUT) {
// In that case, we don't try to update the resource blob, we
// just update the properties.
return defaultAction(BaseModel.TYPE_RESOURCE, request, id, link);
} else {
// If it's a POST request, the file content is required.
throw new ErrorBadRequest('Resource cannot be created without a file');
}
}
if (isUpdate && !id) throw new ErrorBadRequest('Missing resource ID');
const filePath = request.files[0].path;
const defaultProps = request.bodyJson(readonlyProperties(request.method));

View File

@@ -53,7 +53,7 @@ describe('ShareService', function() {
},
});
await msleep(1);
await service.shareNote(note.id);
await service.shareNote(note.id, false);
function checkTimestamps(previousNote: NoteEntity, newNote: NoteEntity) {
// After sharing or unsharing, only the updated_time property should

View File

@@ -228,11 +228,14 @@ export default class ShareService {
}
}
public async shareNote(noteId: string): Promise<StateShare> {
public async shareNote(noteId: string, recursive: boolean): Promise<StateShare> {
const note = await Note.load(noteId);
if (!note) throw new Error(`No such note: ${noteId}`);
const share = await this.api().exec('POST', 'api/shares', {}, { note_id: noteId });
const share = await this.api().exec('POST', 'api/shares', {}, {
note_id: noteId,
recursive: recursive ? 1 : 0,
});
await Note.save({
id: note.id,

View File

@@ -106,6 +106,7 @@ const supportDir = `${oldTestDir}/support`;
// various space-in-path issues.
const dataDir = `${oldTestDir}/test data/${suiteName_}`;
const profileDir = `${dataDir}/profile`;
const rootProfileDir = profileDir;
fs.mkdirpSync(logDir);
fs.mkdirpSync(baseTempDir);
@@ -185,6 +186,7 @@ Setting.setConstant('tempDir', baseTempDir);
Setting.setConstant('cacheDir', baseTempDir);
Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('rootProfileDir', rootProfileDir);
Setting.setConstant('env', 'dev');
BaseService.logger_ = logger;
@@ -271,6 +273,8 @@ async function switchClient(id: number, options: any = null) {
await Setting.reset();
Setting.settingFilename = `settings-${id}.json`;
Setting.setConstant('profileDir', rootProfileDir);
Setting.setConstant('rootProfileDir', rootProfileDir);
Setting.setConstant('resourceDirName', resourceDirName(id));
Setting.setConstant('resourceDir', resourceDir(id));
Setting.setConstant('pluginDir', pluginDir(id));
@@ -330,6 +334,9 @@ async function setupDatabase(id: number = null, options: any = null) {
// running.
await Setting.reset();
Setting.setConstant('profileDir', rootProfileDir);
Setting.setConstant('rootProfileDir', rootProfileDir);
if (databases_[id]) {
BaseModel.setDb(databases_[id]);
await clearDatabase(id);

View File

@@ -1,6 +1,10 @@
const fs = require('fs-extra');
import { pathExistsSync, readFileSync } from 'fs-extra';
export async function credentialDir() {
// All these calls used to be async but certain scripts need to load config
// files early, so they've been converted to sync calls. Do not convert them
// back to async.
export function credentialDir() {
const username = require('os').userInfo().username;
const toTry = [
@@ -11,23 +15,23 @@ export async function credentialDir() {
];
for (const dirPath of toTry) {
if (await fs.pathExists(dirPath)) return dirPath;
if (pathExistsSync(dirPath)) return dirPath;
}
throw new Error(`Could not find credential directory in any of these paths: ${JSON.stringify(toTry)}`);
}
export async function credentialFile(filename: string) {
const rootDir = await credentialDir();
export function credentialFile(filename: string) {
const rootDir = credentialDir();
const output = `${rootDir}/${filename}`;
if (!(await fs.pathExists(output))) throw new Error(`No such file: ${output}`);
if (!(pathExistsSync(output))) throw new Error(`No such file: ${output}`);
return output;
}
export async function readCredentialFile(filename: string, defaultValue: string = '') {
export function readCredentialFile(filename: string, defaultValue: string = '') {
try {
const filePath = await credentialFile(filename);
const r = await fs.readFile(filePath);
const filePath = credentialFile(filename);
const r = readFileSync(filePath);
// There's normally no reason to keep the last new line character and it
// can cause problems in certain scripts, so trim it. Any other white
// space should also not be relevant.
@@ -36,3 +40,16 @@ export async function readCredentialFile(filename: string, defaultValue: string
return defaultValue;
}
}
export function readCredentialFileJson<T>(filename: string, defaultValue: T = null): T {
const v = readCredentialFile(filename);
if (!v) return defaultValue;
try {
const o = JSON.parse(v);
return o;
} catch (error) {
error.message = `Could not parse JSON file ${filename}: ${error.message}`;
throw error;
}
}

View File

@@ -112,7 +112,7 @@ export function findPrice(prices: StripePublicConfigPrice[], query: FindPriceQue
const features: Record<FeatureId, PlanFeature> = {
maxItemSize: {
title: 'Publish notes to the internet',
title: 'Max note or attachment size',
basic: true,
pro: true,
teams: true,

View File

@@ -1,5 +1,5 @@
const createUuidV4 = require('uuid/v4');
const { customAlphabet } = require('nanoid/non-secure');
import { customAlphabet } from 'nanoid/non-secure';
// https://zelark.github.io/nano-id-cc/
// https://security.stackexchange.com/a/41749/1873

View File

@@ -148,6 +148,7 @@ export interface RuleOptions {
// Used by checkboxes to specify how it should be rendered
checkboxRenderingType?: number;
checkboxDisabled?: boolean;
// Used by the keyword highlighting plugin (mobile only)
highlightedKeywords?: any[];

View File

@@ -53,7 +53,7 @@ function pluginAssets(theme: any) {
];
}
function createPrefixTokens(Token: any, id: string, checked: boolean, label: string, postMessageSyntax: string, sourceToken: any): any[] {
function createPrefixTokens(Token: any, id: string, checked: boolean, label: string, postMessageSyntax: string, sourceToken: any, disabled: boolean): any[] {
let token = null;
const tokens = [];
@@ -89,6 +89,7 @@ function createPrefixTokens(Token: any, id: string, checked: boolean, label: str
token = new Token('checkbox_input', 'input', 0);
token.attrs = [['type', 'checkbox'], ['id', id], ['onclick', js]];
if (disabled) token.attrs.push(['disabled', 'disabled']);
if (checked) token.attrs.push(['checked', 'checked']);
tokens.push(token);
@@ -169,7 +170,7 @@ function checkboxPlugin(markdownIt: any, options: RuleOptions) {
// Prepend the text content with the checkbox markup and the opening <label> tag
// then append the </label> tag at the end of the text content.
const prefix = createPrefixTokens(Token, id, checked, label, options.postMessageSyntax, token);
const prefix = createPrefixTokens(Token, id, checked, label, options.postMessageSyntax, token, !!options.checkboxDisabled);
const suffix = createSuffixTokens(Token);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix);

View File

@@ -22,8 +22,10 @@ export default {
markdownIt.renderer.rules[key] = (tokens: any[], idx: number, options: any, env: any, self: any) => {
if (!!tokens[idx].map && tokens[idx].level <= allowedLevel) {
const line = tokens[idx].map[0];
const lineEnd = tokens[idx].map[1];
tokens[idx].attrJoin('class', 'maps-to-line');
tokens[idx].attrSet('source-line', `${line}`);
tokens[idx].attrSet('source-line-end', `${lineEnd}`);
}
if (precedentRule) {
return precedentRule(tokens, idx, options, env, self);

View File

@@ -1,23 +1,24 @@
# Installing
## Configuration
## Requirements
First copy `.env-sample` to `.env` and edit the values in there:
- Docker Engine runs Joplin Server. See [Install Docker Engine](https://docs.docker.com/engine/install/) for steps to install Docker Engine for your operating system.
- Docker Compose is required to store item contents (notes, tags, etc.) if PostgreSQL is not used. See [Install Docker Compose](https://docs.docker.com/compose/install/) for steps to install Docker Compose for your operating system.
- `APP_BASE_URL`: This is the base public URL where the service will be running. For example, if you want it to run from `https://example.com/joplin`, this is what you should set the URL to. The base URL can include the port.
- `APP_PORT`: The local port on which the Docker container will listen. You would typically map this port to 443 (TLS) with a reverse proxy.
## Configure Docker for Joplin Server
## Running the server
To start the server with default configuration, run:
1. Copy `.env-sample` (located [here](https://github.com/laurent22/joplin/blob/dev/.env-sample)) to the location of your Docker configuration files. Example: /home/[user]/docker
2. Rename the file `.env-sample` to `.env`.
3. Run the following command to test starting the server using the default configuration:
```shell
docker run --env-file .env -p 22300:22300 joplin/server:latest
```
This will start the server, which will listen on port **22300** on **localhost**. By default it will use SQLite, which allows you to test the app without setting up a database. To run it for production though, you'll want to connect the container to a database, as described below.
The server will listen on port **22300** on **localhost**. By default, the server will use SQLite, which allows you to test the app without setting up a database. When running the server for production use, you should connect the container to a database, as described below.
## Supported docker tags
## Supported Docker tags
The following tags are available:
@@ -29,7 +30,7 @@ The following tags are available:
## Setup the database
You can setup the container to either use an existing PostgreSQL server, or connect it to a new one using docker-compose
You can setup the container to either use an existing PostgreSQL server, or connect it to a new database using docker-compose.
### Using an existing PostgreSQL server
@@ -44,39 +45,48 @@ POSTGRES_PORT=5432
POSTGRES_HOST=localhost
```
Make sure that the provided database and user exist as the server will not create them. When running on macOS or Windows through Docker Desktop, a mapping of localhost is made automatically. On Linux, you can add `--net=host --add-host=host.docker.internal:127.0.0.1` to the `docker run` command line to make the mapping happen. Any other `POSTGRES_HOST` than localhost or 127.0.0.1 should work as expected without further action.
Ensure that the provided database and user exist as Joplin Server will not create them. When running on macOS or Windows through Docker Desktop, a mapping of localhost is made automatically. On Linux, you can add `--net=host --add-host=host.docker.internal:127.0.0.1` to the `docker run` command line to make the mapping happen. Any other `POSTGRES_HOST` than localhost or 127.0.0.1 should work as expected without further action.
### Using docker-compose
A [sample docker-compose file](https://github.com/laurent22/joplin/blob/dev/docker-compose.server.yml
) is available to show how to use Docker to install both the database and server and connect them:
1. Using the [sample docker-compose file](https://github.com/laurent22/joplin/blob/dev/docker-compose.server.yml), create a docker compose file in the location of your Docker configuration files. Example: /home/[user]/docker/docker-compose.yml
2. Update the fields in the docker-compose file as seen in the sample file.
## Setup reverse proxy
Once Joplin Server is running, you will then need to expose it to the internet by setting up a reverse proxy, and that will depend on how your server is currently configured, and whether you already have Nginx or Apache running:
This step is optional.
Configuring a reverse proxy is not required for core functionality and is only required if Joplin Server needs to be accessible over the internet. See the following documentation for configuring a reverse proxy with Apache or Nginx.
- [Apache Reverse Proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html)
- [Nginx Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
## Setup storage
By default, the item contents (notes, tags, etc.) are stored in the database and you don't need to do anything special to get that working.
This step is optional.
However since that content can be quite large, you also have the option to store it outside the database by setting the `STORAGE_DRIVER` environment variable.
By default, the item contents (notes, tags, etc.) are stored in the database and no additional steps are required to get that working.
However, since that content can be quite large, you have the option to store it outside the database by setting the `STORAGE_DRIVER` environment variable.
### Setting up storage on a new installation
Again this is optional - by default items will simply be saved to the database. To save to the local filesystem instead, use:
This step is optional.
To save item contents (notes, tags, etc.) to the local filesystem instead, use:
STORAGE_DRIVER=Type=Filesystem; Path=/path/to/dir
Then all item data will be saved under this `/path/to/dir` directory.
After this is set, all item contents will be saved under the defined `/path/to/dir` directory.
### Migrating storage for an existing installation
Migrating storage is a bit more complicated because the old content will have to be migrated to the new storage. This is done by providing a fallback driver, which tells the server where to look if a particular item is not yet available on the new storage.
This step is optional.
To migrate from the database to the file system for example, you would set the environment variables like so:
Migrating storage is a bit more complicated because the old content will have to be migrated to the new storage. This is done by providing a fallback driver, which tells the server where to look if a particular item is not yet available on the new storage.
To migrate from the database to the file system, you would set the environment variables as follows:
STORAGE_DRIVER=Type=Filesystem; Path=/path/to/dir
STORAGE_DRIVER_FALLBACK=Type=Database; Mode=ReadAndWrite
@@ -111,13 +121,15 @@ Besides the database and filesystem, it's also possible to use AWS S3 for storag
STORAGE_DRIVER=Type=S3; Region=YOUR_REGION_CODE; AccessKeyId=YOUR_ACCESS_KEY; SecretAccessKeyId=YOUR_SECRET_ACCESS_KEY; Bucket=YOUR_BUCKET
## Setup the website
## Verify access to the admin page
Once the server is exposed to the internet, you can open the admin UI and get it ready for synchronisation. For the following instructions, we'll assume that the Joplin server is running on `https://example.com/joplin`.
Once Joplin Server is exposed to the internet, open the admin UI. For the following instructions, we'll assume that Joplin Server is running on `https://example.com/joplin`.
### Secure the admin user
If Joplin Server is running running locally only, access the Admin Page using `http://[hostname]:22300`
By default, the instance will be setup with an admin user with email **admin@localhost** and password **admin** and you should change this. To do so, open `https://example.com/joplin/login` and login as admin. Then go to the Profile section and change the admin password.
### Update the admin user credentials
By default, Joplin Server will be setup with an admin user with email **admin@localhost** and password **admin**. For security purposes, the admin user's credentials should be changed. On the Admin Page, login as the admin user. In the upper right, select the Profile button update the admin password.
### Create a user for sync

View File

@@ -203,6 +203,7 @@ async function renderNote(share: Share, note: NoteEntity, resourceInfos: Resourc
audioPlayerEnabled: false,
videoPlayerEnabled: false,
pdfViewerEnabled: false,
checkboxDisabled: true,
linkRenderingType: 2,
};

View File

@@ -92,7 +92,7 @@ function platformFromTag(tagName: string): Platform {
}
function filterLogs(logs: LogEntry[], platform: Platform) {
const output = [];
const output: LogEntry[] = [];
const revertedLogs = [];
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
@@ -126,7 +126,7 @@ function filterLogs(logs: LogEntry[], platform: Platform) {
if (platform === 'cli' && prefix.indexOf('cli') >= 0) addIt = true;
if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true;
if (platform === 'server' && prefix.indexOf('server') >= 0) addIt = true;
if (platform === 'cloud' && (prefix.indexOf('cloud') >= 0 || prefix.indexOf('server'))) addIt = true;
if (platform === 'cloud' && (prefix.indexOf('cloud') >= 0 || prefix.indexOf('server') >= 0)) addIt = true;
// Translation updates often comes in format "Translation: Update pt_PT.po"
// but that's not useful in a changelog especially since most people
@@ -137,6 +137,11 @@ function filterLogs(logs: LogEntry[], platform: Platform) {
addIt = false;
}
// Remove duplicate messages
if (output.find(l => l.message === log.message)) {
addIt = false;
}
if (addIt) output.push(log);
}

View File

@@ -7,14 +7,16 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: genneko <genneko217@gmail.com>\n"
"Language-Team: \n"
"Language: ja_JP\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.4.2\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.0.1\n"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:565
msgid "- Camera: to allow taking a picture and attaching it to a note."
@@ -264,11 +266,11 @@ msgstr "辞書に追加"
#: packages/server/src/services/MustacheService.ts:183
#: packages/server/src/services/MustacheService.ts:307
msgid "Admin"
msgstr ""
msgstr "管理者"
#: packages/server/src/routes/admin/dashboard.ts:10
msgid "Admin dashboard"
msgstr ""
msgstr "管理者ダッシュボード"
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:189
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:147
@@ -393,16 +395,15 @@ msgstr "自動"
#: packages/server/src/services/TaskService.ts:39
msgid "Auto-add disabled accounts for deletion"
msgstr ""
msgstr "無効アカウントを削除に自動追加"
#: packages/lib/models/Setting.ts:855
msgid "Auto-pair braces, parenthesis, quotations, etc."
msgstr "始めの括弧や引用符入力時に終わりの括弧や引用符を自動入力する。"
#: packages/lib/models/Setting.ts:1195
#, fuzzy
msgid "Automatically check for updates"
msgstr "アップデートのチェック..."
msgstr "アップデートを自動的に確認する"
#: packages/lib/models/Setting.ts:772
msgid "Automatically switch theme to match system theme"
@@ -705,7 +706,7 @@ msgstr "完了: %s (%s)"
#: packages/server/src/services/TaskService.ts:37
msgid "Compress old changes"
msgstr ""
msgstr "古い変更を圧縮する"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:637
#: packages/app-mobile/components/side-menu-content.js:332
@@ -767,9 +768,8 @@ msgid "Copy external link"
msgstr "外部リンクをコピー"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:134
#, fuzzy
msgid "Copy image"
msgstr "トークンのコピー"
msgstr "画像のコピー"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:172
msgid "Copy Link Address"
@@ -861,14 +861,12 @@ msgid "Create a notebook"
msgstr "ノートブックを作成します"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:161
#, fuzzy
msgid "Create notebook"
msgstr "ノートブック作成します"
msgstr "ノートブック作成"
#: packages/server/src/routes/admin/users.ts:171
#, fuzzy
msgid "Create user"
msgstr "作成しました:%s"
msgstr "ユーザーを作成"
#: packages/app-desktop/gui/NotePropertiesDialog.min.js:29
msgid "Created"
@@ -960,7 +958,7 @@ msgstr "暗い"
#: packages/server/src/services/MustacheService.ts:136
msgid "Dashboard"
msgstr ""
msgstr "ダッシュボード"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:625
msgid "Database v%s"
@@ -1017,14 +1015,12 @@ msgid "Delete attachment \"%s\"?"
msgstr "添付ファイル \"%s\" を削除しますか?"
#: packages/server/src/services/TaskService.ts:36
#, fuzzy
msgid "Delete expired sessions"
msgstr "数式表現を有効にする"
msgstr "期限切れセッションを削除"
#: packages/server/src/services/TaskService.ts:31
#, fuzzy
msgid "Delete expired tokens"
msgstr "ノート \"%d\" を削除しますか?"
msgstr "期限切れトークンを削除"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:88
msgid "Delete line"
@@ -1342,12 +1338,12 @@ msgstr "Emacs"
#: packages/app-desktop/gui/SyncWizard/Dialog.tsx:236
#: packages/server/src/routes/admin/emails.ts:128
msgid "Email"
msgstr ""
msgstr "Eメール"
#: packages/server/src/routes/admin/emails.ts:112
#: packages/server/src/services/MustacheService.ts:152
msgid "Emails"
msgstr ""
msgstr "Eメール"
#: packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx:194
msgid "emphasised text"
@@ -1697,9 +1693,8 @@ msgstr ""
"力してください"
#: packages/lib/models/Setting.ts:569
#, fuzzy
msgid "Force path style"
msgstr "Path Styleを強制する"
msgstr "パス形式を強制する"
#: packages/lib/commands/historyForward.ts:6
msgid "Forward"
@@ -1789,7 +1784,7 @@ msgstr "ハイライト"
#: packages/server/src/services/MustacheService.ts:167
#: packages/server/src/services/MustacheService.ts:300
msgid "Home"
msgstr ""
msgstr "ホーム"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:78
msgid "Horizontal Rule"
@@ -1833,7 +1828,7 @@ msgstr "TLS証明書のエラーを無視"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:102
msgid "Images"
msgstr ""
msgstr "画像"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.tsx:170
#: packages/app-desktop/gui/MenuBar.tsx:484
@@ -2016,7 +2011,7 @@ msgstr "アイテム \"%s\" はダウンロードできませんでした:%s"
#: packages/server/src/services/MustacheService.ts:175
#: packages/server/src/services/MustacheService.ts:302
msgid "Items"
msgstr ""
msgstr "アイテム"
#: packages/lib/services/ReportService.ts:208
msgid "Items that cannot be decrypted"
@@ -2231,11 +2226,11 @@ msgstr "OneDriveログイン"
#: packages/server/src/services/MustacheService.ts:306
msgid "Logout"
msgstr ""
msgstr "ログアウト"
#: packages/server/src/services/MustacheService.ts:179
msgid "Logs"
msgstr ""
msgstr "ログ"
#: packages/app-desktop/gui/MenuBar.tsx:716
#: packages/app-mobile/components/screens/ConfigScreen.tsx:583
@@ -2697,9 +2692,8 @@ msgid "Or create an account."
msgstr "またはアカウントを作成。"
#: packages/app-desktop/gui/MenuBar.tsx:352
#, fuzzy
msgid "Other applications..."
msgstr "アプリケーションの終了。"
msgstr "その他のアプリケーション..."
#: packages/app-cli/app/command-import.js:27
msgid "Output format: %s"
@@ -2901,19 +2895,19 @@ msgstr "プライバシーポリシー"
#: packages/server/src/services/TaskService.ts:35
msgid "Process failed payment subscriptions"
msgstr ""
msgstr "サブスクリプションの支払い処理に失敗しました"
#: packages/server/src/services/TaskService.ts:33
msgid "Process oversized accounts"
msgstr ""
msgstr "容量オーバーのアカウントを処理する"
#: packages/server/src/services/TaskService.ts:38
msgid "Process user deletions"
msgstr ""
msgstr "ユーザー削除を実施する"
#: packages/server/src/routes/admin/users.ts:168
msgid "Profile"
msgstr ""
msgstr "プロファイル"
#: packages/lib/versionInfo.ts:26
msgid "Profile Version: %s"
@@ -3099,27 +3093,22 @@ msgid "S3"
msgstr "S3"
#: packages/lib/models/Setting.ts:547
#, fuzzy
msgid "S3 access key"
msgstr "アクセスキー"
msgstr "S3 アクセスキー"
#: packages/lib/models/Setting.ts:507
#, fuzzy
msgid "S3 bucket"
msgstr "S3 バケット"
#: packages/lib/models/Setting.ts:536
#, fuzzy
msgid "S3 region"
msgstr "リージョン"
msgstr "S3 リージョン"
#: packages/lib/models/Setting.ts:558
#, fuzzy
msgid "S3 secret key"
msgstr "シークレットアクセスキー"
msgstr "S3 シークレットアクセスキー"
#: packages/lib/models/Setting.ts:522
#, fuzzy
msgid "S3 URL"
msgstr "S3 URL"
@@ -3198,14 +3187,12 @@ msgid "Select all"
msgstr "すべて選択"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:140
#, fuzzy
msgid "Select emoji..."
msgstr "日付の選択"
msgstr "絵文字の選択..."
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:144
#, fuzzy
msgid "Select file..."
msgstr "すべて選択"
msgstr "ファイルの選択..."
#: packages/app-cli/app/command-server.js:38
msgid "Server is already running on port %d"
@@ -3625,7 +3612,7 @@ msgstr "写真を撮影する"
#: packages/server/src/services/MustacheService.ts:148
#: packages/server/src/services/MustacheService.ts:304
msgid "Tasks"
msgstr ""
msgstr "タスク"
#: packages/lib/models/Setting.ts:1230
msgid "Text editor command"
@@ -4014,7 +4001,7 @@ msgstr "DropboxでJoplinを同期する場合は、次のステップを実行
#: packages/lib/components/EncryptionConfigScreen/utils.ts:54
msgid "To continue, please enter your master password below."
msgstr "続行するには、マスターパスワードを入力してください"
msgstr "続行するには、マスターパスワードを入力してください"
#: packages/app-cli/app/app-gui.js:452
msgid "To delete a tag, untag the associated notes."
@@ -4210,14 +4197,12 @@ msgstr "アップデート"
#: packages/server/src/routes/admin/users.ts:171
#: packages/server/src/routes/index/users.ts:89
#, fuzzy
msgid "Update profile"
msgstr "プロファイルをエクスポート"
msgstr "プロファイルの更新"
#: packages/server/src/services/TaskService.ts:32
#, fuzzy
msgid "Update total sizes"
msgstr "ローカルアイテムの更新: %d."
msgstr "総容量を更新する"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx:208
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx:209
@@ -4321,13 +4306,13 @@ msgstr ""
#: packages/server/src/services/MustacheService.ts:144
msgid "User deletions"
msgstr ""
msgstr "ユーザー削除"
#: packages/server/src/routes/admin/users.ts:107
#: packages/server/src/services/MustacheService.ts:140
#: packages/server/src/services/MustacheService.ts:301
msgid "Users"
msgstr ""
msgstr "ユーザー"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:182
msgid "Valid"

View File

@@ -11,14 +11,16 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Felipe Viggiano <felipeviggiano@gmail.com>\n"
"Last-Translator: Renato Nunes Bastos <rnbastos@gmail.com>\n"
"Language-Team: \n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Gtranslator 40.0\n"
"X-Generator: Poedit 2.2.1\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:565
msgid "- Camera: to allow taking a picture and attaching it to a note."
@@ -269,11 +271,11 @@ msgstr "Adicionar ao dicionário"
#: packages/server/src/services/MustacheService.ts:183
#: packages/server/src/services/MustacheService.ts:307
msgid "Admin"
msgstr ""
msgstr "Admin"
#: packages/server/src/routes/admin/dashboard.ts:10
msgid "Admin dashboard"
msgstr ""
msgstr "Dashboard de Admin "
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:189
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:147
@@ -401,16 +403,15 @@ msgstr "Automático"
#: packages/server/src/services/TaskService.ts:39
msgid "Auto-add disabled accounts for deletion"
msgstr ""
msgstr "Adicionar automaticamente contas desabilitadas para deleção"
#: packages/lib/models/Setting.ts:855
msgid "Auto-pair braces, parenthesis, quotations, etc."
msgstr "Parear automaticamente chaves, parênteses, aspas etc."
#: packages/lib/models/Setting.ts:1195
#, fuzzy
msgid "Automatically check for updates"
msgstr "Verificar atualizações..."
msgstr "Verificar atualizações automaticamente"
#: packages/lib/models/Setting.ts:772
msgid "Automatically switch theme to match system theme"
@@ -568,6 +569,9 @@ msgid ""
"enabled end-to-end encryption. They may do so from the screen Configuration "
"> Encryption."
msgstr ""
"Não é possível compartilhar caderno criptografado com %s porque ele não tem "
"habilitado a criptografia ponta-a-ponta. Ele pode fazer isso a partir da "
"tela Configuração > Criptografia."
#: packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.ts:7
msgid "Change application layout"
@@ -716,7 +720,7 @@ msgstr "Completado: %s (%s)"
#: packages/server/src/services/TaskService.ts:37
msgid "Compress old changes"
msgstr ""
msgstr "Comprimir mudanças antigas"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:637
#: packages/app-mobile/components/side-menu-content.js:332
@@ -749,9 +753,8 @@ msgid "Conflicts (attachments)"
msgstr "Conflitos (anexos)"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.tsx:108
#, fuzzy
msgid "Content provided by %s"
msgstr "Propriedades do conteúdo"
msgstr "Conteúdo fornecido por %s"
#: packages/app-mobile/components/screens/Note.tsx:933
msgid "Convert to note"
@@ -776,14 +779,12 @@ msgstr ""
#: packages/app-desktop/gui/Sidebar/Sidebar.tsx:355
#: packages/app-desktop/gui/Sidebar/Sidebar.tsx:369
#: packages/app-desktop/gui/utils/NoteListUtils.ts:139
#, fuzzy
msgid "Copy external link"
msgstr "Encerrar edição externa"
msgstr "Copiar link externo"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:134
#, fuzzy
msgid "Copy image"
msgstr "Copiar token"
msgstr "Copiar imagem"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:172
msgid "Copy Link Address"
@@ -854,6 +855,10 @@ msgid ""
"\n"
"The error was: \"%s\""
msgstr ""
"Não foi possível responder o convite. Por favor, tente novamente, ou "
"verifique com o dono do cadernos se ele ainda está sendo compartilhado. \n"
"\n"
"O erro foi: \"%s\""
#: packages/lib/components/EncryptionConfigScreen/utils.ts:219
msgid "Could not upgrade master key: %s"
@@ -864,20 +869,20 @@ msgid ""
"Could not verify the share status of this notebook - aborting. Please try "
"again when you are connected to the internet."
msgstr ""
"Não foi possível verificar o status de compartilhamento deste caderno - "
"abortando. Por favor, tente de novo quando estiver conectado na internet."
#: packages/app-mobile/components/note-list.js:101
msgid "Create a notebook"
msgstr "Criar um caderno"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:161
#, fuzzy
msgid "Create notebook"
msgstr "Criar um caderno"
msgstr "Criar caderno"
#: packages/server/src/routes/admin/users.ts:171
#, fuzzy
msgid "Create user"
msgstr "Criado: %s"
msgstr "Criar usuário"
#: packages/app-desktop/gui/NotePropertiesDialog.min.js:29
msgid "Created"
@@ -969,7 +974,7 @@ msgstr "Escuro"
#: packages/server/src/services/MustacheService.ts:136
msgid "Dashboard"
msgstr ""
msgstr "Dashboard"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:625
msgid "Database v%s"
@@ -1026,14 +1031,12 @@ msgid "Delete attachment \"%s\"?"
msgstr "Excluir o anexo \"%s\"?"
#: packages/server/src/services/TaskService.ts:36
#, fuzzy
msgid "Delete expired sessions"
msgstr "Habilitar expressões matemáticas"
msgstr "Excluir sessões expiradas"
#: packages/server/src/services/TaskService.ts:31
#, fuzzy
msgid "Delete expired tokens"
msgstr "Excluir estas %d notas?"
msgstr "Excluir tokens expirados"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:88
msgid "Delete line"
@@ -1223,16 +1226,14 @@ msgid "Do not ask for confirmation."
msgstr "Não pedir confirmação."
#: packages/lib/components/EncryptionConfigScreen/utils.ts:56
#, fuzzy
msgid ""
"Do not lose the password as, for security purposes, this will be the *only* "
"way to decrypt the data! To enable encryption, please enter your password "
"below."
msgstr ""
"Habilitar a criptografia significa que *todas* as suas notas e anexos serão "
"ressincronizados e reenviados com encriptação. Não perca sua senha, pois, "
"por medidas de segurança, esse será o *único* modo de descriptografar seus "
"dados! Para habilitar a criptografia, por favor insira sua senha abaixo."
"Não perca a senha já que, por segurança, esse será o *único* modo de "
"descriptografar os dados! Para habilitar a criptografia, por favor entre a "
"senha abaixo."
#: packages/app-desktop/checkForUpdates.ts:199
msgid "Download"
@@ -1352,12 +1353,12 @@ msgstr "Emacs"
#: packages/app-desktop/gui/SyncWizard/Dialog.tsx:236
#: packages/server/src/routes/admin/emails.ts:128
msgid "Email"
msgstr ""
msgstr "Email"
#: packages/server/src/routes/admin/emails.ts:112
#: packages/server/src/services/MustacheService.ts:152
msgid "Emails"
msgstr ""
msgstr "Emails"
#: packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx:194
msgid "emphasised text"
@@ -1465,13 +1466,12 @@ msgid "Enabled"
msgstr "Habilitado"
#: packages/lib/components/EncryptionConfigScreen/utils.ts:51
#, fuzzy
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent encrypted to the sync target."
msgstr ""
"Desabilitar a criptografia significa que *todas* as suas notas e anexos "
"serão ressincronizados e enviados sem criptografia. Você quer continuar?"
"Hbilitar a criptografia significa que *todas* as suas notas e anexos serão "
"ressincronizados e enviados com criptografia pra o alvo de sincronização."
#: packages/lib/models/BaseItem.ts:808
msgid "Encrypted"
@@ -1500,19 +1500,16 @@ msgid "Encryption is: %s"
msgstr "Criptografia está: %s"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:172
#, fuzzy
msgid "Encryption keys"
msgstr "Criptografia está:"
msgstr "Chaves de criptografia"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:249
#, fuzzy
msgid "Encryption:"
msgstr "Criptografia"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:247
#, fuzzy
msgid "End-to-end encryption"
msgstr "Habilitar criptografia"
msgstr "Criptografia ponta-a-ponta"
#: packages/app-mobile/components/screens/dropbox-login.js:66
msgid "Enter code here"
@@ -1713,7 +1710,7 @@ msgstr ""
#: packages/lib/models/Setting.ts:569
msgid "Force path style"
msgstr ""
msgstr "Forçar estilo de caminho"
#: packages/lib/commands/historyForward.ts:6
msgid "Forward"
@@ -1738,9 +1735,8 @@ msgid "General"
msgstr "Geral"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:252
#, fuzzy
msgid "Generated"
msgstr "Geral"
msgstr "Gerado"
#: packages/app-desktop/gui/ShareNoteDialog.tsx:207
msgid "Generating link..."
@@ -1792,9 +1788,8 @@ msgid "Hide %s"
msgstr "Ocultar %s"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:172
#, fuzzy
msgid "Hide disabled keys"
msgstr "Esconder chaves mestras desabilitadas"
msgstr "Esconder chaves desabilitadas"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.ts:22
msgid "Hide Joplin"
@@ -1807,7 +1802,7 @@ msgstr "Realçar"
#: packages/server/src/services/MustacheService.ts:167
#: packages/server/src/services/MustacheService.ts:300
msgid "Home"
msgstr ""
msgstr "Início"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:78
msgid "Horizontal Rule"
@@ -1851,7 +1846,7 @@ msgstr "Ignorar erros de certificados TLS"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:102
msgid "Images"
msgstr ""
msgstr "Imagens"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.tsx:170
#: packages/app-desktop/gui/MenuBar.tsx:484
@@ -2023,9 +2018,8 @@ msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor da opção inválido: \"%s\". Os valores possíveis são: %s."
#: packages/app-cli/app/command-e2ee.ts:46
#, fuzzy
msgid "Invalid password"
msgstr "Resposta inválida: %s"
msgstr "Senha inválida"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:38
msgid "Italic"
@@ -2038,7 +2032,7 @@ msgstr "O item \"%s\" não pôde ser baixado: %s"
#: packages/server/src/services/MustacheService.ts:175
#: packages/server/src/services/MustacheService.ts:302
msgid "Items"
msgstr ""
msgstr "Itens"
#: packages/lib/services/ReportService.ts:208
msgid "Items that cannot be decrypted"
@@ -2152,9 +2146,8 @@ msgid "Keychain Supported: %s"
msgstr "Keychain Suportada: %s"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:71
#, fuzzy
msgid "Keys that need upgrading"
msgstr "Chaves-mestras que necessitam de atualização"
msgstr "Chaves que necessitam de atualização"
#: packages/lib/models/Setting.ts:1244
msgid "Landscape"
@@ -2181,9 +2174,8 @@ msgid "Layout button sequence"
msgstr "Sequência de botão de leiaute"
#: packages/app-desktop/gui/MainScreen/commands/leaveSharedFolder.ts:10
#, fuzzy
msgid "Leave notebook..."
msgstr "Compartilhar caderno..."
msgstr "Sair do caderno..."
#: packages/lib/models/Setting.ts:1238
msgid "Legal"
@@ -2259,11 +2251,11 @@ msgstr "Login com OneDrive"
#: packages/server/src/services/MustacheService.ts:306
msgid "Logout"
msgstr ""
msgstr "Logout"
#: packages/server/src/services/MustacheService.ts:179
msgid "Logs"
msgstr ""
msgstr "Logs"
#: packages/app-desktop/gui/MenuBar.tsx:716
#: packages/app-mobile/components/screens/ConfigScreen.tsx:583
@@ -2271,14 +2263,12 @@ msgid "Make a donation"
msgstr "Fazer uma doação"
#: packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx:212
#, fuzzy
msgid "Manage master password"
msgstr "Digite a senha mestra:"
msgstr "Gerenciar senha mestra"
#: packages/lib/commands/openMasterPasswordDialog.ts:6
#, fuzzy
msgid "Manage master password..."
msgstr "Digite a senha mestra:"
msgstr "Gerenciar senha mestra..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx:314
msgid "Manage your plugins"
@@ -2286,7 +2276,6 @@ msgstr "Gerencie seus plugins"
#. `generate-ppk`
#: packages/app-cli/app/command-e2ee.ts:20
#, fuzzy
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status`, `decrypt-file`, and `target-status`."
@@ -2307,7 +2296,7 @@ msgstr "Markdown"
#: packages/lib/services/interop/InteropService.ts:119
#: packages/lib/services/interop/InteropService.ts:66
msgid "Markdown + Front Matter"
msgstr ""
msgstr "Markdown + Front Matter"
#: packages/app-cli/app/command-done.js:14
msgid "Marks a to-do as done."
@@ -2341,9 +2330,8 @@ msgid "Max concurrent connections"
msgstr "Máximo de conexões simultâneas"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:324
#, fuzzy
msgid "Missing keys"
msgstr "Chaves-Mestras Faltando"
msgstr "Chaves faltando"
#: packages/app-mobile/components/screens/encryption-config.tsx:271
msgid "Missing Master Keys"
@@ -2520,7 +2508,7 @@ msgstr "Não baixado"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:252
msgid "Not generated"
msgstr ""
msgstr "Não gerado"
#: packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx:110
#: packages/server/src/models/UserModel.ts:214
@@ -2597,9 +2585,8 @@ msgid "Note&book"
msgstr "Nota&Caderno"
#: packages/lib/models/Setting.ts:2145
#, fuzzy
msgid "Notebook"
msgstr "Cadernos"
msgstr "Caderno"
#: packages/lib/models/Setting.ts:1380
msgid "Notebook list growth factor"
@@ -2621,9 +2608,8 @@ msgstr ""
#: packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderField.ts:8
#: packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderReverse.ts:9
#, fuzzy
msgid "Notes"
msgstr "Nota"
msgstr "Notas"
#: packages/lib/models/Setting.ts:2161
msgid "Notes and settings are stored in: %s"
@@ -2730,9 +2716,8 @@ msgid "Or create an account."
msgstr "Ou crie uma nova nota."
#: packages/app-desktop/gui/MenuBar.tsx:352
#, fuzzy
msgid "Other applications..."
msgstr "Sai da aplicação."
msgstr "Outras aplicações..."
#: packages/app-cli/app/command-import.js:27
msgid "Output format: %s"
@@ -2790,7 +2775,7 @@ msgstr "Permissão para utilizar a câmera"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:271
msgid "Please click on \"%s\" to proceed"
msgstr ""
msgstr "Por favor, clique em \"%s\" para prosseguir"
#: packages/lib/components/EncryptionConfigScreen/utils.ts:65
msgid ""
@@ -2811,6 +2796,8 @@ msgid ""
"Please note that if it is a large notebook, it may take a few minutes for "
"all the notes to show up on the recipient's device."
msgstr ""
"Por favor, note que se esse é um caderno grande, pode levar alguns minutos "
"para todas as notas aprecerem no dispositivo do destinatário."
#: packages/lib/onedrive-api-node-utils.js:116
msgid ""
@@ -2938,19 +2925,19 @@ msgstr "Política de Privacidade"
#: packages/server/src/services/TaskService.ts:35
msgid "Process failed payment subscriptions"
msgstr ""
msgstr "Processar pagamentos de assinaturas com falha"
#: packages/server/src/services/TaskService.ts:33
msgid "Process oversized accounts"
msgstr ""
msgstr "Processar contas com tamanho acima do limite"
#: packages/server/src/services/TaskService.ts:38
msgid "Process user deletions"
msgstr ""
msgstr "Processar exclusão de usuários"
#: packages/server/src/routes/admin/users.ts:168
msgid "Profile"
msgstr ""
msgstr "Perfil"
#: packages/lib/versionInfo.ts:26
msgid "Profile Version: %s"
@@ -2962,7 +2949,7 @@ msgstr "Propriedades"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:252
msgid "Public-private key pair:"
msgstr ""
msgstr "Par de chaves público-privado:"
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.ts:6
msgid "Publish note..."
@@ -3070,9 +3057,8 @@ msgstr "Renovar token"
#: packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx:212
#: packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx:213
#, fuzzy
msgid "Reset master password"
msgstr "Digite a senha mestra:"
msgstr "Redefinir senha mestra"
#: packages/app-cli/app/command-import.js:51
#: packages/app-desktop/gui/ImportScreen.min.js:72
@@ -3134,31 +3120,27 @@ msgstr ""
#: packages/lib/SyncTargetAmazonS3.js:28
msgid "S3"
msgstr ""
msgstr "S3"
#: packages/lib/models/Setting.ts:547
#, fuzzy
msgid "S3 access key"
msgstr "AWS key"
msgstr "Chave de acesso S3"
#: packages/lib/models/Setting.ts:507
#, fuzzy
msgid "S3 bucket"
msgstr "AWS S3 bucket"
msgstr "S3 bucket"
#: packages/lib/models/Setting.ts:536
msgid "S3 region"
msgstr ""
msgstr "Região S3"
#: packages/lib/models/Setting.ts:558
#, fuzzy
msgid "S3 secret key"
msgstr "AWS secret"
msgstr "Chave secreta S3"
#: packages/lib/models/Setting.ts:522
#, fuzzy
msgid "S3 URL"
msgstr "AWS S3 URL"
msgstr "URL S3"
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:579
msgid ""
@@ -3235,14 +3217,12 @@ msgid "Select all"
msgstr "Selecionar tudo"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:140
#, fuzzy
msgid "Select emoji..."
msgstr "Selecionar data"
msgstr "Selecionar emoji..."
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:144
#, fuzzy
msgid "Select file..."
msgstr "Selecionar tudo"
msgstr "Selecionar arquivo"
#: packages/app-cli/app/command-server.js:38
msgid "Server is already running on port %d"
@@ -3268,11 +3248,12 @@ msgid "Set alarm:"
msgstr "Definir alarme:"
#: packages/lib/models/Setting.ts:1127
#, fuzzy
msgid ""
"Set it to 0 to make it take the complete available space. Recommended width "
"is 600."
msgstr "Fixe em 0 para fazer com que ocupe todo o espaço disponível."
msgstr ""
"Configure em 0 para fazer com que ocupe todo o espaço disponível. A largura "
"recomendada é 600."
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:627
msgid "Set the password"
@@ -3325,18 +3306,16 @@ msgid "Show completed to-dos"
msgstr "Mostrar tarefas completas"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:172
#, fuzzy
msgid "Show disabled keys"
msgstr "Mostrar chaves mestras desabilitadas"
msgstr "Mostrar chaves desabilitadas"
#: packages/lib/models/Setting.ts:812
msgid "Show note counts"
msgstr "Exibir contagem de notas"
#: packages/lib/models/Setting.ts:868
#, fuzzy
msgid "Show sort order buttons"
msgstr "Exibir contagem de notas"
msgstr "Exibir botões de ordenação"
#: packages/lib/models/Setting.ts:1044
msgid "Show tray icon"
@@ -3666,7 +3645,7 @@ msgstr "Tirar foto"
#: packages/server/src/services/MustacheService.ts:148
#: packages/server/src/services/MustacheService.ts:304
msgid "Tasks"
msgstr ""
msgstr "Tarefas"
#: packages/lib/models/Setting.ts:1230
msgid "Text editor command"
@@ -3762,16 +3741,14 @@ msgid "The following attachments are being watched for changes:"
msgstr "Os seguintes anexos estão sendo observados para alterações:"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:72
#, fuzzy
msgid ""
"The following keys use an out-dated encryption algorithm and it is "
"recommended to upgrade them. The upgraded key will still be able to decrypt "
"and encrypt your data as usual."
msgstr ""
"As seguintes chaves-mestras utilizam um algoritmo de criptografia "
"desatualizado e é recomendável que você as atualize. A chave-mestra "
"atualizada ainda será capaz de descriptografar e criptografar normalmente os "
"seus dados."
"As seguintes chaves utilizam um algoritmo de criptografia desatualizado e é "
"recomendável que você as atualize. A chave atualizada ainda será capaz de "
"descriptografar e criptografar normalmente os seus dados."
#: packages/app-mobile/components/screens/Note.tsx:193
msgid "The Joplin mobile app does not currently support this type of link: %s"
@@ -3782,19 +3759,18 @@ msgid ""
"The Joplin team has vetted this plugin and it meets our standards for "
"security and performance."
msgstr ""
"A equipe do Joplin aprovou este plugin e ele atende aos requisitos de "
"segurança e performance"
"A equipe do Joplin examinou este plugin e ele atende aos nossos padrões de "
"segurança e performance."
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:325
#, fuzzy
msgid ""
"The keys with these IDs are used to encrypt some of your items, however the "
"application does not currently have access to them. It is likely they will "
"eventually be downloaded via synchronisation."
msgstr ""
"As chaves-mestras com essas IDs são usadas para criptografar alguns de seus "
"itens, contudo a aplicação atualmente não tem acesso à elas. Provavelmente, "
"elas serão baixadas via sincronização."
"As chaves com essas IDs são usadas para criptografar alguns de seus itens, "
"contudo a aplicação atualmente não tem acesso a elas. Provavelmente, elas "
"serão baixadas via sincronização."
#: packages/lib/components/EncryptionConfigScreen/utils.ts:217
msgid "The master key has been upgraded successfully!"
@@ -3833,6 +3809,9 @@ msgid ""
"\n"
"The error was: \"%s\""
msgstr ""
"O destinatário não pôde ser removido da lista. Por favor, tente novamente\n"
"\n"
"O erro foi: \"%s\""
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:585
msgid ""
@@ -3845,10 +3824,10 @@ msgstr ""
"terá que ser reiniciado. Para proceder, por favor, clique neste link."
#: packages/app-mobile/components/screen-header.js:461
#, fuzzy
msgid "The sync target needs to be upgraded. Press this banner to proceed."
msgstr ""
"O alvo de sincronização deve passar por upgrade! Rode `%s` para proceder."
"O alvo de sincronização deve passar por upgrade! Clique nesse banner para "
"executar."
#: packages/lib/models/Tag.ts:204
msgid "The tag \"%s\" already exists. Please choose a different name."
@@ -4030,13 +4009,12 @@ msgid "This will open a new screen. Save your current changes?"
msgstr "Isso abrirá uma nova tela. Salvar seu progresso atual?"
#: packages/app-desktop/gui/MainScreen/commands/leaveSharedFolder.ts:16
#, fuzzy
msgid ""
"This will remove the notebook from your collection and you will no longer "
"have access to its content. Do you wish to continue?"
msgstr ""
"Descompartilhar este caderno? Os destinatários não terão mais acesso ao seu "
"conteúdo."
"Isso vai remover o cadernos de sua coleção e você não vai mais ter acesso ao "
"conteúdo. Deseja continuar?"
#: packages/lib/models/Setting.ts:739
msgid "Time format"
@@ -4061,9 +4039,8 @@ msgstr ""
"seguintes passos:"
#: packages/lib/components/EncryptionConfigScreen/utils.ts:54
#, fuzzy
msgid "To continue, please enter your master password below."
msgstr "Digite a senha mestra"
msgstr "Para continuar, por favor entre com sua senha mestre abaixo."
#: packages/app-cli/app/app-gui.js:452
msgid "To delete a tag, untag the associated notes."
@@ -4142,9 +4119,8 @@ msgid "Toggle note list"
msgstr "Alternar lista de notas"
#: packages/app-desktop/gui/MainScreen/commands/togglePerFolderSortOrder.ts:7
#, fuzzy
msgid "Toggle own sort order"
msgstr "Alternar modo seguro"
msgstr "Alternar a própria ordenação"
#: packages/app-desktop/commands/toggleSafeMode.ts:8
msgid "Toggle safe mode"
@@ -4155,9 +4131,8 @@ msgid "Toggle sidebar"
msgstr "Alternar barra lateral"
#: packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderField.ts:7
#, fuzzy
msgid "Toggle sort order field"
msgstr "Alternar modo seguro"
msgstr "Alternar campo de ordenação"
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:40
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:41
@@ -4262,14 +4237,12 @@ msgstr "Atualizar"
#: packages/server/src/routes/admin/users.ts:171
#: packages/server/src/routes/index/users.ts:89
#, fuzzy
msgid "Update profile"
msgstr "Exportar perfil"
msgstr "Atualizar perfil"
#: packages/server/src/services/TaskService.ts:32
#, fuzzy
msgid "Update total sizes"
msgstr "Itens locais atualizados: %d."
msgstr "Atualizar tamanhos"
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx:208
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.tsx:209
@@ -4376,13 +4349,13 @@ msgstr ""
#: packages/server/src/services/MustacheService.ts:144
msgid "User deletions"
msgstr ""
msgstr "Exclusões de usuários"
#: packages/server/src/routes/admin/users.ts:107
#: packages/server/src/services/MustacheService.ts:140
#: packages/server/src/services/MustacheService.ts:301
msgid "Users"
msgstr ""
msgstr "Usuários"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:182
msgid "Valid"
@@ -4535,12 +4508,15 @@ msgstr "Seus dados serão recriptografados e sincronizados novamente."
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:271
msgid "Your master password is needed to decrypt some of your data."
msgstr ""
"Sua senha mestra é necessária para descriptografar alguns de seus dados."
#: packages/app-cli/app/command-sync.ts:242
msgid ""
"Your password is needed to decrypt some of your data. Type `:e2ee decrypt` "
"to set it."
msgstr ""
"Sua senha é necessária para descriptografar alguns de seus dados. Digite `:"
"e2ee decrypt` para ver."
#: packages/app-mobile/components/CameraView.tsx:189
msgid "Your permission to use your camera is required."
@@ -4618,12 +4594,10 @@ msgstr "Menos zoom"
#~ msgid "Delete these notes?"
#~ msgstr "Excluir estas notas?"
#, javascript-format
#~ msgid "Encryption will be enabled using the master key created on %s"
#~ msgstr ""
#~ "A encriptação será habilitada utilizando a senha master criada em %s"
#, javascript-format
#~ msgid "%s %s (%s)"
#~ msgstr "%s %s (%s)"
@@ -4725,7 +4699,6 @@ msgstr "Menos zoom"
#~ msgid "Notebook properties"
#~ msgstr "Propriedades do caderno"
#, javascript-format
#~ msgid "This file could not be opened: %s"
#~ msgstr "Este arquivo não pôde ser aberto: %s"

View File

@@ -1748,7 +1748,7 @@ msgstr[1] "Skapar länkar..."
#: packages/app-desktop/gui/ExtensionBadge.min.js:44
msgid "Get it now:"
msgstr "Hämta den nu:"
msgstr "Hämta det nu:"
#: packages/lib/models/Setting.ts:1196
msgid "Get pre-releases when checking for updates"

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: horaceyoung <yonghaoharry@gmail.com>\n"
"Language-Team: zh_CN <jackytsu.vip.qq.com>\n"
"Last-Translator: horaceyoung <paventyang@gmail.com>\n"
"Language-Team: zh_CN <paventyang@gmail.com>\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -1797,7 +1797,7 @@ msgstr "忽略 TLS 证书错误"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:102
msgid "Images"
msgstr ""
msgstr "图片"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.tsx:170
#: packages/app-desktop/gui/MenuBar.tsx:484
@@ -2640,9 +2640,8 @@ msgid "Or create an account."
msgstr "创建帐号。"
#: packages/app-desktop/gui/MenuBar.tsx:352
#, fuzzy
msgid "Other applications..."
msgstr "退出应用。"
msgstr "其他应用..."
#: packages/app-cli/app/command-import.js:27
msgid "Output format: %s"
@@ -3125,14 +3124,12 @@ msgid "Select all"
msgstr "全选"
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:140
#, fuzzy
msgid "Select emoji..."
msgstr "选择日期"
msgstr "选择emoji..."
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:144
#, fuzzy
msgid "Select file..."
msgstr "选"
msgstr "选择文件..."
#: packages/app-cli/app/command-server.js:38
msgid "Server is already running on port %d"

View File

@@ -22,6 +22,7 @@
"dependencies": {
"@joplin/lib": "~2.7",
"@joplin/renderer": "~2.7",
"@types/node-fetch": "1.6.9",
"dayjs": "^1.10.7",
"execa": "^4.1.0",
"fs-extra": "^4.0.3",
@@ -31,7 +32,7 @@
"md5-file": "^4.0.0",
"moment": "^2.24.0",
"mustache": "^2.3.0",
"node-fetch": "^1.7.3",
"node-fetch": "1.7.3",
"relative": "^3.0.2",
"request": "^2.88.0",
"sharp": "^0.25.2",

View File

@@ -41,6 +41,7 @@ yarn install
git reset --hard
yarn run updateMarkdownDoc
yarn run updateNews $DISCOURSE_API_KEY $DISCOURSE_USERNAME
# We commit and push the change. It will be a noop if nothing was actually
# changed

View File

@@ -8,14 +8,21 @@ import { MarkdownAndFrontMatter, stripOffFrontMatter } from './utils/frontMatter
import { dirname, basename } from 'path';
import { readmeFileTitle, replaceGitHubByWebsiteLinks } from './utils/parser';
import { extractOpenGraphTags } from './utils/openGraph';
import { readCredentialFileJson } from '@joplin/lib/utils/credentialFiles';
const moment = require('moment');
interface BuildConfig {
env: Env;
}
const buildConfig = readCredentialFileJson<BuildConfig>('website-build.json', {
env: Env.Prod,
});
const glob = require('glob');
const path = require('path');
const md5File = require('md5-file/promise');
const env = Env.Dev;
const docDir = `${dirname(dirname(dirname(dirname(__dirname))))}/joplin-website/docs`;
if (!pathExistsSync(docDir)) throw new Error(`Doc directory does not exist: ${docDir}`);
@@ -25,7 +32,7 @@ const readmeDir = `${rootDir}/readme`;
const mainTemplateHtml = readFileSync(`${websiteAssetDir}/templates/main-new.mustache`, 'utf8');
const frontTemplateHtml = readFileSync(`${websiteAssetDir}/templates/front.mustache`, 'utf8');
const plansTemplateHtml = readFileSync(`${websiteAssetDir}/templates/plans.mustache`, 'utf8');
const stripeConfig = loadStripeConfig(env, `${rootDir}/packages/server/stripeConfig.json`);
const stripeConfig = loadStripeConfig(buildConfig.env, `${rootDir}/packages/server/stripeConfig.json`);
const partialDir = `${websiteAssetDir}/templates/partials`;
const discussLink = 'https://discourse.joplinapp.org/c/news/9';
@@ -82,7 +89,7 @@ async function getAssetUrls(): Promise<AssetUrls> {
function defaultTemplateParams(assetUrls: AssetUrls): TemplateParams {
return {
env,
env: buildConfig.env,
baseUrl,
imageBaseUrl: `${baseUrl}/images`,
cssBaseUrl,

View File

@@ -0,0 +1,190 @@
// This script reads through the Markdown files in readme/news and post each of
// them as Dicourse forum posts. It then also update the news file with a link
// to that forum post.
import { readdir, readFile, writeFile } from 'fs-extra';
import { basename } from 'path';
import { rootDir } from '../tool-utils';
import fetch from 'node-fetch';
import { compileWithFrontMatter, MarkdownAndFrontMatter, stripOffFrontMatter } from './utils/frontMatter';
interface ApiConfig {
baseUrl: string;
key: string;
username: string;
newsCategoryId: number;
}
interface Post {
id: string;
path: string;
}
interface PostContent {
title: string;
body: string;
parsed: MarkdownAndFrontMatter;
}
enum HttpMethod {
GET = 'GET',
POST = 'POST',
DELETE = 'DELETE',
PUT = 'PUT',
PATCH = 'PATCH',
}
interface ForumTopPost {
id: number;
raw: string;
title: string;
}
const ignoredPostIds = ['20180621-172112','20180621-182112','20180906-101039','20180906-111039','20180916-200431','20180916-210431','20180929-111053','20180929-121053','20181004-081123','20181004-091123','20181101-174335','20181213-173459','20190130-230218','20190404-064157','20190404-074157','20190424-102410','20190424-112410','20190523-221026','20190523-231026','20190610-230711','20190611-000711','20190613-192613','20190613-202613','20190814-215957','20190814-225957','20190924-230254','20190925-000254','20190929-142834','20190929-152834','20191012-223121','20191012-233121','20191014-155136','20191014-165136','20191101-131852','20191117-183855','20191118-072700','20200220-190804','20200301-125055','20200314-001555','20200406-214254','20200406-224254','20200505-181736','20200606-151446','20200607-112720','20200613-103545','20200616-191918','20200620-114515','20200622-084127','20200626-134029','20200708-192444','20200906-172325','20200913-163730','20200915-091108','20201030-114530','20201126-114649','20201130-145937','20201212-172039','20201228-112150','20210104-131645','20210105-153008','20210130-144626','20210309-111950','20210310-100852','20210413-091132','20210430-083248','20210506-083359','20210513-095238','20210518-085514','20210621-104753','20210624-171844','20210705-094247','20210706-140228','20210711-095626','20210718-103538','20210729-103234','20210804-085003','20210831-154354','20210901-113415','20210929-144036','20210930-163458','20211031-115215','20211102-150403','20211217-120324','20220215-142000','20220224-release-2-7','20220308-gsoc2022-start','20220405-gsoc-contributor-proposals'];
const config: ApiConfig = {
baseUrl: 'https://discourse.joplinapp.org',
key: '',
username: '',
newsCategoryId: 9,
};
const getPosts = async (newsDir: string): Promise<Post[]> => {
const filenames = await readdir(newsDir);
const output: Post[] = [];
for (const filename of filenames) {
if (!filename.endsWith('.md')) continue;
output.push({
id: basename(filename, '.md'),
path: `${newsDir}/${filename}`,
});
}
return output;
};
const getPostContent = async (post: Post): Promise<PostContent> => {
const raw = await readFile(post.path, 'utf8');
const parsed = stripOffFrontMatter(raw);
const lines = parsed.doc.split('\n');
const titleLine = lines[0];
if (!titleLine.startsWith('# ')) throw new Error('Cannot extract title from post: no header detected');
lines.splice(0, 1);
return {
title: titleLine.substr(1).trim(),
body: lines.join('\n').trim(),
parsed,
};
};
const execApi = async (method: HttpMethod, path: string, body: Record<string, string | number> = null) => {
const headers: Record<string, string> = {
'Api-Key': config.key,
'Api-Username': config.username,
};
if (method !== HttpMethod.GET) headers['Content-Type'] = 'application/json;';
const response = await fetch(`${config.baseUrl}/${path}`, {
method,
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
const error = new Error(`On ${method} ${path}: ${errorText}`);
let apiObject = null;
try {
apiObject = JSON.parse(errorText);
} catch (error) {
// Ignore - it just means that the error object is a plain string
}
(error as any).apiObject = apiObject;
throw error;
}
return response.json();
};
const getForumTopPostByExternalId = async (externalId: string): Promise<ForumTopPost> => {
try {
const existingForumTopic = await execApi(HttpMethod.GET, `t/external_id/${externalId}.json`);
const existingForumPost = await execApi(HttpMethod.GET, `posts/${existingForumTopic.post_stream.posts[0].id}.json`);
return {
id: existingForumPost.id,
title: existingForumTopic.title,
raw: existingForumPost.raw,
};
} catch (error) {
if (error.apiObject && error.apiObject.error_type === 'not_found') return null;
throw error;
}
};
const main = async () => {
const argv = require('yargs').argv;
config.key = argv._[0];
config.username = argv._[1];
if (!config.key || !config.username) throw new Error('API Key and Username are required');
const posts = await getPosts(`${rootDir}/readme/news`);
for (const post of posts) {
if (ignoredPostIds.includes(post.id)) continue;
console.info(`Processing ${post.path}...`);
try {
const content = await getPostContent(post);
const existingForumPost = await getForumTopPostByExternalId(post.id);
if (existingForumPost) {
// console.info('EXISTING ========================');
// console.info(existingForumPost.title);
// console.info(existingForumPost.raw);
// console.info('NEW ========================');
// console.info(content.title);
// console.info(content.body);
if (existingForumPost.title === content.title && existingForumPost.raw === content.body) {
console.info('Post already exists and has not changed: skipping it...');
} else {
console.info('Post already exists and has changed: updating it...');
await execApi(HttpMethod.PUT, `posts/${existingForumPost.id}.json`, {
title: content.title,
raw: content.body,
edit_reason: 'Auto-updated by script',
});
}
} else {
console.info('Post does not exists: creating it...');
const response = await execApi(HttpMethod.POST, 'posts', {
title: content.title,
raw: content.body,
category: config.newsCategoryId,
external_id: post.id,
});
const postUrl = `https://discourse.joplinapp.org/t/${response.topic_id}`;
content.parsed.forum_url = postUrl;
const compiled = compileWithFrontMatter(content.parsed);
await writeFile(post.path, compiled, 'utf8');
}
} catch (error) {
console.error(error);
}
}
};
main().catch((error) => {
console.error('Fatal error', error);
process.exit(1);
});

View File

@@ -5,6 +5,7 @@ export interface MarkdownAndFrontMatter {
created?: Date;
updated?: Date;
source_url?: string;
forum_url?: string;
}
const readProp = (line: string): string[] => {
@@ -62,3 +63,37 @@ export const stripOffFrontMatter = (md: string): MarkdownAndFrontMatter => {
return output;
};
// ---
// created: 2021-07-05T09:42:47.000+00:00
// source_url: https://www.patreon.com/posts/any-ideas-for-53317699
// ---
const formatFrontMatterValue = (key: string, value: any) => {
if (['created', 'updated'].includes(key)) {
return moment((value as Date)).toISOString();
} else {
return value.toString();
}
};
export const compileWithFrontMatter = (md: MarkdownAndFrontMatter): string => {
const output: string[] = [];
const header: string[] = [];
for (const [key, value] of Object.entries(md)) {
if (key === 'doc') continue;
header.push(`${key}: ${formatFrontMatterValue(key, value)}`);
}
if (header.length) {
output.push('---');
output.push(header.join('\n'));
output.push('---');
output.push('');
}
output.push(md.doc);
return output.join('\n');
};

View File

@@ -13,6 +13,23 @@ Name | Type | Required? | Description
`keywords` | string[] | No | Keywords associated with the plugins. They are used in search in particular.
`homepage_url` | string | No | Homepage URL of the plugin. It can also be, for example, a link to a GitHub repository.
`repository_url` | string | No | Repository URL where the plugin source code is hosted.
`categories` | string[] | No | [Categories](#categories) that describes the functionality of the plugin. |
## Categories
| Category | Description |
| --- | --- |
| appearance | dealing with appearance of some element/s of the app. For example line numbers, layout, etc. |
| developer tools | built for the developers. |
| editor | enhancing note editor. |
| files | dealing with files. For example import, export, backup, etc. |
| integrations | integrating third party services or apps. |
| personal knowledge management | managing and organizing notes. |
| productivity | making Joplin more productive to use. |
| search | enhancing search inside the app. |
| tags |  dealing with note tags. |
| themes | changing theme of the app. |
| viewer | enhancing the rendering of a note. |
## Manifest example

View File

@@ -331,10 +331,14 @@ Creating a new resource is special because you also need to upload the file. Unl
curl -F 'data=@/path/to/file.jpg' -F 'props={"title":"my resource title"}' http://localhost:41184/resources
Or to **update** a resource:
To **update** the resource content, you can make a PUT request with the same arguments:
curl -X PUT -F 'data=@/path/to/file.jpg' -F 'props={"title":"my modified title"}' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696
Or if you only need to update the resource properties (title, etc.), without changing the content, you can make a regular PUT request:
curl -X PUT --data '{"title": "My new title"}' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696
The "data" field is required, while the "props" one is not. If not specified, default values will be used.
**From a plugin** the syntax to create a resource is also a bit special:

View File

@@ -0,0 +1,11 @@
---
forum_url: https://discourse.joplinapp.org/t/24913
---
# GSoC "Contributor Proposals" phase is starting now!
The "Contributor Proposals" phase of GSoC 2022 is starting today! If you would like to be a contributor, now is the time to choose your project idea, write your proposal, and upload it to https://summerofcode.withgoogle.com/
When it's done, please also let us know by posting an update on your forum introduction post.
If you haven't created a pull request yet, it's still time to create one. Doing so will greatly increase your chances of being selected!

View File

@@ -3118,6 +3118,7 @@ __metadata:
"@types/fs-extra": ^9.0.6
"@types/jest": ^26.0.15
"@types/js-yaml": ^4.0.2
"@types/nanoid": ^3.0.0
"@types/node": ^14.14.6
"@types/node-rsa": ^1.1.1
"@types/react": ^17.0.20
@@ -3444,6 +3445,7 @@ __metadata:
"@types/jest": ^26.0.15
"@types/mustache": ^0.8.32
"@types/node": ^14.14.6
"@types/node-fetch": 1.6.9
dayjs: ^1.10.7
execa: ^4.1.0
fs-extra: ^4.0.3
@@ -3456,7 +3458,7 @@ __metadata:
md5-file: ^4.0.0
moment: ^2.24.0
mustache: ^2.3.0
node-fetch: ^1.7.3
node-fetch: 1.7.3
relative: ^3.0.2
request: ^2.88.0
sass: ^1.39.2
@@ -5575,6 +5577,24 @@ __metadata:
languageName: node
linkType: hard
"@types/nanoid@npm:^3.0.0":
version: 3.0.0
resolution: "@types/nanoid@npm:3.0.0"
dependencies:
nanoid: "*"
checksum: 6e84d71ce2b8a2e23b20a018249a2e6a1c36b4ea0f45ff0265e8db514010ccf13141d87d6892f35b8e50ca5832d1d04ddc27062b5e69f91db2d5b3c5321c8c56
languageName: node
linkType: hard
"@types/node-fetch@npm:1.6.9":
version: 1.6.9
resolution: "@types/node-fetch@npm:1.6.9"
dependencies:
"@types/node": "*"
checksum: 9c5306e852275a464ec6106d8ce7f0b7d009b2a62a613ea183601e3c469cc66d6c22fe786d12bdebbdece396df3b53cc387a32b2834634cd5f4b8b2d4fe1136c
languageName: node
linkType: hard
"@types/node-rsa@npm:^1.1.1":
version: 1.1.1
resolution: "@types/node-rsa@npm:1.1.1"
@@ -21809,6 +21829,15 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:*":
version: 3.3.2
resolution: "nanoid@npm:3.3.2"
bin:
nanoid: bin/nanoid.cjs
checksum: 376717f0685251fad77850bd84c6b8d57837c71eeb1c05be7c742140cc1835a5a2953562add05166d6dbc8fb65f3fdffa356213037b967a470e1691dc3e7b9cc
languageName: node
linkType: hard
"nanoid@npm:^2.1.1":
version: 2.1.11
resolution: "nanoid@npm:2.1.11"
@@ -22049,7 +22078,7 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^1.0.1, node-fetch@npm:^1.7.1, node-fetch@npm:^1.7.3":
"node-fetch@npm:1.7.3, node-fetch@npm:^1.0.1, node-fetch@npm:^1.7.1, node-fetch@npm:^1.7.3":
version: 1.7.3
resolution: "node-fetch@npm:1.7.3"
dependencies: