You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-02 00:08:04 +02:00
Compare commits
50 Commits
v2.3.1
...
sync_wizar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a2f08e2c3 | ||
|
|
dc6d2be77e | ||
|
|
2fc9461df0 | ||
|
|
e68dfbfddf | ||
|
|
79c3f0b12b | ||
|
|
17ee2e3395 | ||
|
|
5c50ee9b14 | ||
|
|
0aaf888aa5 | ||
|
|
8c93a89c0a | ||
|
|
bee6001462 | ||
|
|
dc192fb4c1 | ||
|
|
3abebe6a32 | ||
|
|
fd78306111 | ||
|
|
2011369c83 | ||
|
|
2a732ba03d | ||
|
|
bcadb3662b | ||
|
|
7b2c271070 | ||
|
|
9783f57f75 | ||
|
|
e0971baec4 | ||
|
|
c7421dfbe1 | ||
|
|
543413d64b | ||
|
|
2c79ce25fa | ||
|
|
c8ccab808f | ||
|
|
d27f3b6ad3 | ||
|
|
c8dad95c38 | ||
|
|
8646d8240b | ||
|
|
8063c94ff7 | ||
|
|
6ae5ccb638 | ||
|
|
30a23ea118 | ||
|
|
a9961ae3ec | ||
|
|
d42d17e1ed | ||
|
|
60ef24a3f7 | ||
|
|
f518549cfe | ||
|
|
d0ffc03ccc | ||
|
|
32c8cc9030 | ||
|
|
d42d181f31 | ||
|
|
48f23f1ee7 | ||
|
|
58efe1c87f | ||
|
|
1478c368c5 | ||
|
|
feda5afe44 | ||
|
|
c96ec36d0d | ||
|
|
e57e5d3b3f | ||
|
|
6ce19ed680 | ||
|
|
d0251400a4 | ||
|
|
f0ad813f40 | ||
|
|
f07636e4b7 | ||
|
|
11cf8474f7 | ||
|
|
77a8ab8125 | ||
|
|
3309f42fb6 | ||
|
|
bd907ddeb2 |
@@ -7,4 +7,6 @@ packages/app-cli
|
||||
packages/app-mobile
|
||||
packages/app-clipper
|
||||
packages/generator-joplin
|
||||
packages/plugin-repo-cli
|
||||
packages/plugin-repo-cli
|
||||
packages/server/db-*.sqlite
|
||||
packages/server/temp
|
||||
|
||||
@@ -573,6 +573,9 @@ packages/app-desktop/gui/Sidebar/styles/index.js.map
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js.map
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
||||
packages/app-desktop/gui/TagList.d.ts
|
||||
packages/app-desktop/gui/TagList.js
|
||||
packages/app-desktop/gui/TagList.js.map
|
||||
@@ -870,6 +873,9 @@ packages/lib/SyncTargetJoplinServer.js.map
|
||||
packages/lib/SyncTargetOneDrive.d.ts
|
||||
packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetOneDrive.js.map
|
||||
packages/lib/SyncTargetRegistry.d.ts
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/SyncTargetRegistry.js.map
|
||||
packages/lib/Synchronizer.d.ts
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/Synchronizer.js.map
|
||||
@@ -927,6 +933,12 @@ packages/lib/fs-driver-node.js.map
|
||||
packages/lib/fsDriver.test.d.ts
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/fsDriver.test.js.map
|
||||
packages/lib/hooks/useElementSize.d.ts
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useElementSize.js.map
|
||||
packages/lib/hooks/useEventListener.d.ts
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/useEventListener.js.map
|
||||
packages/lib/htmlUtils.d.ts
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils.js.map
|
||||
|
||||
2
.github/scripts/run_ci.sh
vendored
2
.github/scripts/run_ci.sh
vendored
@@ -134,7 +134,7 @@ if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
elif [[ $GIT_TAG_NAME = server-v* ]] && [[ $IS_LINUX = 1 ]]; then
|
||||
echo "Step: Building Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
npm run buildServerDocker -- --tag-name $GIT_TAG_NAME
|
||||
npm run buildServerDocker -- --tag-name $GIT_TAG_NAME --push-images
|
||||
else
|
||||
echo "Step: Building but *not* publishing desktop application..."
|
||||
USE_HARD_LINKS=false npm run dist -- --publish=never
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -558,6 +558,9 @@ packages/app-desktop/gui/Sidebar/styles/index.js.map
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js.map
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
||||
packages/app-desktop/gui/TagList.d.ts
|
||||
packages/app-desktop/gui/TagList.js
|
||||
packages/app-desktop/gui/TagList.js.map
|
||||
@@ -855,6 +858,9 @@ packages/lib/SyncTargetJoplinServer.js.map
|
||||
packages/lib/SyncTargetOneDrive.d.ts
|
||||
packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetOneDrive.js.map
|
||||
packages/lib/SyncTargetRegistry.d.ts
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/SyncTargetRegistry.js.map
|
||||
packages/lib/Synchronizer.d.ts
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/Synchronizer.js.map
|
||||
@@ -912,6 +918,12 @@ packages/lib/fs-driver-node.js.map
|
||||
packages/lib/fsDriver.test.d.ts
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/fsDriver.test.js.map
|
||||
packages/lib/hooks/useElementSize.d.ts
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useElementSize.js.map
|
||||
packages/lib/hooks/useEventListener.d.ts
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/useEventListener.js.map
|
||||
packages/lib/htmlUtils.d.ts
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils.js.map
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# https://versatile.nl/blog/deploying-lerna-web-apps-with-docker
|
||||
|
||||
FROM node:12
|
||||
FROM node:16
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get --yes install vim
|
||||
|
||||
RUN echo "Node: $(node --version)"
|
||||
RUN echo "Npm: $(npm --version)"
|
||||
|
||||
ARG user=joplin
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash $user
|
||||
|
||||
@@ -10,7 +10,7 @@ const { cliUtils } = require('./cli-utils.js');
|
||||
const md5 = require('md5');
|
||||
const locker = require('proper-lockfile');
|
||||
const fs = require('fs-extra');
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry').default;
|
||||
const MigrationHandler = require('@joplin/lib/services/synchronizer/MigrationHandler').default;
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
28810
packages/app-cli/package-lock.json
generated
28810
packages/app-cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.2",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
const os = require('os');
|
||||
const { filename } = require('@joplin/lib/path-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -57,10 +56,8 @@ describe('MdToHtml', function() {
|
||||
const result = await mdToHtml.render(markdown, null, mdToHtmlOptions);
|
||||
let actualHtml = result.html;
|
||||
|
||||
if (os.EOL === '\r\n') {
|
||||
expectedHtml = expectedHtml.replace(/\r\n/g, '\n');
|
||||
actualHtml = actualHtml.replace(/\r\n/g, '\n');
|
||||
}
|
||||
expectedHtml = expectedHtml.replace(/\r?\n/g, '\n');
|
||||
actualHtml = actualHtml.replace(/\r?\n/g, '\n');
|
||||
|
||||
if (actualHtml !== expectedHtml) {
|
||||
console.info('');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="javascript" data-joplin-source-open="```javascript " data-joplin-source-close=" ```">function() {
|
||||
console.info('bonjour');
|
||||
}</pre><pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
|
||||
<span class="hljs-built_in">console</span>.info(<span class="hljs-string">'bonjour'</span>);
|
||||
}</pre><pre class="hljs"><code><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) {
|
||||
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">'bonjour'</span>);
|
||||
}</code></pre></div>
|
||||
|
||||
@@ -114,6 +114,10 @@ interface AppStateRoute {
|
||||
props: any;
|
||||
}
|
||||
|
||||
export interface AppStateDialog {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AppState extends State {
|
||||
route: AppStateRoute;
|
||||
navHistory: any[];
|
||||
@@ -130,6 +134,7 @@ export interface AppState extends State {
|
||||
// Extra reducer keys go here
|
||||
watchedResources: any;
|
||||
mainLayout: LayoutItem;
|
||||
dialogs: AppStateDialog[];
|
||||
}
|
||||
|
||||
const appDefaultState: AppState = {
|
||||
@@ -150,6 +155,7 @@ const appDefaultState: AppState = {
|
||||
layoutMoveMode: false,
|
||||
mainLayout: null,
|
||||
startupPluginsLoaded: false,
|
||||
dialogs: [],
|
||||
...resourceEditWatcherDefaultState,
|
||||
};
|
||||
|
||||
@@ -370,6 +376,30 @@ class Application extends BaseApplication {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DIALOG_OPEN':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const newDialogs = newState.dialogs.slice();
|
||||
|
||||
if (newDialogs.find(d => d.name === action.name)) throw new Error(`This dialog is already opened: ${action.name}`);
|
||||
|
||||
newDialogs.push({
|
||||
name: action.name,
|
||||
});
|
||||
newState.dialogs = newDialogs;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DIALOG_CLOSE':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const newDialogs = newState.dialogs.slice().filter(d => d.name !== action.name);
|
||||
newState.dialogs = newDialogs;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LAYOUT_MOVE_MODE_SET':
|
||||
|
||||
newState = {
|
||||
@@ -834,6 +864,14 @@ class Application extends BaseApplication {
|
||||
// });
|
||||
// }, 5000);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
this.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="43px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 43 40" version="1.1" height="40px">
|
||||
<path d="m12.5 0l-12.5 8.1 8.7 7 12.5-7.8-8.7-7.3zm-12.5 21.9l12.5 8.2 8.7-7.3-12.5-7.7-8.7 6.8zm21.2 0.9l8.8 7.3 12.4-8.1-8.6-6.9-12.6 7.7zm21.2-14.7l-12.4-8.1-8.8 7.3 12.6 7.8 8.6-7zm-21.1 16.3l-8.8 7.3-3.7-2.5v2.8l12.5 7.5 12.5-7.5v-2.8l-3.8 2.5-8.7-7.3z" fill="#007EE5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 441 B |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 682.66669 682.66669" height="682.66669" width="682.66669" xml:space="preserve" id="svg2" version="1.1">
|
||||
<defs id="defs6">
|
||||
<linearGradient id="linearGradient26" spreadMethod="pad" gradientTransform="matrix(-4387.91,4387.91,4387.91,4387.91,4753.95,366.05)" gradientUnits="userSpaceOnUse" y2="0" x2="1" y1="0" x1="0">
|
||||
<stop id="stop22" offset="0" style="stop-opacity:1;stop-color:#004caf"/>
|
||||
<stop id="stop24" offset="1" style="stop-opacity:1;stop-color:#1f95f8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,682.66667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<g clip-path="url(#clipPath20)" id="g16">
|
||||
<path id="path28" style="fill:url(#linearGradient26);fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 3873.89,0 H 1246.11 C 560.754,0 0,560.75 0,1246.11 V 3873.88 C 0,4559.25 560.754,5120 1246.11,5120 H 3873.89 C 4559.25,5120 5120,4559.25 5120,3873.88 V 1246.11 C 5120,560.75 4559.25,0 3873.89,0"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 3961.59,4435.23 H 2570.18 c -13.15,0 -23.78,-10.64 -23.78,-23.77 v -441.84 c 0,-14.87 12.04,-26.92 26.92,-26.92 h 190.77 c 77.16,0 139.73,-59.35 146.43,-134.77 V 3505 3336.23 1728.75 1717.36 h -0.05 c 0.48,-16.84 -0.19,-33.4 -1.83,-49.71 -0.18,-2.38 -0.5,-4.73 -0.79,-7.09 -1.1,-9.53 -2.32,-19.01 -4.17,-28.29 -1.01,-5.29 -2.44,-10.44 -3.71,-15.65 -1.71,-6.93 -3.09,-13.97 -5.22,-20.75 -12.58,-40.27 -32.47,-77.62 -59.98,-110.5 -1.01,-1.17 -2.26,-2.25 -3.26,-3.41 -8.39,-9.72 -17.2,-19.19 -26.95,-28.06 -9.84,-8.95 -20.26,-17.27 -31.21,-25 -77.84,-55.14 -182.61,-79.4 -299.67,-68.2 -149.26,14.03 -297.34,81.72 -417.03,190.62 -119.67,108.89 -194.08,243.62 -209.48,379.41 -13.85,121.48 22.55,228.38 102.42,301.05 0.21,0.16 0.4,0.31 0.56,0.48 3.09,2.77 6.49,5.2 9.67,7.87 57.16,47.89 131.67,76.91 216.7,84.91 0.96,0.09 1.88,0.24 2.79,0.32 8.95,0.79 18.07,1.15 27.27,1.49 4.81,0.16 9.56,0.5 14.44,0.54 1.62,0.02 3.16,0.19 4.78,0.19 2.9,0 5.91,-0.38 8.81,-0.42 13.4,-0.21 26.9,-0.76 40.67,-1.94 1.74,-0.14 3.4,-0.08 5.19,-0.24 1.27,-0.13 2.53,-0.41 3.8,-0.54 78,-7.82 155.23,-31.11 228.52,-66.4 1.53,-0.07 3.3,-0.54 5.51,-1.76 22.34,-12.34 26.62,0.9 27.28,9.65 v 382.24 282.82 c 0,19.05 -13.25,35.9 -31.83,39.99 -394.76,86.88 -782.08,-3.55 -1055.38,-252.34 -238.75,-217.18 -354.24,-530.58 -316.82,-859.79 33.39,-293.23 183.91,-574.94 423.88,-793.33 233.89,-212.79 531.69,-345.86 838.88,-374.801 42.33,-3.918 84.86,-5.938 126.36,-5.938 293.38,0 565.61,100.598 766.54,283.379 190.34,173.3 304.35,411.27 321.08,670.16 l 1.55,1697.91 h 0.17 v 453.97 h 0.06 v 7.92 c 1.72,80.12 67.05,144.58 147.61,144.58 h 190.77 c 14.86,0 26.92,12.05 26.92,26.92 v 441.84 c 0,13.13 -10.63,23.77 -23.78,23.77"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Livello_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1030.04 659.922"
|
||||
enable-background="new 0 0 1030.04 659.922" xml:space="preserve">
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="659.922" width="1030.04" x="-490" y="-344.922"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g id="STYLE_COLOR_1_">
|
||||
<path fill="#0364B8" d="M622.292,445.338l212.613-203.327C790.741,69.804,615.338-33.996,443.13,10.168
|
||||
C365.58,30.056,298.224,78.13,254.209,145.005C257.5,144.922,622.292,445.338,622.292,445.338z"/>
|
||||
<path fill="#0078D4" d="M392.776,183.283l-0.01,0.035c-40.626-25.162-87.479-38.462-135.267-38.397
|
||||
c-1.104,0-2.189,0.07-3.291,0.083C112.064,146.765-1.74,263.423,0.02,405.567c0.638,51.562,16.749,101.743,46.244,144.04
|
||||
l318.528-39.894l244.209-196.915L392.776,183.283z"/>
|
||||
<path fill="#1490DF" d="M834.905,242.012c-4.674-0.312-9.371-0.528-14.123-0.528c-28.523-0.028-56.749,5.798-82.93,17.117
|
||||
l-0.006-0.022l-128.844,54.22l142.041,175.456l253.934,61.728c54.799-101.732,16.752-228.625-84.98-283.424
|
||||
c-26.287-14.16-55.301-22.529-85.091-24.546V242.012z"/>
|
||||
<path fill="#28A8EA" d="M46.264,549.607C94.359,618.756,173.27,659.966,257.5,659.922h563.281
|
||||
c76.946,0.022,147.691-42.202,184.195-109.937L609.001,312.798L46.264,549.607z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -11,7 +11,7 @@ import EncryptionConfigScreen from '../EncryptionConfigScreen';
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const pathUtils = require('@joplin/lib/path-utils');
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
@@ -94,6 +94,11 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData);
|
||||
await Setting.saveAll();
|
||||
bridge().restart();
|
||||
} else if (key === 'sync.openSyncWizard') {
|
||||
this.props.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unhandled key: ${key}`);
|
||||
}
|
||||
@@ -606,11 +611,15 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
const labelComp = md.hideLabel ? null : (
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
{labelComp}
|
||||
<Button level={ButtonLevel.Secondary} title={md.label()} onClick={md.onClick ? md.onClick : () => this.handleSettingButton(key)}/>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
|
||||
@@ -18,10 +18,11 @@ const DialogRoot = styled.div`
|
||||
background-color: ${props => props.theme.backgroundColor};
|
||||
padding: 16px;
|
||||
box-shadow: 6px 6px 20px rgba(0,0,0,0.5);
|
||||
margin-top: 20px;
|
||||
margin: 20px;
|
||||
min-height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
justify-content: ${props => props.justifyContent ? props.justifyContent : 'flex-start'};
|
||||
font-family: ${props => props.theme.fontFamily};
|
||||
font-size: ${props => props.theme.fontSize * 1.5}px;
|
||||
line-height: 1.6em;
|
||||
@@ -12,10 +14,11 @@ const Root = styled.div`
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
justifyContent?: string;
|
||||
}
|
||||
|
||||
export default function DialogTitle(props: Props) {
|
||||
return (
|
||||
<Root>{props.title}</Root>
|
||||
<Root justifyContent={props.justifyContent}>{props.title}</Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import bridge from '../services/bridge';
|
||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/database/types';
|
||||
import { getEncryptionEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { toggleAndSetupEncryption } from '../../lib/services/e2ee/utils';
|
||||
import MasterKey from '../../lib/models/MasterKey';
|
||||
import { toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import MasterKey from '@joplin/lib/models/MasterKey';
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
||||
@@ -33,10 +33,11 @@ const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ErrorBoundary from '../../../ErrorBoundary';
|
||||
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
||||
|
||||
const menuUtils = new MenuUtils(CommandService.instance());
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||
return { ...override };
|
||||
}
|
||||
|
||||
@@ -384,6 +385,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
|
||||
monospaceFonts.push('monospace');
|
||||
|
||||
const maxWidthCss = props.contentMaxWidth ? `
|
||||
margin-right: auto !important;
|
||||
margin-left: auto !important;
|
||||
max-width: ${props.contentMaxWidth}px !important;
|
||||
` : '';
|
||||
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('id', 'codemirrorStyle');
|
||||
document.head.appendChild(element);
|
||||
@@ -418,6 +425,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
/* Add a fixed right padding to account for the appearance (and disappearance) */
|
||||
/* of the sidebar */
|
||||
padding-right: 10px !important;
|
||||
${maxWidthCss}
|
||||
}
|
||||
|
||||
/* This enforces monospace for certain elements (code, tables, etc.) */
|
||||
@@ -467,6 +475,20 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
color: ${theme.codeColor};
|
||||
}
|
||||
|
||||
div.CodeMirror span.cm-comment.cm-jn-inline-code {
|
||||
border: 1px solid ${theme.codeBorderColor};
|
||||
background-color: ${theme.codeBackgroundColor};
|
||||
padding-right: .2em;
|
||||
padding-left: .2em;
|
||||
border-radius: .25em;
|
||||
}
|
||||
|
||||
div.CodeMirror pre.cm-jn-code-block {
|
||||
background-color: ${theme.codeBackgroundColor};
|
||||
padding-right: .2em;
|
||||
padding-left: .2em;
|
||||
}
|
||||
|
||||
div.CodeMirror span.cm-strong {
|
||||
color: ${theme.colorBright};
|
||||
}
|
||||
@@ -533,7 +555,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [props.themeId]);
|
||||
}, [props.themeId, props.contentMaxWidth]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
@@ -572,7 +594,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
|
||||
}
|
||||
|
||||
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
||||
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({
|
||||
resourceInfos: props.resourceInfos,
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
}));
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
@@ -795,6 +820,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
viewerStyle={styles.viewer}
|
||||
onIpcMessage={webview_ipcMessage}
|
||||
onDomReady={webview_domReady}
|
||||
contentMaxWidth={props.contentMaxWidth}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ interface JoplinModeState {
|
||||
outer: any;
|
||||
openCharacter: string;
|
||||
inTable: boolean;
|
||||
inCodeBlock: boolean;
|
||||
inner: any;
|
||||
}
|
||||
|
||||
@@ -48,6 +49,7 @@ export default function useJoplinMode(CodeMirror: any) {
|
||||
outer: CodeMirror.startState(markdownMode),
|
||||
openCharacter: '',
|
||||
inTable: false,
|
||||
inCodeBlock: false,
|
||||
inner: CodeMirror.startState(stex),
|
||||
};
|
||||
},
|
||||
@@ -57,6 +59,7 @@ export default function useJoplinMode(CodeMirror: any) {
|
||||
outer: CodeMirror.copyState(markdownMode, state.outer),
|
||||
openCharacter: state.openCharacter,
|
||||
inTable: state.inTable,
|
||||
inCodeBlock: state.inCodeBlock,
|
||||
inner: CodeMirror.copyState(stex, state.inner),
|
||||
};
|
||||
},
|
||||
@@ -115,9 +118,26 @@ export default function useJoplinMode(CodeMirror: any) {
|
||||
let isMonospace = false;
|
||||
// After being passed to the markdown mode we can check if the
|
||||
// code state variables are set
|
||||
// Code Block
|
||||
if (state.outer.code || (state.outer.thisLine && state.outer.thisLine.fencedCodeEnd)) {
|
||||
// Code
|
||||
if (state.outer.code > 0) {
|
||||
// state.outer.code holds the number of preceding backticks
|
||||
// anything > 0 backticks is an inline-code-block
|
||||
// -1 is used for actual code blocks
|
||||
isMonospace = true;
|
||||
token = `${token} jn-inline-code`;
|
||||
} else if (state.outer.thisLine && state.outer.thisLine.fencedCodeEnd) {
|
||||
state.inCodeBlock = false;
|
||||
isMonospace = true;
|
||||
token = `${token} line-cm-jn-code-block`;
|
||||
} else if (state.outer.code === -1 || state.inCodeBlock) {
|
||||
state.inCodeBlock = true;
|
||||
isMonospace = true;
|
||||
token = `${token} line-cm-jn-code-block`;
|
||||
} else if (stream.pos > 0 && stream.string[stream.pos - 1] === '`' &&
|
||||
!!token && token.includes('comment')) {
|
||||
// This grabs the closing backtick for inline Code
|
||||
isMonospace = true;
|
||||
token = `${token} jn-inline-code`;
|
||||
}
|
||||
// Indented Code
|
||||
if (state.outer.indentedCode) {
|
||||
@@ -163,6 +183,10 @@ export default function useJoplinMode(CodeMirror: any) {
|
||||
}
|
||||
|
||||
state.inTable = false;
|
||||
|
||||
if (state.inCodeBlock) return 'line-cm-jn-code-block';
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
electricChars: markdownMode.electricChars,
|
||||
|
||||
@@ -14,18 +14,18 @@ import { _, closestSupportedLocale } from '@joplin/lib/locale';
|
||||
import useContextMenu from './utils/useContextMenu';
|
||||
import { copyHtmlToClipboard } from '../../utils/clipboardUtils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import setupToolbarButtons from './utils/setupToolbarButtons';
|
||||
import { plainTextToHtml } from '@joplin/lib/htmlUtils';
|
||||
import openEditDialog from './utils/openEditDialog';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
const { clipboard } = require('electron');
|
||||
const supportedLocales = require('./supportedLocales');
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||
return {
|
||||
plugins: {
|
||||
checkbox: {
|
||||
@@ -148,8 +148,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
if (!resourceMd) return;
|
||||
const result = await props.markupToHtml(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMd, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
// editor.fire('joplinChange');
|
||||
// dispatchDidUpdate(editor);
|
||||
}, [props.markupToHtml, editor]);
|
||||
|
||||
const insertResourcesIntoContentRef = useRef(null);
|
||||
|
||||
@@ -159,8 +159,8 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
customCss: props.customCss,
|
||||
});
|
||||
|
||||
return markupToHtml.allAssets(markupLanguage, theme);
|
||||
}, [props.themeId, props.customCss]);
|
||||
return markupToHtml.allAssets(markupLanguage, theme, { contentMaxWidth: props.contentMaxWidth });
|
||||
}, [props.themeId, props.customCss, props.contentMaxWidth]);
|
||||
|
||||
const handleProvisionalFlag = useCallback(() => {
|
||||
if (props.isProvisional) {
|
||||
@@ -400,6 +400,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
noteToolbarButtonInfos: props.toolbarButtonInfos,
|
||||
plugins: props.plugins,
|
||||
fontSize: Setting.value('style.editor.fontSize'),
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
};
|
||||
|
||||
let editor = null;
|
||||
@@ -601,6 +602,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'setTags',
|
||||
], whenClauseContext)[0],
|
||||
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUt
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
|
||||
export interface ToolbarButtonInfos {
|
||||
[key: string]: ToolbarButtonInfo;
|
||||
@@ -37,6 +38,7 @@ export interface NoteEditorProps {
|
||||
toolbarButtonInfos: ToolbarButtonInfo[];
|
||||
setTagsToolbarButtonInfo: ToolbarButtonInfo;
|
||||
richTextBannerDismissed: boolean;
|
||||
contentMaxWidth: number;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorProps {
|
||||
@@ -51,7 +53,7 @@ export interface NoteBodyEditorProps {
|
||||
onWillChange(event: any): void;
|
||||
onMessage(event: any): void;
|
||||
onScroll(event: any): void;
|
||||
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: any)=> Promise<RenderResult>;
|
||||
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
htmlToMarkdown: Function;
|
||||
allAssets: (markupLanguage: MarkupLanguage)=> Promise<RenderResultPluginAsset[]>;
|
||||
disabled: boolean;
|
||||
@@ -67,6 +69,7 @@ export interface NoteBodyEditorProps {
|
||||
noteToolbarButtonInfos: ToolbarButtonInfo[];
|
||||
plugins: PluginStates;
|
||||
fontSize: number;
|
||||
contentMaxWidth: number;
|
||||
}
|
||||
|
||||
export interface FormNote {
|
||||
|
||||
@@ -13,9 +13,12 @@ interface HookDependencies {
|
||||
plugins: PluginStates;
|
||||
}
|
||||
|
||||
interface MarkupToHtmlOptions {
|
||||
export interface MarkupToHtmlOptions {
|
||||
replaceResourceInternalToExternalLinks?: boolean;
|
||||
resourceInfos?: ResourceInfos;
|
||||
contentMaxWidth?: number;
|
||||
plugins?: Record<string, any>;
|
||||
bodyOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import app from '../app';
|
||||
import app, { AppState, AppStateDialog } from '../app';
|
||||
import MainScreen from './MainScreen/MainScreen';
|
||||
import ConfigScreen from './ConfigScreen/ConfigScreen';
|
||||
import StatusScreen from './StatusScreen/StatusScreen';
|
||||
@@ -10,7 +10,6 @@ import { Size } from './ResizableLayout/utils/types';
|
||||
import MenuBar from './MenuBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const React = require('react');
|
||||
|
||||
const { render } = require('react-dom');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -19,6 +18,7 @@ import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
import DialogTitle from './DialogTitle';
|
||||
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
|
||||
import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
@@ -33,6 +33,7 @@ interface Props {
|
||||
size: Size;
|
||||
zoomFactor: number;
|
||||
needApiAuth: boolean;
|
||||
dialogs: AppStateDialog;
|
||||
}
|
||||
|
||||
interface ModalDialogProps {
|
||||
@@ -42,6 +43,24 @@ interface ModalDialogProps {
|
||||
onClick: ClickEventHandler;
|
||||
}
|
||||
|
||||
interface RegisteredDialogProps {
|
||||
themeId: number;
|
||||
key: string;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
interface RegisteredDialog {
|
||||
render: (props: RegisteredDialogProps)=> any;
|
||||
}
|
||||
|
||||
const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
syncWizard: {
|
||||
render: (props: RegisteredDialogProps) => {
|
||||
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId}/>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -151,6 +170,22 @@ class RootComponent extends React.Component<Props, any> {
|
||||
};
|
||||
}
|
||||
|
||||
private renderDialogs() {
|
||||
if (!this.props.dialogs.length) return null;
|
||||
|
||||
const output: any[] = [];
|
||||
for (const dialog of this.props.dialogs) {
|
||||
const md = registeredDialogs[dialog.name];
|
||||
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
|
||||
output.push(md.render({
|
||||
key: dialog.name,
|
||||
themeId: this.props.themeId,
|
||||
dispatch: this.props.dispatch,
|
||||
}));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const navigatorStyle = {
|
||||
width: this.props.size.width / this.props.zoomFactor,
|
||||
@@ -176,19 +211,21 @@ class RootComponent extends React.Component<Props, any> {
|
||||
<GlobalStyle/>
|
||||
<Navigator style={navigatorStyle} screens={screens} />
|
||||
{this.renderModalMessage(this.modalDialogProps())}
|
||||
{this.renderDialogs()}
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
size: state.windowContentSize,
|
||||
zoomFactor: state.settings.windowContentZoomFactor / 100,
|
||||
appState: state.appState,
|
||||
themeId: state.settings.theme,
|
||||
needApiAuth: state.needApiAuth,
|
||||
dialogs: state.dialogs,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
343
packages/app-desktop/gui/SyncWizard/Dialog.tsx
Normal file
343
packages/app-desktop/gui/SyncWizard/Dialog.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DialogButtonRow from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
import styled from 'styled-components';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import SyncTargetRegistry, { SyncTargetInfo } from '@joplin/lib/SyncTargetRegistry';
|
||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import bridge from '../../services/bridge';
|
||||
import StyledInput from '../style/StyledInput';
|
||||
import Setting from '../../../lib/models/Setting';
|
||||
import SyncTargetJoplinCloud from '../../../lib/SyncTargetJoplinCloud';
|
||||
import StyledLink from '../style/StyledLink';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
min-width: 500px;
|
||||
max-width: 1200px;
|
||||
`;
|
||||
|
||||
const SyncTargetDescription = styled.div`
|
||||
${props => props.height ? `height: ${props.height}px` : ''};
|
||||
margin-bottom: 1.3em;
|
||||
line-height: ${props => props.theme.lineHeight};
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const CreateAccountLink = styled(StyledLink)`
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const ContentRoot = styled.div`
|
||||
background-color: ${props => props.theme.backgroundColor3};
|
||||
padding: 1em;
|
||||
padding-right: 0;
|
||||
`;
|
||||
|
||||
const SelfHostingMessage = styled.div`
|
||||
color: ${props => props.theme.color};
|
||||
padding-right: 1em;
|
||||
font-style: italic;
|
||||
margin-top: 1em;
|
||||
opacity: 0.6;
|
||||
`;
|
||||
|
||||
const SyncTargetBoxes = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const SyncTargetTitle = styled.p`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: bold;
|
||||
font-size: 1.7em;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const SyncTargetLogo = styled.img`
|
||||
height: 1.3em;
|
||||
margin-right: 0.4em;
|
||||
`;
|
||||
|
||||
const SyncTargetBox = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
font-family: ${props => props.theme.fontFamily};
|
||||
color: ${props => props.theme.color};
|
||||
background-color: ${props => props.theme.backgroundColor};
|
||||
border: 1px solid ${props => props.theme.dividerColor};
|
||||
border-radius: 8px;
|
||||
padding: 0.8em 2.2em 2em 2.2em;
|
||||
margin-right: 1em;
|
||||
max-width: 400px;
|
||||
opacity: ${props => props.faded ? 0.5 : 1};
|
||||
`;
|
||||
|
||||
const FeatureList = styled.div`
|
||||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
const FeatureIcon = styled.i`
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.color4};
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
const FeatureLine = styled.div`
|
||||
margin-bottom: .5em;
|
||||
opacity: ${props => props.enabled ? 1 : 0.5};
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const FeatureLabel = styled.div`
|
||||
margin-left: 24px;
|
||||
line-height: ${props => props.theme.lineHeight};
|
||||
`;
|
||||
|
||||
const SelectButton = styled(Button)`
|
||||
padding: 10px 10px;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
max-height: fit-content;
|
||||
font-size: 1em;
|
||||
`;
|
||||
|
||||
const JoplinCloudLoginForm = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const FormLabel = styled.label`
|
||||
font-weight: bold;
|
||||
margin: 1em 0 0.6em 0;
|
||||
`;
|
||||
|
||||
const syncTargetNames: string[] = [
|
||||
'joplinCloud',
|
||||
'dropbox',
|
||||
'onedrive',
|
||||
'nextcloud',
|
||||
'webdav',
|
||||
'amazon_s3',
|
||||
'joplinServer',
|
||||
];
|
||||
|
||||
|
||||
const logosImageNames: Record<string, string> = {
|
||||
'dropbox': 'Dropbox.svg',
|
||||
'joplinCloud': 'JoplinCloud.svg',
|
||||
'onedrive': 'OneDrive.svg',
|
||||
};
|
||||
|
||||
export default function(props: Props) {
|
||||
const [showJoplinCloudForm, setShowJoplinCloudForm] = useState(false);
|
||||
const joplinCloudDescriptionRef = useRef(null);
|
||||
const [joplinCloudEmail, setJoplinCloudEmail] = useState('');
|
||||
const [joplinCloudPassword, setJoplinCloudPassword] = useState('');
|
||||
const [joplinCloudLoginInProgress, setJoplinCloudLoginInProgress] = useState(false);
|
||||
|
||||
function closeDialog(dispatch: Function) {
|
||||
dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
}
|
||||
|
||||
const onButtonRowClick = useCallback(() => {
|
||||
closeDialog(props.dispatch);
|
||||
}, [props.dispatch]);
|
||||
|
||||
const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef);
|
||||
|
||||
function renderFeature(enabled: boolean, label: string) {
|
||||
const className = enabled ? 'fas fa-check' : 'fas fa-times';
|
||||
return (
|
||||
<FeatureLine enabled={enabled} key={label}><FeatureIcon className={className}></FeatureIcon> <FeatureLabel>{label}</FeatureLabel></FeatureLine>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFeatures(name: string) {
|
||||
return (
|
||||
<FeatureList>
|
||||
{[
|
||||
renderFeature(true, _('Sync your notes')),
|
||||
renderFeature(name === 'joplinCloud', _('Publish notes to the internet')),
|
||||
renderFeature(name === 'joplinCloud', _('Collaborate on notebooks with others')),
|
||||
]}
|
||||
</FeatureList>
|
||||
);
|
||||
}
|
||||
|
||||
const onJoplinCloudEmailChange = useCallback((event: any) => {
|
||||
setJoplinCloudEmail(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onJoplinCloudPasswordChange = useCallback((event: any) => {
|
||||
setJoplinCloudPassword(event.target.value);
|
||||
}, []);
|
||||
|
||||
const onJoplinCloudLoginClick = useCallback(async () => {
|
||||
setJoplinCloudLoginInProgress(true);
|
||||
|
||||
try {
|
||||
const result = await SyncTargetJoplinCloud.checkConfig({
|
||||
password: () => joplinCloudPassword,
|
||||
path: () => Setting.value('sync.10.path'),
|
||||
userContentPath: () => Setting.value('sync.10.userContentPath'),
|
||||
username: () => joplinCloudEmail,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
Setting.setValue('sync.target', 10);
|
||||
Setting.setValue('sync.10.username', joplinCloudEmail);
|
||||
Setting.setValue('sync.10.password', joplinCloudPassword);
|
||||
await Setting.saveAll();
|
||||
|
||||
alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.'));
|
||||
|
||||
closeDialog(props.dispatch);
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
});
|
||||
} else {
|
||||
alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage));
|
||||
}
|
||||
} finally {
|
||||
setJoplinCloudLoginInProgress(false);
|
||||
}
|
||||
}, [joplinCloudEmail, joplinCloudPassword, props.dispatch]);
|
||||
|
||||
const onJoplinCloudCreateAccountClick = useCallback(() => {
|
||||
bridge().openExternal('https://joplinapp.org/plans/');
|
||||
}, []);
|
||||
|
||||
function renderJoplinCloudLoginForm() {
|
||||
return (
|
||||
<JoplinCloudLoginForm>
|
||||
<div>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<StyledInput type="email" onChange={onJoplinCloudEmailChange}/>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<StyledInput type="password" onChange={onJoplinCloudPasswordChange}/>
|
||||
<SelectButton mt="1.3em" disabled={joplinCloudLoginInProgress} level={ButtonLevel.Primary} title={_('Login')} onClick={onJoplinCloudLoginClick}/>
|
||||
</JoplinCloudLoginForm>
|
||||
);
|
||||
}
|
||||
|
||||
const onSelectButtonClick = useCallback(async (name: string) => {
|
||||
if (name === 'joplinCloud') {
|
||||
setShowJoplinCloudForm(true);
|
||||
} else {
|
||||
Setting.setValue('sync.target', name === 'dropbox' ? 7 : 3);
|
||||
await Setting.saveAll();
|
||||
closeDialog(props.dispatch);
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: name === 'dropbox' ? 'DropboxLogin' : 'OneDriveLogin',
|
||||
});
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
|
||||
function renderSelectArea(info: SyncTargetInfo) {
|
||||
if (info.name === 'joplinCloud' && showJoplinCloudForm) {
|
||||
return renderJoplinCloudLoginForm();
|
||||
} else {
|
||||
return (
|
||||
<SelectButton
|
||||
level={ButtonLevel.Primary}
|
||||
title={_('Select')}
|
||||
onClick={() => onSelectButtonClick(info.name)}
|
||||
disabled={joplinCloudLoginInProgress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSyncTarget(info: SyncTargetInfo) {
|
||||
const key = `syncTarget_${info.name}`;
|
||||
const height = info.name !== 'joplinCloud' ? descriptionHeight : null;
|
||||
|
||||
const logoImageName = logosImageNames[info.name];
|
||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/syncTargetLogos/${logoImageName}` : '';
|
||||
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
|
||||
const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>;
|
||||
const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name);
|
||||
|
||||
return (
|
||||
<SyncTargetBox id={key} key={key} faded={showJoplinCloudForm && info.name !== 'joplinCloud'}>
|
||||
<SyncTargetTitle>{logo}{info.label}</SyncTargetTitle>
|
||||
{descriptionComp}
|
||||
{featuresComp}
|
||||
{renderSelectArea(info)}
|
||||
</SyncTargetBox>
|
||||
);
|
||||
}
|
||||
|
||||
const onSelfHostingClick = useCallback(() => {
|
||||
closeDialog(props.dispatch);
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Config',
|
||||
props: {
|
||||
defaultSection: 'sync',
|
||||
},
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
function renderContent() {
|
||||
const boxes: any[] = [];
|
||||
|
||||
for (const name of syncTargetNames) {
|
||||
const info = SyncTargetRegistry.infoByName(name);
|
||||
if (info.supportsSelfHosted) continue;
|
||||
boxes.push(renderSyncTarget(info));
|
||||
}
|
||||
|
||||
const selfHostingMessage = showJoplinCloudForm ? null : <SelfHostingMessage>Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. <a href="#" onClick={onSelfHostingClick}>Click here to select one</a>.</SelfHostingMessage>;
|
||||
|
||||
return (
|
||||
<ContentRoot>
|
||||
<SyncTargetBoxes>
|
||||
{boxes}
|
||||
</SyncTargetBoxes>
|
||||
{selfHostingMessage}
|
||||
</ContentRoot>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDialogWrapper() {
|
||||
return (
|
||||
<StyledRoot>
|
||||
<DialogTitle title={_('Joplin can synchronise your notes using various providers. Select one from the list below.')} justifyContent="center"/>
|
||||
{renderContent()}
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
onClick={onButtonRowClick}
|
||||
okButtonShow={false}
|
||||
cancelButtonLabel={_('Close')}
|
||||
/>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog renderContent={renderDialogWrapper}/>
|
||||
);
|
||||
}
|
||||
726
packages/app-desktop/package-lock.json
generated
726
packages/app-desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.3",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -141,10 +141,10 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097644
|
||||
versionName "2.3.0"
|
||||
versionCode 2097648
|
||||
versionName "2.3.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
// https://github.com/react-native-community/react-native-camera/issues/2138
|
||||
@@ -158,7 +158,7 @@ android {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86"
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
|
||||
@@ -19,7 +19,7 @@ const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { Dropdown } = require('../Dropdown.js');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const RNFS = require('react-native-fs');
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MasterKeyEntity } from '@joplin/lib/services/database/types';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import MasterKey from '../../../lib/models/MasterKey';
|
||||
import MasterKey from '@joplin/lib/models/MasterKey';
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
||||
@@ -486,13 +486,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.3.0;
|
||||
MARKETING_VERSION = 12.3.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -514,12 +514,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.3.0;
|
||||
MARKETING_VERSION = 12.3.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -659,14 +659,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.3.0;
|
||||
MARKETING_VERSION = 12.3.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -690,14 +690,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.3.0;
|
||||
MARKETING_VERSION = 12.3.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
30897
packages/app-mobile/package-lock.json
generated
30897
packages/app-mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -78,7 +78,7 @@ import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
|
||||
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
|
||||
const { themeStyle } = require('./components/global-style.js');
|
||||
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry.js');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const SyncTargetFilesystem = require('@joplin/lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
|
||||
|
||||
22719
packages/fork-htmlparser2/package-lock.json
generated
22719
packages/fork-htmlparser2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"description": "Fast & forgiving HTML/XML/RSS parser",
|
||||
"version": "4.1.32",
|
||||
"version": "4.1.33",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
16765
packages/fork-sax/package-lock.json
generated
16765
packages/fork-sax/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/fork-sax",
|
||||
"description": "An evented streaming XML parser in JavaScript",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||
"version": "1.2.36",
|
||||
"version": "1.2.37",
|
||||
"main": "lib/sax.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
26377
packages/generator-joplin/package-lock.json
generated
26377
packages/generator-joplin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ const fs = require('fs-extra');
|
||||
import JoplinError from './JoplinError';
|
||||
const EventEmitter = require('events');
|
||||
const syswidecas = require('./vendor/syswide-cas');
|
||||
const SyncTargetRegistry = require('./SyncTargetRegistry.js');
|
||||
import SyncTargetRegistry from './SyncTargetRegistry';
|
||||
const SyncTargetFilesystem = require('./SyncTargetFilesystem.js');
|
||||
const SyncTargetNextcloud = require('./SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('./SyncTargetWebDAV.js');
|
||||
|
||||
@@ -25,6 +25,14 @@ export default class BaseSyncTarget {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static description(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public static supportsSelfHosted(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public option(name: string, defaultValue: any = null) {
|
||||
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
|
||||
return `${_('AWS S3')} (Beta)`;
|
||||
}
|
||||
|
||||
static description() {
|
||||
return 'A service offered by Amazon Web Services (AWS) that provides object storage through a web service interface.';
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ class SyncTargetDropbox extends BaseSyncTarget {
|
||||
return _('Dropbox');
|
||||
}
|
||||
|
||||
static description() {
|
||||
return 'A file hosting service that offers cloud storage and file synchronization';
|
||||
}
|
||||
|
||||
static supportsSelfHosted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
authRouteName() {
|
||||
return 'DropboxLogin';
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
||||
return _('Joplin Cloud');
|
||||
}
|
||||
|
||||
public static description() {
|
||||
return _('Joplin\'s own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.');
|
||||
}
|
||||
|
||||
public static supportsSelfHosted(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
|
||||
return 'joplinServer';
|
||||
}
|
||||
|
||||
public static description() {
|
||||
return 'Besides synchronisation and improved performances, Joplin Server also gives access to Joplin-specific sharing features.';
|
||||
}
|
||||
|
||||
public static label() {
|
||||
return `${_('Joplin Server')} (Beta)`;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
return _('Nextcloud');
|
||||
}
|
||||
|
||||
static description() {
|
||||
return 'A suite of client-server software for creating and using file hosting services.';
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
return _('OneDrive');
|
||||
}
|
||||
|
||||
public static description() {
|
||||
return 'A file hosting service operated by Microsoft as part of its web version of Office.';
|
||||
}
|
||||
|
||||
public static supportsSelfHosted(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return !!this.api().auth();
|
||||
}
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
class SyncTargetRegistry {
|
||||
static classById(syncTargetId) {
|
||||
export interface SyncTargetInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
label: string;
|
||||
supportsSelfHosted: boolean;
|
||||
supportsConfigCheck: boolean;
|
||||
description: string;
|
||||
classRef: any;
|
||||
}
|
||||
|
||||
export default class SyncTargetRegistry {
|
||||
|
||||
private static reg_: Record<number, SyncTargetInfo> = {};
|
||||
|
||||
public static classById(syncTargetId: number) {
|
||||
const info = SyncTargetRegistry.reg_[syncTargetId];
|
||||
if (!info) throw new Error(`Invalid id: ${syncTargetId}`);
|
||||
return info.classRef;
|
||||
}
|
||||
|
||||
static addClass(SyncTargetClass) {
|
||||
public static infoByName(name: string): SyncTargetInfo {
|
||||
for (const [, info] of Object.entries(this.reg_)) {
|
||||
if (info.name === name) return info;
|
||||
}
|
||||
throw new Error(`Unknown name: ${name}`);
|
||||
}
|
||||
|
||||
public static addClass(SyncTargetClass: any) {
|
||||
this.reg_[SyncTargetClass.id()] = {
|
||||
id: SyncTargetClass.id(),
|
||||
name: SyncTargetClass.targetName(),
|
||||
label: SyncTargetClass.label(),
|
||||
classRef: SyncTargetClass,
|
||||
description: SyncTargetClass.description(),
|
||||
supportsSelfHosted: SyncTargetClass.supportsSelfHosted(),
|
||||
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
|
||||
};
|
||||
}
|
||||
|
||||
static allIds() {
|
||||
public static allIds() {
|
||||
return Object.keys(this.reg_);
|
||||
}
|
||||
|
||||
static nameToId(name) {
|
||||
public static nameToId(name: string) {
|
||||
for (const n in this.reg_) {
|
||||
if (!this.reg_.hasOwnProperty(n)) continue;
|
||||
if (this.reg_[n].name === name) return this.reg_[n].id;
|
||||
@@ -27,7 +49,7 @@ class SyncTargetRegistry {
|
||||
throw new Error(`Name not found: ${name}. Was the sync target registered?`);
|
||||
}
|
||||
|
||||
static idToMetadata(id) {
|
||||
public static idToMetadata(id: number) {
|
||||
for (const n in this.reg_) {
|
||||
if (!this.reg_.hasOwnProperty(n)) continue;
|
||||
if (this.reg_[n].id === id) return this.reg_[n];
|
||||
@@ -35,12 +57,12 @@ class SyncTargetRegistry {
|
||||
throw new Error(`ID not found: ${id}`);
|
||||
}
|
||||
|
||||
static idToName(id) {
|
||||
public static idToName(id: number) {
|
||||
return this.idToMetadata(id).name;
|
||||
}
|
||||
|
||||
static idAndLabelPlainObject(os) {
|
||||
const output = {};
|
||||
public static idAndLabelPlainObject(os: string) {
|
||||
const output: Record<string, string> = {};
|
||||
for (const n in this.reg_) {
|
||||
if (!this.reg_.hasOwnProperty(n)) continue;
|
||||
const info = this.reg_[n];
|
||||
@@ -52,7 +74,3 @@ class SyncTargetRegistry {
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTargetRegistry.reg_ = {};
|
||||
|
||||
module.exports = SyncTargetRegistry;
|
||||
@@ -23,6 +23,10 @@ class SyncTargetWebDAV extends BaseSyncTarget {
|
||||
return _('WebDAV');
|
||||
}
|
||||
|
||||
static description() {
|
||||
return 'The WebDAV protocol allows users to create, change and move documents on a server. There are many WebDAV compatible servers, including SeaFile, Nginx or Apache.';
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Setting = require('../../models/Setting').default;
|
||||
const SyncTargetRegistry = require('../../SyncTargetRegistry');
|
||||
const SyncTargetRegistry = require('../../SyncTargetRegistry').default;
|
||||
const ObjectUtils = require('../../ObjectUtils');
|
||||
const { _ } = require('../../locale');
|
||||
const { createSelector } = require('reselect');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const shim = require('../../shim').default;
|
||||
const SyncTargetRegistry = require('../../SyncTargetRegistry');
|
||||
const SyncTargetRegistry = require('../../SyncTargetRegistry').default;
|
||||
const { reg } = require('../../registry.js');
|
||||
const { _ } = require('../../locale');
|
||||
const Setting = require('../../models/Setting').default;
|
||||
|
||||
38
packages/lib/hooks/useElementSize.ts
Normal file
38
packages/lib/hooks/useElementSize.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import shim from '../shim';
|
||||
const { useCallback, useEffect, useState } = shim.react();
|
||||
import useEventListener from './useEventListener';
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function useElementSize(elementRef: any): Size {
|
||||
const [size, setSize] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
// Prevent too many rendering using useCallback
|
||||
const updateSize = useCallback(() => {
|
||||
const node = elementRef?.current;
|
||||
if (node) {
|
||||
setSize({
|
||||
width: node.offsetWidth || 0,
|
||||
height: node.offsetHeight || 0,
|
||||
});
|
||||
}
|
||||
}, [elementRef]);
|
||||
|
||||
// Initial size on mount
|
||||
useEffect(() => {
|
||||
updateSize();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEventListener('resize', updateSize);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export default useElementSize;
|
||||
41
packages/lib/hooks/useEventListener.ts
Normal file
41
packages/lib/hooks/useEventListener.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import shim from '../shim';
|
||||
const { useEffect, useRef } = shim.react();
|
||||
|
||||
function useEventListener(
|
||||
eventName: any,
|
||||
handler: any,
|
||||
element?: any
|
||||
) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
// Define the listening target
|
||||
const targetElement = element?.current || window;
|
||||
if (!(targetElement && targetElement.addEventListener)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update saved handler if necessary
|
||||
if (savedHandler.current !== handler) {
|
||||
savedHandler.current = handler;
|
||||
}
|
||||
|
||||
// Create event listener that calls handler function stored in ref
|
||||
const eventListener = (event: Event) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!savedHandler?.current) {
|
||||
savedHandler.current(event);
|
||||
}
|
||||
};
|
||||
|
||||
targetElement.addEventListener(eventName, eventListener);
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
targetElement.removeEventListener(eventName, eventListener);
|
||||
};
|
||||
}, [eventName, element, handler]);
|
||||
}
|
||||
|
||||
export default useEventListener;
|
||||
@@ -3,7 +3,7 @@ import { _, supportedLocalesToLanguages, defaultLocale } from '../locale';
|
||||
import eventManager from '../eventManager';
|
||||
import BaseModel from '../BaseModel';
|
||||
import Database from '../database';
|
||||
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
|
||||
import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||
import time from '../time';
|
||||
import FileHandler, { SettingValues } from './settings/FileHandler';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
@@ -55,6 +55,7 @@ export interface SettingItem {
|
||||
needRestart?: boolean;
|
||||
autoSave?: boolean;
|
||||
storage?: SettingStorage;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
interface SettingItems {
|
||||
@@ -306,6 +307,19 @@ class Setting extends BaseModel {
|
||||
appTypes: [AppType.Desktop],
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'sync.openSyncWizard': {
|
||||
value: null,
|
||||
type: SettingItemType.Button,
|
||||
public: true,
|
||||
appTypes: [AppType.Desktop],
|
||||
label: () => _('Open Sync Wizard...'),
|
||||
hideLabel: true,
|
||||
section: 'sync',
|
||||
// advanced: true,
|
||||
// description: () => 'If the data on the sync target is incorrect or empty, you can use this button to force a re-upload of your data to the sync target. Application will have to be restarted',
|
||||
},
|
||||
|
||||
'sync.target': {
|
||||
value: SyncTargetRegistry.nameToId('dropbox'),
|
||||
type: SettingItemType.Int,
|
||||
@@ -972,6 +986,8 @@ class Setting extends BaseModel {
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'style.editor.contentMaxWidth': { value: 600, 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.') },
|
||||
|
||||
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
|
||||
|
||||
// TODO: Is there a better way to do this? The goal here is to simply have
|
||||
|
||||
2
packages/lib/package-lock.json
generated
2
packages/lib/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
@@ -25,11 +25,11 @@
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.32",
|
||||
"@joplin/fork-sax": "^1.2.36",
|
||||
"@joplin/renderer": "~2.3",
|
||||
"@joplin/turndown": "^4.0.54",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.36",
|
||||
"@joplin/fork-htmlparser2": "^4.1.33",
|
||||
"@joplin/fork-sax": "^1.2.37",
|
||||
"@joplin/renderer": "^2.3.1",
|
||||
"@joplin/turndown": "^4.0.55",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.37",
|
||||
"async-mutex": "^0.1.3",
|
||||
"aws-sdk": "^2.588.0",
|
||||
"base-64": "^0.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Logger from './Logger';
|
||||
import Setting from './models/Setting';
|
||||
import shim from './shim';
|
||||
const SyncTargetRegistry = require('./SyncTargetRegistry.js');
|
||||
import SyncTargetRegistry from './SyncTargetRegistry';
|
||||
|
||||
class Registry {
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SqlQuery } from '../../database';
|
||||
import JoplinDatabase from '../../JoplinDatabase';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import Setting from '../../models/Setting';
|
||||
const SyncTargetRegistry = require('../../SyncTargetRegistry');
|
||||
import SyncTargetRegistry from '../../SyncTargetRegistry';
|
||||
|
||||
async function clearSyncContext() {
|
||||
const syncTargetIds = SyncTargetRegistry.allIds();
|
||||
|
||||
@@ -35,7 +35,7 @@ const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js');
|
||||
const { FileApiDriverDropbox } = require('../file-api-driver-dropbox.js');
|
||||
const { FileApiDriverOneDrive } = require('../file-api-driver-onedrive.js');
|
||||
const { FileApiDriverAmazonS3 } = require('../file-api-driver-amazon-s3.js');
|
||||
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
|
||||
import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||
const SyncTargetMemory = require('../SyncTargetMemory.js');
|
||||
const SyncTargetFilesystem = require('../SyncTargetFilesystem.js');
|
||||
const SyncTargetNextcloud = require('../SyncTargetNextcloud.js');
|
||||
|
||||
6355
packages/plugin-repo-cli/package-lock.json
generated
6355
packages/plugin-repo-cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/plugin-repo-cli",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
@@ -18,8 +18,8 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~2.3",
|
||||
"@joplin/tools": "~2.3",
|
||||
"@joplin/lib": "^2.3.1",
|
||||
"@joplin/tools": "^2.3.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"gh-release-assets": "^2.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import MdToHtml from './MdToHtml';
|
||||
import HtmlToHtml from './HtmlToHtml';
|
||||
import htmlUtils from './htmlUtils';
|
||||
import { Options as NoteStyleOptions } from './noteStyle';
|
||||
const MarkdownIt = require('markdown-it');
|
||||
|
||||
export enum MarkupLanguage {
|
||||
@@ -48,7 +49,7 @@ export default class MarkupToHtml {
|
||||
private options_: Options;
|
||||
private rawMarkdownIt_: any;
|
||||
|
||||
public constructor(options: Options) {
|
||||
public constructor(options: Options = null) {
|
||||
this.options_ = {
|
||||
ResourceModel: {
|
||||
isResourceUrl: () => false,
|
||||
@@ -119,7 +120,7 @@ export default class MarkupToHtml {
|
||||
return this.renderer(markupLanguage).render(markup, theme, options);
|
||||
}
|
||||
|
||||
public async allAssets(markupLanguage: MarkupLanguage, theme: any) {
|
||||
return this.renderer(markupLanguage).allAssets(theme);
|
||||
public async allAssets(markupLanguage: MarkupLanguage, theme: any, noteStyleOptions: NoteStyleOptions = null) {
|
||||
return this.renderer(markupLanguage).allAssets(theme, noteStyleOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,28 @@ import setupLinkify from './MdToHtml/setupLinkify';
|
||||
import validateLinks from './MdToHtml/validateLinks';
|
||||
import { ItemIdToUrlHandler } from './utils';
|
||||
import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
|
||||
import { Options as NoteStyleOptions } from './noteStyle';
|
||||
|
||||
const MarkdownIt = require('markdown-it');
|
||||
const md5 = require('md5');
|
||||
|
||||
export interface RenderOptions {
|
||||
contentMaxWidth?: number;
|
||||
bodyOnly?: boolean;
|
||||
splitted?: boolean;
|
||||
externalAssetsOnly?: boolean;
|
||||
postMessageSyntax?: string;
|
||||
highlightedKeywords?: string[];
|
||||
codeTheme?: string;
|
||||
theme?: any;
|
||||
plugins?: Record<string, any>;
|
||||
audioPlayerEnabled?: boolean;
|
||||
videoPlayerEnabled?: boolean;
|
||||
pdfViewerEnabled?: boolean;
|
||||
codeHighlightCacheKey?: string;
|
||||
plainResourceRendering?: boolean;
|
||||
}
|
||||
|
||||
interface RendererRule {
|
||||
install(context: any, ruleOptions: any): any;
|
||||
assets?(theme: any): any;
|
||||
@@ -331,7 +349,7 @@ export default class MdToHtml {
|
||||
}
|
||||
|
||||
// This is similar to allProcessedAssets() but used only by the Rich Text editor
|
||||
public async allAssets(theme: any) {
|
||||
public async allAssets(theme: any, noteStyleOptions: NoteStyleOptions = null) {
|
||||
const assets: any = {};
|
||||
for (const key in rules) {
|
||||
if (!this.pluginEnabled(key)) continue;
|
||||
@@ -343,7 +361,7 @@ export default class MdToHtml {
|
||||
}
|
||||
|
||||
const processedAssets = this.processPluginAssets(assets);
|
||||
processedAssets.cssStrings.splice(0, 0, noteStyle(theme).join('\n'));
|
||||
processedAssets.cssStrings.splice(0, 0, noteStyle(theme, noteStyleOptions).join('\n'));
|
||||
if (this.customCss_) processedAssets.cssStrings.push(this.customCss_);
|
||||
const output = await this.outputAssetsToExternalAssets_(processedAssets);
|
||||
return output.pluginAssets;
|
||||
@@ -376,8 +394,9 @@ export default class MdToHtml {
|
||||
}
|
||||
|
||||
// "theme" is the theme as returned by themeStyle()
|
||||
public async render(body: string, theme: any = null, options: any = null): Promise<RenderResult> {
|
||||
options = Object.assign({}, {
|
||||
public async render(body: string, theme: any = null, options: RenderOptions = null): Promise<RenderResult> {
|
||||
|
||||
options = {
|
||||
// In bodyOnly mode, the rendered Markdown is returned without the wrapper DIV
|
||||
bodyOnly: false,
|
||||
// In splitted mode, the CSS and HTML will be returned in separate properties.
|
||||
@@ -395,7 +414,10 @@ export default class MdToHtml {
|
||||
audioPlayerEnabled: this.pluginEnabled('audioPlayer'),
|
||||
videoPlayerEnabled: this.pluginEnabled('videoPlayer'),
|
||||
pdfViewerEnabled: this.pluginEnabled('pdfViewer'),
|
||||
}, options);
|
||||
|
||||
contentMaxWidth: 0,
|
||||
...options,
|
||||
};
|
||||
|
||||
// The "codeHighlightCacheKey" option indicates what set of cached object should be
|
||||
// associated with this particular Markdown body. It is only used to allow us to
|
||||
@@ -525,7 +547,9 @@ export default class MdToHtml {
|
||||
|
||||
const renderedBody = markdownIt.render(body, context);
|
||||
|
||||
let cssStrings = noteStyle(options.theme);
|
||||
let cssStrings = noteStyle(options.theme, {
|
||||
contentMaxWidth: options.contentMaxWidth,
|
||||
});
|
||||
|
||||
let output = { ...this.allProcessedAssets(allRules, options.theme, options.codeTheme) };
|
||||
cssStrings = cssStrings.concat(output.cssStrings);
|
||||
|
||||
@@ -7,11 +7,28 @@ function formatCssSize(v: any): string {
|
||||
return `${v}px`;
|
||||
}
|
||||
|
||||
export default function(theme: any) {
|
||||
export interface Options {
|
||||
contentMaxWidth?: number;
|
||||
}
|
||||
|
||||
export default function(theme: any, options: Options = null) {
|
||||
options = {
|
||||
contentMaxWidth: 0,
|
||||
...options,
|
||||
};
|
||||
|
||||
theme = theme ? theme : {};
|
||||
|
||||
const fontFamily = '\'Avenir\', \'Arial\', sans-serif';
|
||||
|
||||
const maxWidthCss = options.contentMaxWidth ? `
|
||||
#rendered-md {
|
||||
max-width: ${options.contentMaxWidth}px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
` : '';
|
||||
|
||||
const css =
|
||||
`
|
||||
/* https://necolas.github.io/normalize.css/ */
|
||||
@@ -61,6 +78,8 @@ export default function(theme: any) {
|
||||
background: rgba(100, 100, 100, 0.7);
|
||||
}
|
||||
|
||||
${maxWidthCss}
|
||||
|
||||
/* Remove top padding and margin from first child so that top of rendered text is aligned to top of text editor text */
|
||||
|
||||
#rendered-md > h1:first-child,
|
||||
|
||||
19
packages/renderer/package-lock.json
generated
19
packages/renderer/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "^10.2.1",
|
||||
"highlight.js": "^11.2.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"katex": "^0.13.3",
|
||||
@@ -3551,12 +3551,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.2.1.tgz",
|
||||
"integrity": "sha512-A+sckVPIb9zQTUydC9lpRX1qRFO/N0OKEh0NwIr65ckvWA/oMY8v9P3+kGRK3w2ULSh9E8v5MszXafodQ6039g==",
|
||||
"deprecated": "Potential vulnerability. Please upgrade to @latest",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
|
||||
"integrity": "sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
@@ -11519,9 +11518,9 @@
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.2.1.tgz",
|
||||
"integrity": "sha512-A+sckVPIb9zQTUydC9lpRX1qRFO/N0OKEh0NwIr65ckvWA/oMY8v9P3+kGRK3w2ULSh9E8v5MszXafodQ6039g=="
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.2.0.tgz",
|
||||
"integrity": "sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw=="
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "The Joplin note renderer, used the mobile and desktop application",
|
||||
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
|
||||
"main": "index.js",
|
||||
@@ -24,10 +24,10 @@
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.32",
|
||||
"@joplin/fork-htmlparser2": "^4.1.33",
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "^10.2.1",
|
||||
"highlight.js": "^11.2.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"katex": "^0.13.3",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// We don't want the tests to fail due to timeout, especially on CI, and certain
|
||||
// tests can take more time since we do integration testing too.
|
||||
jest.setTimeout(30 * 1000);
|
||||
|
||||
process.env.JOPLIN_IS_TESTING = '1';
|
||||
|
||||
27966
packages/server/package-lock.json
generated
27966
packages/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.3.2",
|
||||
"version": "2.3.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
"start-dev-no-watch": "node dist/app.js --env dev",
|
||||
"devCreateDb": "node dist/app.js --env dev --create-db",
|
||||
"devDropTables": "node dist/app.js --env dev --drop-tables",
|
||||
"devDropDb": "node dist/app.js --env dev --drop-db",
|
||||
"start": "node dist/app.js",
|
||||
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
|
||||
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-latest --env buildTypes && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"test": "jest --verbose=false",
|
||||
"test-ci": "npm run test",
|
||||
@@ -42,6 +43,7 @@
|
||||
"pg": "^8.5.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"query-string": "^6.8.3",
|
||||
"rate-limiter-flexible": "^2.2.4",
|
||||
"raw-body": "^2.4.1",
|
||||
"sqlite3": "^4.1.0",
|
||||
"stripe": "^8.150.0",
|
||||
|
||||
0
packages/server/public/css/index/changes.css
Normal file
0
packages/server/public/css/index/changes.css
Normal file
@@ -57,6 +57,10 @@ ul li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ul.pagination-list li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.readable-block {
|
||||
max-width: 740px;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { argv } from 'yargs';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
||||
import config, { initConfig, runningInDocker, EnvVariables } from './config';
|
||||
import { createDb, dropDb } from './tools/dbTools';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteDefaultDir } from './db';
|
||||
import { dropTables, connectDb, disconnectDb, migrateLatest, waitForConnection, sqliteDefaultDir, migrateList, migrateUp, migrateDown } from './db';
|
||||
import { AppContext, Env, KoaNext } from './utils/types';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import routeHandler from './middleware/routeHandler';
|
||||
@@ -205,10 +205,23 @@ async function main() {
|
||||
fs.writeFileSync(pidFile, `${process.pid}`);
|
||||
}
|
||||
|
||||
if (argv.migrateDb) {
|
||||
let runCommandAndExitApp = true;
|
||||
|
||||
if (argv.migrateLatest) {
|
||||
const db = await connectDb(config().database);
|
||||
await migrateDb(db);
|
||||
await migrateLatest(db);
|
||||
await disconnectDb(db);
|
||||
} else if (argv.migrateUp) {
|
||||
const db = await connectDb(config().database);
|
||||
await migrateUp(db);
|
||||
await disconnectDb(db);
|
||||
} else if (argv.migrateDown) {
|
||||
const db = await connectDb(config().database);
|
||||
await migrateDown(db);
|
||||
await disconnectDb(db);
|
||||
} else if (argv.migrateList) {
|
||||
const db = await connectDb(config().database);
|
||||
console.info(await migrateList(db));
|
||||
} else if (argv.dropDb) {
|
||||
await dropDb(config().database, { ignoreIfNotExists: true });
|
||||
} else if (argv.dropTables) {
|
||||
@@ -218,6 +231,8 @@ async function main() {
|
||||
} else if (argv.createDb) {
|
||||
await createDb(config().database);
|
||||
} else {
|
||||
runCommandAndExitApp = false;
|
||||
|
||||
appLogger().info(`Starting server v${config().appVersion} (${env}) on port ${config().port} and PID ${process.pid}...`);
|
||||
appLogger().info('Running in Docker:', runningInDocker());
|
||||
appLogger().info('Public base URL:', config().baseUrl);
|
||||
@@ -239,7 +254,7 @@ async function main() {
|
||||
await initializeJoplinUtils(config(), ctx.joplinBase.models, ctx.joplinBase.services.mustache);
|
||||
|
||||
appLogger().info('Migrating database...');
|
||||
await migrateDb(ctx.joplinBase.db);
|
||||
await migrateLatest(ctx.joplinBase.db);
|
||||
|
||||
appLogger().info('Starting services...');
|
||||
await startServices(ctx.joplinBase.services);
|
||||
@@ -248,6 +263,8 @@ async function main() {
|
||||
|
||||
app.listen(config().port);
|
||||
}
|
||||
|
||||
if (runCommandAndExitApp) process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error: any) => {
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface EnvVariables {
|
||||
SUPPORT_NAME?: string;
|
||||
|
||||
BUSINESS_EMAIL?: string;
|
||||
|
||||
COOKIES_SECURE?: string;
|
||||
}
|
||||
|
||||
let runningInDocker_: boolean = false;
|
||||
@@ -168,6 +170,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
||||
supportEmail,
|
||||
supportName: env.SUPPORT_NAME || appName,
|
||||
businessEmail: env.BUSINESS_EMAIL || supportEmail,
|
||||
cookieSecure: env.COOKIES_SECURE === '1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,14 +121,85 @@ export async function disconnectDb(db: DbConnection) {
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
export async function migrateDb(db: DbConnection) {
|
||||
export async function migrateLatest(db: DbConnection) {
|
||||
await db.migrate.latest({
|
||||
directory: migrationDir,
|
||||
// Disable transactions because the models might open one too
|
||||
disableTransactions: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function migrateUp(db: DbConnection) {
|
||||
await db.migrate.up({
|
||||
directory: migrationDir,
|
||||
});
|
||||
}
|
||||
|
||||
export async function migrateDown(db: DbConnection) {
|
||||
await db.migrate.down({
|
||||
directory: migrationDir,
|
||||
});
|
||||
}
|
||||
|
||||
export async function migrateList(db: DbConnection, asString: boolean = true) {
|
||||
const migrations: any = await db.migrate.list({
|
||||
directory: migrationDir,
|
||||
});
|
||||
|
||||
// The migration array has a rather inconsistent format:
|
||||
//
|
||||
// [
|
||||
// // Done migrations
|
||||
// [
|
||||
// '20210809222118_email_key_fix.js',
|
||||
// '20210814123815_testing.js',
|
||||
// '20210814123816_testing.js'
|
||||
// ],
|
||||
// // Not done migrations
|
||||
// [
|
||||
// {
|
||||
// file: '20210814123817_testing.js',
|
||||
// directory: '/path/to/packages/server/dist/migrations'
|
||||
// }
|
||||
// ]
|
||||
// ]
|
||||
|
||||
if (!asString) return migrations;
|
||||
|
||||
const formatName = (migrationInfo: any) => {
|
||||
const name = migrationInfo.file ? migrationInfo.file : migrationInfo;
|
||||
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
return s.join('.');
|
||||
};
|
||||
|
||||
interface Line {
|
||||
text: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
const output: Line[] = [];
|
||||
|
||||
for (const s of migrations[0]) {
|
||||
output.push({
|
||||
text: formatName(s),
|
||||
done: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const s of migrations[1]) {
|
||||
output.push({
|
||||
text: formatName(s),
|
||||
done: false,
|
||||
});
|
||||
}
|
||||
|
||||
output.sort((a, b) => {
|
||||
return a.text < b.text ? -1 : +1;
|
||||
});
|
||||
|
||||
return output.map(l => `${l.done ? '✓' : '✗'} ${l.text}`).join('\n');
|
||||
}
|
||||
|
||||
function allTableNames(): string[] {
|
||||
const tableNames = Object.keys(databaseSchema);
|
||||
tableNames.push('knex_migrations');
|
||||
|
||||
@@ -38,6 +38,8 @@ export default async function(ctx: AppContext) {
|
||||
|
||||
const responseFormat = routeResponseFormat(ctx);
|
||||
|
||||
if (error.retryAfterMs) ctx.response.set('Retry-After', Math.ceil(error.retryAfterMs / 1000).toString());
|
||||
|
||||
if (error.code === 'invalidOrigin') {
|
||||
ctx.response.body = error.message;
|
||||
} else if (responseFormat === RouteResponseFormat.Html) {
|
||||
|
||||
@@ -2,13 +2,17 @@ import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
table.text('key', 'mediumtext').defaultTo('').notNullable();
|
||||
});
|
||||
try {
|
||||
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
table.text('key', 'mediumtext').defaultTo('').notNullable();
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Could not add "emails.key" column', error);
|
||||
}
|
||||
|
||||
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
table.unique(['recipient_email', 'key']);
|
||||
});
|
||||
// await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
// table.unique(['recipient_email', 'key']);
|
||||
// });
|
||||
}
|
||||
|
||||
export async function down(_db: DbConnection): Promise<any> {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Knex } from 'knex';
|
||||
// import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
table.dropUnique(['recipient_email', 'key']);
|
||||
});
|
||||
export async function up(_db: DbConnection): Promise<any> {
|
||||
// try {
|
||||
// await db.schema.alterTable('emails', function(table: Knex.CreateTableBuilder) {
|
||||
// table.dropUnique(['recipient_email', 'key']);
|
||||
// });
|
||||
// } catch (error) {
|
||||
// // console.warn('Could not drop unique constraint - this is not an error.', error);
|
||||
// }
|
||||
}
|
||||
|
||||
export async function down(_db: DbConnection): Promise<any> {
|
||||
|
||||
@@ -5,12 +5,15 @@ import { ErrorForbidden } from '../../utils/errors';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import { User } from '../../db';
|
||||
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||
|
||||
const router = new Router(RouteType.Api);
|
||||
|
||||
router.public = true;
|
||||
|
||||
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
|
||||
await limiterLoginBruteForce(ctx.ip);
|
||||
|
||||
const fields: User = await bodyFields(ctx.req);
|
||||
const user = await ctx.joplin.models.user().login(fields.email, fields.password);
|
||||
if (!user) throw new ErrorForbidden('Invalid username or password');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Session } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { cookieGet } from '../../utils/cookies';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, parseHtml, createUser } from '../../utils/testing/testUtils';
|
||||
import { AppContext } from '../../utils/types';
|
||||
|
||||
@@ -52,7 +53,7 @@ describe('index_login', function() {
|
||||
const user = await createUser(1);
|
||||
|
||||
const context = await doLogin(user.email, '123456');
|
||||
const sessionId = context.cookies.get('sessionId');
|
||||
const sessionId = cookieGet(context, 'sessionId');
|
||||
const session: Session = await models().session().load(sessionId);
|
||||
expect(session.user_id).toBe(user.id);
|
||||
});
|
||||
@@ -62,12 +63,12 @@ describe('index_login', function() {
|
||||
|
||||
{
|
||||
const context = await doLogin('bad', '123456');
|
||||
expect(!context.cookies.get('sessionId')).toBe(true);
|
||||
expect(!cookieGet(context, 'sessionId')).toBe(true);
|
||||
}
|
||||
|
||||
{
|
||||
const context = await doLogin(user.email, 'bad');
|
||||
expect(!context.cookies.get('sessionId')).toBe(true);
|
||||
expect(!cookieGet(context, 'sessionId')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import { formParse } from '../../utils/requestUtils';
|
||||
import config from '../../config';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||
import { cookieSet } from '../../utils/cookies';
|
||||
|
||||
function makeView(error: any = null): View {
|
||||
const view = defaultView('login', 'Login');
|
||||
@@ -25,11 +27,13 @@ router.get('login', async (_path: SubPath, _ctx: AppContext) => {
|
||||
});
|
||||
|
||||
router.post('login', async (_path: SubPath, ctx: AppContext) => {
|
||||
await limiterLoginBruteForce(ctx.ip);
|
||||
|
||||
try {
|
||||
const body = await formParse(ctx.req);
|
||||
|
||||
const session = await ctx.joplin.models.session().authenticate(body.fields.email, body.fields.password);
|
||||
ctx.cookies.set('sessionId', session.id);
|
||||
cookieSet(ctx, 'sessionId', session.id);
|
||||
return redirect(ctx, `${config().baseUrl}/home`);
|
||||
} catch (error) {
|
||||
return makeView(error);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { cookieGet } from '../../utils/cookies';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils';
|
||||
|
||||
describe('index_logout', function() {
|
||||
@@ -26,11 +27,11 @@ describe('index_logout', function() {
|
||||
},
|
||||
});
|
||||
|
||||
expect(context.cookies.get('sessionId')).toBe(session.id);
|
||||
expect(cookieGet(context, 'sessionId')).toBe(session.id);
|
||||
expect(!!(await models().session().load(session.id))).toBe(true);
|
||||
await routeHandler(context);
|
||||
|
||||
expect(!context.cookies.get('sessionId')).toBe(true);
|
||||
expect(!cookieGet(context, 'sessionId')).toBe(true);
|
||||
expect(!!(await models().session().load(session.id))).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import config from '../../config';
|
||||
import { contextSessionId } from '../../utils/requestUtils';
|
||||
import { cookieSet } from '../../utils/cookies';
|
||||
|
||||
const router = new Router(RouteType.Web);
|
||||
|
||||
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
|
||||
const sessionId = contextSessionId(ctx, false);
|
||||
ctx.cookies.set('sessionId', '');
|
||||
cookieSet(ctx, 'sessionId', '');
|
||||
await ctx.joplin.models.session().logout(sessionId);
|
||||
return redirect(ctx, `${config().baseUrl}/login`);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { AccountType } from '../../models/UserModel';
|
||||
import { getCanShareFolder, getMaxItemSize } from '../../models/utils/user';
|
||||
import { MB } from '../../utils/bytes';
|
||||
import { cookieGet } from '../../utils/cookies';
|
||||
import { execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
@@ -50,7 +51,7 @@ describe('index_signup', function() {
|
||||
expect(getMaxItemSize(user)).toBe(10 * MB);
|
||||
|
||||
// Check that the user is logged in
|
||||
const session = await models().session().load(context.cookies.get('sessionId'));
|
||||
const session = await models().session().load(cookieGet(context, 'sessionId'));
|
||||
expect(session.user_id).toBe(user.id);
|
||||
|
||||
// Check that the notification has been created
|
||||
|
||||
@@ -10,6 +10,7 @@ import { checkRepeatPassword } from './users';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { AccountType } from '../../models/UserModel';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import { cookieSet } from '../../utils/cookies';
|
||||
|
||||
function makeView(error: Error = null): View {
|
||||
const view = defaultView('signup', 'Sign Up');
|
||||
@@ -51,7 +52,7 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => {
|
||||
});
|
||||
|
||||
const session = await ctx.joplin.models.session().createUserSession(user.id);
|
||||
ctx.cookies.set('sessionId', session.id);
|
||||
cookieSet(ctx, 'sessionId', session.id);
|
||||
|
||||
await ctx.joplin.models.notification().add(user.id, NotificationKey.ConfirmEmail);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { User } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { cookieGet } from '../../utils/cookies';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils';
|
||||
@@ -240,7 +241,7 @@ describe('index/users', function() {
|
||||
password: newPassword,
|
||||
password2: newPassword,
|
||||
});
|
||||
const sessionId = context.cookies.get('sessionId');
|
||||
const sessionId = cookieGet(context, 'sessionId');
|
||||
expect(sessionId).toBeFalsy();
|
||||
}
|
||||
|
||||
@@ -253,7 +254,7 @@ describe('index/users', function() {
|
||||
password2: newPassword,
|
||||
token: token2,
|
||||
});
|
||||
const sessionId = context.cookies.get('sessionId');
|
||||
const sessionId = cookieGet(context, 'sessionId');
|
||||
expect(sessionId).toBeFalsy();
|
||||
}
|
||||
|
||||
@@ -266,7 +267,7 @@ describe('index/users', function() {
|
||||
});
|
||||
|
||||
// Check that the user has been logged in
|
||||
const sessionId = context.cookies.get('sessionId');
|
||||
const sessionId = cookieGet(context, 'sessionId');
|
||||
const session = await models().session().load(sessionId);
|
||||
expect(session.user_id).toBe(user1.id);
|
||||
|
||||
@@ -303,7 +304,7 @@ describe('index/users', function() {
|
||||
user1 = await models().user().load(user1.id);
|
||||
|
||||
// Check that the user has been logged in
|
||||
const sessionId = context.cookies.get('sessionId');
|
||||
const sessionId = cookieGet(context, 'sessionId');
|
||||
expect(sessionId).toBeFalsy();
|
||||
|
||||
// Check that the email has been verified
|
||||
|
||||
@@ -19,6 +19,7 @@ import { confirmUrl } from '../../utils/urlUtils';
|
||||
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
||||
import { createCsrfTag } from '../../utils/csrf';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import { cookieSet } from '../../utils/cookies';
|
||||
|
||||
export interface CheckRepeatPasswordInput {
|
||||
password: string;
|
||||
@@ -236,7 +237,7 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => {
|
||||
await ctx.joplin.models.token().deleteByValue(userId, fields.token);
|
||||
|
||||
const session = await ctx.joplin.models.session().createUserSession(userId);
|
||||
ctx.cookies.set('sessionId', session.id);
|
||||
cookieSet(ctx, 'sessionId', session.id);
|
||||
|
||||
await ctx.joplin.models.notification().add(userId, NotificationKey.PasswordSet);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { connectDb, disconnectDb, migrateDb } from '../db';
|
||||
import { connectDb, disconnectDb, migrateLatest } from '../db';
|
||||
import * as fs from 'fs-extra';
|
||||
import { DatabaseConfig } from '../utils/types';
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
|
||||
|
||||
try {
|
||||
const db = await connectDb(config);
|
||||
await migrateDb(db);
|
||||
await migrateLatest(db);
|
||||
await disconnectDb(db);
|
||||
} catch (error) {
|
||||
error.message += `: ${config.name}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DbConnection, dropTables, migrateDb } from '../db';
|
||||
import { DbConnection, dropTables, migrateLatest } from '../db';
|
||||
import newModelFactory from '../models/factory';
|
||||
import { AccountType } from '../models/UserModel';
|
||||
import { Config } from '../utils/types';
|
||||
@@ -15,7 +15,7 @@ export async function handleDebugCommands(argv: any, db: DbConnection, config: C
|
||||
|
||||
export async function createTestUsers(db: DbConnection, config: Config) {
|
||||
await dropTables(db);
|
||||
await migrateDb(db);
|
||||
await migrateLatest(db);
|
||||
|
||||
const password = 'hunter1hunter2hunter3';
|
||||
const models = newModelFactory(db, config);
|
||||
|
||||
17
packages/server/src/utils/cookies.ts
Normal file
17
packages/server/src/utils/cookies.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import config from '../config';
|
||||
import { AppContext } from './types';
|
||||
|
||||
export function cookieSet(ctx: AppContext, name: string, value: string) {
|
||||
ctx.cookies.set(name, value, {
|
||||
// Means that the cookies cannot be accessed from JavaScript
|
||||
httpOnly: true,
|
||||
// Can only be transferred over https
|
||||
secure: config().cookieSecure,
|
||||
// Prevent cookies from being sent in cross-site requests
|
||||
sameSite: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function cookieGet(ctx: AppContext, name: string) {
|
||||
return ctx.cookies.get(name);
|
||||
}
|
||||
@@ -97,6 +97,17 @@ export class ErrorPayloadTooLarge extends ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorTooManyRequests extends ApiError {
|
||||
public static httpCode: number = 429;
|
||||
public retryAfterMs: number = 0;
|
||||
|
||||
public constructor(message: string = null, retryAfterMs: number = 0) {
|
||||
super(message === null ? 'Too Many Requests' : message, ErrorTooManyRequests.httpCode);
|
||||
this.retryAfterMs = retryAfterMs;
|
||||
Object.setPrototypeOf(this, ErrorTooManyRequests.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export function errorToString(error: Error): string {
|
||||
const msg: string[] = [];
|
||||
msg.push(error.message ? error.message : 'Unknown error');
|
||||
|
||||
19
packages/server/src/utils/request/limiterLoginBruteForce.ts
Normal file
19
packages/server/src/utils/request/limiterLoginBruteForce.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
|
||||
import { ErrorTooManyRequests } from '../errors';
|
||||
|
||||
const limiterSlowBruteByIP = new RateLimiterMemory({
|
||||
points: 3, // Up to 3 requests per IP
|
||||
duration: 30, // Per 30 seconds
|
||||
});
|
||||
|
||||
export default async function(ip: string) {
|
||||
// Tests need to make many requests quickly so we disable it in this case.
|
||||
if (process.env.JOPLIN_IS_TESTING === '1') return;
|
||||
|
||||
try {
|
||||
await limiterSlowBruteByIP.consume(ip);
|
||||
} catch (error) {
|
||||
const result = error as RateLimiterRes;
|
||||
throw new ErrorTooManyRequests(`Too many login attempts. Please try again in ${Math.ceil(result.msBeforeNext / 1000)} seconds.`, result.msBeforeNext);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cookieGet } from './cookies';
|
||||
import { ErrorForbidden } from './errors';
|
||||
import { AppContext } from './types';
|
||||
|
||||
@@ -61,7 +62,7 @@ export function headerSessionId(headers: any): string {
|
||||
export function contextSessionId(ctx: AppContext, throwIfNotFound = true): string {
|
||||
if (ctx.headers['x-api-auth']) return ctx.headers['x-api-auth'];
|
||||
|
||||
const id = ctx.cookies.get('sessionId');
|
||||
const id = cookieGet(ctx, 'sessionId');
|
||||
if (!id && throwIfNotFound) throw new ErrorForbidden('Invalid or missing session');
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -106,9 +106,9 @@ async function main() {
|
||||
|
||||
fs.removeSync(`${serverRoot}/db-testing.sqlite`);
|
||||
|
||||
// const migrateCommand = 'NODE_ENV=testing node dist/app.js --migrate-db --env dev';
|
||||
// const migrateCommand = 'NODE_ENV=testing node dist/app.js --migrate-latest --env dev';
|
||||
const clearCommand = 'node dist/app.js --env dev --drop-tables';
|
||||
const migrateCommand = 'node dist/app.js --env dev --migrate-db';
|
||||
const migrateCommand = 'node dist/app.js --env dev --migrate-latest';
|
||||
|
||||
await execCommand(clearCommand);
|
||||
await execCommand(migrateCommand);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { initializeJoplinUtils } from '../joplinUtils';
|
||||
import MustacheService from '../../services/MustacheService';
|
||||
import uuidgen from '../uuidgen';
|
||||
import { createCsrfToken } from '../csrf';
|
||||
import { cookieSet } from '../cookies';
|
||||
|
||||
// Takes into account the fact that this file will be inside the /dist directory
|
||||
// when it runs.
|
||||
@@ -211,7 +212,7 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
|
||||
};
|
||||
|
||||
if (options.sessionId) {
|
||||
appContext.cookies.set('sessionId', options.sessionId);
|
||||
cookieSet(appContext, 'sessionId', options.sessionId);
|
||||
}
|
||||
|
||||
return appContext as AppContext;
|
||||
|
||||
11
packages/server/src/utils/time.test.ts
Normal file
11
packages/server/src/utils/time.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Day, Month, Second } from './time';
|
||||
|
||||
describe('time', function() {
|
||||
|
||||
it('should have correct interval durations', async function() {
|
||||
expect(Second).toBe(1000);
|
||||
expect(Day).toBe(86400000);
|
||||
expect(Month).toBe(2592000000);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -14,7 +14,7 @@ function initDayJs() {
|
||||
|
||||
initDayJs();
|
||||
|
||||
export const Second = 60 * 1000;
|
||||
export const Second = 1000;
|
||||
export const Minute = 60 * Second;
|
||||
export const Hour = 60 * Minute;
|
||||
export const Day = 24 * Hour;
|
||||
|
||||
@@ -108,6 +108,7 @@ export interface Config {
|
||||
supportName: string;
|
||||
businessEmail: string;
|
||||
isJoplinCloud: boolean;
|
||||
cookieSecure: boolean;
|
||||
}
|
||||
|
||||
export enum HttpMethod {
|
||||
|
||||
@@ -16,6 +16,7 @@ async function main() {
|
||||
const argv = require('yargs').argv;
|
||||
if (!argv.tagName) throw new Error('--tag-name not provided');
|
||||
|
||||
const pushImages = !!argv.pushImages;
|
||||
const tagName = argv.tagName;
|
||||
const isPreRelease = getIsPreRelease(tagName);
|
||||
const imageVersion = getVersionFromTag(tagName, isPreRelease);
|
||||
@@ -38,6 +39,7 @@ async function main() {
|
||||
console.info(`Running from: ${process.cwd()}`);
|
||||
|
||||
console.info('tagName:', tagName);
|
||||
console.info('pushImages:', pushImages);
|
||||
console.info('imageVersion:', imageVersion);
|
||||
console.info('isPreRelease:', isPreRelease);
|
||||
console.info('Docker tags:', dockerTags.join(', '));
|
||||
@@ -45,7 +47,7 @@ async function main() {
|
||||
await execCommand2(`docker build -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`);
|
||||
for (const tag of dockerTags) {
|
||||
await execCommand2(`docker tag "joplin/server:${imageVersion}" "joplin/server:${tag}"`);
|
||||
await execCommand2(`docker push joplin/server:${tag}`);
|
||||
if (pushImages) await execCommand2(`docker push joplin/server:${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3153,7 +3153,7 @@ msgstr "titel"
|
||||
|
||||
#: packages/lib/models/Folder.js:39 packages/lib/models/Note.js:37
|
||||
msgid "updated date"
|
||||
msgstr "uppdaterad datum"
|
||||
msgstr "uppdaterat datum"
|
||||
|
||||
#: packages/lib/models/Folder.js:103
|
||||
msgid "Conflicts"
|
||||
@@ -3171,7 +3171,7 @@ msgstr ""
|
||||
|
||||
#: packages/lib/models/Note.js:38
|
||||
msgid "created date"
|
||||
msgstr "skapad datum"
|
||||
msgstr "skapat datum"
|
||||
|
||||
#: packages/lib/models/Note.js:39
|
||||
msgid "custom order"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user