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

Compare commits

..

7 Commits

Author SHA1 Message Date
Laurent Cozic
afd63aa3c8 test 2022-01-08 12:49:45 +00:00
Laurent Cozic
04c3c218b9 test 2022-01-08 12:39:24 +00:00
Laurent Cozic
088ae44c63 test 2022-01-08 12:37:56 +00:00
Laurent Cozic
4aa9339fbb test 2022-01-08 12:36:15 +00:00
Laurent Cozic
7edcbc5c27 test 2022-01-08 12:33:40 +00:00
Laurent Cozic
f3650097a0 test 2022-01-08 12:22:44 +00:00
Laurent Cozic
6a4326e2db test 2022-01-08 12:21:27 +00:00
52 changed files with 166 additions and 659 deletions

View File

@@ -1,19 +1,12 @@
_mydocs/
_releases/
.git/
.yarn/cache/
**/.DS_Store
**/node_modules
Assets/
docs/
lerna-debug.log
packages/app-cli/
packages/app-clipper/
packages/app-desktop/
packages/app-mobile/
packages/generator-joplin/
packages/plugin-repo-cli/
.git/
_releases/
packages/app-desktop
packages/app-cli
packages/app-mobile
packages/app-clipper
packages/generator-joplin
packages/plugin-repo-cli
packages/server/db-*.sqlite
packages/server/dist/
packages/server/logs/
packages/server/temp/
packages/server/temp

View File

@@ -15,7 +15,6 @@ jobs:
stale-issue-message: "Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may comment on the issue and I will leave it open. Thank you for your contributions."
days-before-stale: 30
days-before-close: 7
operations-per-run: 1000
exempt-issue-labels: 'good first issue,upstream,backlog,high,medium,spec,cannot reproduce'
stale-issue-label: 'stale'
close-issue-message: 'Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, feel free to create a new issue with up-to-date information.'

View File

@@ -1,43 +0,0 @@
diff --git a/index.js b/index.js
index 85d89900d5fe575dd0c19430209fb1703b03554e..5fa68cc9a4bd2b21a7188bd263262fd9b1604ac6 100644
--- a/index.js
+++ b/index.js
@@ -145,7 +145,8 @@ module.exports = function multimd_table_plugin(md, options) {
colspan, leftToken,
rowspan, upTokens = [],
tableLines, tgroupLines,
- tag, text, range, r, c, b;
+ tag, text, range, r, c, b, t,
+ blockState;
if (startLine + 2 > endLine) { return false; }
@@ -315,18 +316,26 @@ module.exports = function multimd_table_plugin(md, options) {
/* Multiline. Join the text and feed into markdown-it blockParser. */
if (options.multiline && trToken.meta.multiline && trToken.meta.mbounds) {
- text = [ text.trimRight() ];
+ // Pad the text with empty lines to ensure the line number mapping is correct
+ text = new Array(trToken.map[0]).fill('').concat([ text.trimRight() ]);
for (b = 1; b < trToken.meta.mbounds.length; b++) {
/* Line with N bounds has cells indexed from 0 to N-2 */
if (c > trToken.meta.mbounds[b].length - 2) { continue; }
range = [ trToken.meta.mbounds[b][c] + 1, trToken.meta.mbounds[b][c + 1] ];
text.push(state.src.slice.apply(state.src, range).trimRight());
}
- state.md.block.parse(text.join('\n'), state.md, state.env, state.tokens);
+ blockState = new state.md.block.State(text.join('\n'), state.md, state.env, []);
+ blockState.level = trToken.level + 1;
+ // Start tokenizing from the actual content (trToken.map[0])
+ state.md.block.tokenize(blockState, trToken.map[0], blockState.lineMax);
+ for (t = 0; t < blockState.tokens.length; t++) {
+ state.tokens.push(blockState.tokens[t]);
+ }
} else {
token = state.push('inline', '', 0);
token.content = text.trim();
token.map = trToken.map;
+ token.level = trToken.level + 1;
token.children = [];
}

View File

@@ -728,23 +728,6 @@ footer .bottom-links-row p {
}
}
/*****************************************************************
MEDIUM VIEW
- Make menu bar elements smaller and closer to each others
so that everything fit.
*****************************************************************/
@media (max-width: 990px) {
#nav-section > .container {
max-width: 960px;
}
#nav-section .button-link {
padding: 4px 12px;
font-size: 15px;
}
}
/*****************************************************************
NARROW VIEW
- Top right menu is displayed
@@ -757,23 +740,6 @@ footer .bottom-links-row p {
padding-bottom: 130px;
}
#menu-mobile .social-links {
display: flex;
justify-content: center;
margin-top: 20px;
}
#menu-mobile .social-links a {
margin-left: 15px;
font-size: 20px;
}
#menu-mobile .social-links .social-link-mastodon,
#menu-mobile .social-links .social-link-reddit,
#menu-mobile .social-links .social-link-patreon {
display: none;
}
.front-page h1 {
font-size: 2.5em;
}
@@ -891,7 +857,7 @@ footer .bottom-links-row p {
}
#menu-mobile .button-link {
padding: 4px 12px;
padding: 10px 15px;
font-size: 16px;
margin-left: 0px;
}

View File

@@ -1,6 +1,15 @@
<footer class="darkblue-bg">
<div class="container">
{{> socialFeeds}}
<div class="row">
<div class="col-12 col-md-12 social-links">
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed"><i class="fab fa-twitter"></i></a>
<a href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
<a href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
<a href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
<a href="https://www.reddit.com/r/joplinapp/" title="Joplin Subreddit"><i class="fab fa-reddit"></i></a>
<a href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
</div>
</div>
<div class="row bottom-links-row">
<div class="col-12 col-md-6">

View File

@@ -12,8 +12,7 @@
</a>
</div>
<div class="col-9 text-right d-none d-md-block">
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
<a href="{{baseUrl}}/news/" class="fw500">News</a>
<a href="{{baseUrl}}/news/" class="fw500">What's New</a>
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
<a href="{{forumUrl}}" class="fw500">Forum</a>
{{#showJoplinCloudLinks}}
@@ -44,7 +43,7 @@
</div>
<div class="text-center menu-mobile-top">
<a href="{{baseUrl}}/news/" class="fw500 mobile-menu-link">News</a>
<a href="{{baseUrl}}/news/" class="fw500 mobile-menu-link">What's New</a>
<a href="{{baseUrl}}/help/" class="fw500 mobile-menu-link">Help</a>
<a href="{{forumUrl}}" class="fw500 mobile-menu-link">Forum</a>
</div>
@@ -60,8 +59,6 @@
{{#showToc}}
<div id="toc-mobile">{{{tocHtml}}}</div>
{{/showToc}}
{{> socialFeeds}}
<div>
<p class="light-blue mobile-menu-link-bottom text-center">

View File

@@ -1,10 +0,0 @@
<div class="row">
<div class="col-12 col-md-12 social-links">
<a class="social-link-twitter" href="https://twitter.com/joplinapp" title="Joplin Twitter feed"><i class="fab fa-twitter"></i></a>
<a class="social-link-mastodon" href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
<a class="social-link-patreon" href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
<a class="social-link-discord" href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
<a class="social-link-reddit" href="https://www.reddit.com/r/joplinapp/" title="Joplin Subreddit"><i class="fab fa-reddit"></i></a>
<a class="social-link-github" href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
</div>
</div>

View File

@@ -20,7 +20,7 @@ There are also a few forks of existing packages under the "fork-*" name.
- Install node 16+ - https://nodejs.org/en/
- [Enable yarn](https://yarnpkg.com/getting-started/install): `corepack enable`
- macOS: Install Cocoapods - `brew install cocoapods`. Apple Silicon [may require libvips](https://github.com/laurent22/joplin/pull/5966#issuecomment-1007158597) - `brew install vips`.
- macOS: Install Cocoapods - `brew install cocoapods`
- Windows: Install Windows Build Tools - `yarn install -g windows-build-tools --vs2015`
- Linux: Install dependencies - `sudo apt install build-essential libnss3 libsecret-1-dev python rsync`

View File

@@ -8,36 +8,66 @@ RUN apt-get update \
# Enables Yarn
RUN corepack enable
RUN echo "Node: $(node --version)" \
&& echo "Npm: $(npm --version)" \
&& echo "Yarn: $(yarn --version)"
RUN echo "Node: $(node --version)"
RUN echo "Npm: $(npm --version)"
RUN echo "Yarn: $(yarn --version)"
ARG user=joplin
RUN useradd --create-home --shell /bin/bash $user
USER $user
ENV NODE_ENV production
ENV RUNNING_IN_DOCKER 1
EXPOSE ${APP_PORT}
ENV NODE_ENV development
WORKDIR /home/$user
RUN mkdir /home/$user/logs \
&& mkdir /home/$user/.yarn
RUN mkdir /home/$user/logs
COPY --chown=$user:$user .yarn/patches ./.yarn/patches
COPY --chown=$user:$user .yarn/plugins ./.yarn/plugins
COPY --chown=$user:$user .yarn/releases ./.yarn/releases
COPY --chown=$user:$user package.json .
# Install the root scripts but don't run postinstall (which would bootstrap
# and build TypeScript files, but we don't have the TypeScript files at
# this point)
COPY --chown=$user:$user package*.json ./
COPY --chown=$user:$user .yarn ./.yarn
COPY --chown=$user:$user .yarnrc.yml .
COPY --chown=$user:$user yarn.lock .
COPY --chown=$user:$user gulpfile.js .
RUN yarn install --inline-builds --mode=skip-build
# To take advantage of the Docker cache, we first copy all the package.json
# and package-lock.json files, as they rarely change, and then bootstrap
# all the packages.
#
# Note that bootstrapping the packages will run all the postinstall
# scripts, which means that for packages that have such scripts, we need to
# copy all the files.
#
# We can't run boostrap with "--ignore-scripts" because that would
# prevent certain sub-packages, such as sqlite3, from being built
COPY --chown=$user:$user packages/fork-sax/package*.json ./packages/fork-sax/
COPY --chown=$user:$user packages/fork-uslug/package*.json ./packages/fork-uslug/
COPY --chown=$user:$user packages/htmlpack/package*.json ./packages/htmlpack/
COPY --chown=$user:$user packages/renderer/package*.json ./packages/renderer/
COPY --chown=$user:$user packages/tools/package*.json ./packages/tools/
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
COPY --chown=$user:$user tsconfig.json .
# The following have postinstall scripts so we need to copy all the files.
# Since they should rarely change this is not an issue
COPY --chown=$user:$user packages/turndown ./packages/turndown
COPY --chown=$user:$user packages/turndown-plugin-gfm ./packages/turndown-plugin-gfm
COPY --chown=$user:$user packages/fork-htmlparser2 ./packages/fork-htmlparser2
COPY --chown=$user:$user packages/server/package*.json ./packages/server/
# Then bootstrap only, without compiling the TypeScript files
RUN yarn install --inline-builds --mode=skip-build
# Now copy the source files. Put lib and server last as they are more likely to change.
COPY --chown=$user:$user packages/fork-sax ./packages/fork-sax
COPY --chown=$user:$user packages/fork-uslug ./packages/fork-uslug
COPY --chown=$user:$user packages/htmlpack ./packages/htmlpack
@@ -46,23 +76,21 @@ COPY --chown=$user:$user packages/tools ./packages/tools
COPY --chown=$user:$user packages/lib ./packages/lib
COPY --chown=$user:$user packages/server ./packages/server
# For some reason there's both a .yarn/cache and .yarn/berry/cache that are
# being generated, and both have the same content. Not clear why it does this
# but we can delete it anyway. We can delete the cache because we use
# `nodeLinker: node-modules`. If we ever implement Zero Install, we'll need to
# keep the cache.
#
# Note that `yarn install` ignores `NODE_ENV=production` and will install dev
# dependencies too, but this is fine because we need them to build the app.
# Finally build everything, in particular the TypeScript files. We can't just
# run `yarn run build` because that wouldn't run the postinstall scripts in
# dependencies (for example the sqlite3 native module would not be built). So
# instead we run `yarn install`, which is going to install again all the
# packages (but because it's already done it should be fast), and then run the
# postinstall scripts, as well as build scripts.
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
&& yarn cache clean \
&& rm -rf .yarn/berry
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds
# Call the command directly, without going via npm:
# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#cmd
WORKDIR "/home/$user/packages/server"
CMD [ "node", "dist/app.js" ]
ENV RUNNING_IN_DOCKER=1
EXPOSE ${APP_PORT}
# Not clear what's the equivalent of "--prefix" in Yarn 3, so keep using npm for
# now.
CMD [ "npm", "--prefix", "packages/server", "start" ]
# Build-time metadata
# https://github.com/opencontainers/image-spec/blob/master/annotations.md

View File

@@ -300,6 +300,7 @@ To add a **Bucket Policy** from the AWS S3 Web Console, navigate to the **Permis
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation",

View File

@@ -78,8 +78,5 @@
"node-gyp": "^8.4.1",
"nodemon": "^2.0.9"
},
"packageManager": "yarn@3.1.1",
"resolutions": {
"markdown-it-multimd-table@4.1.1": "patch:markdown-it-multimd-table@npm:4.1.1#.yarn/patches/markdown-it-multimd-table-npm-4.1.1-47e334d4bd"
}
"packageManager": "yarn@3.1.1"
}

View File

@@ -446,9 +446,8 @@ class Application extends BaseApplication {
await this.checkForLegacyTemplates();
// Note: Auto-update is a misnomer in the code.
// The code below only checks, if a new version is available.
// We only allow Windows and macOS users to automatically check for updates
// Note: Auto-update currently doesn't work in Linux: it downloads the update
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {

View File

@@ -56,7 +56,7 @@ class ClipperConfigScreenComponent extends React.Component {
const containerStyle = Object.assign({}, theme.containerStyle, {
overflowY: 'scroll',
// padding: theme.configScreenPadding,
padding: theme.configScreenPadding,
backgroundColor: theme.backgroundColor3,
});

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useCallback, useState, useRef, useEffect } from 'react';
import { useCallback, useState } from 'react';
import { _ } from '@joplin/lib/locale';
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
import Dialog from '../Dialog';
@@ -22,7 +22,6 @@ interface Props {
export default function(props: Props) {
const [folderTitle, setFolderTitle] = useState('');
const [folderIcon, setFolderIcon] = useState<FolderIcon>();
const titleInputRef = useRef(null);
const isNew = !props.folderId;
@@ -42,14 +41,6 @@ export default function(props: Props) {
});
}, [props.dispatch]);
useEffect(() => {
titleInputRef.current.focus();
setTimeout(() => {
titleInputRef.current.select();
}, 100);
}, []);
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
if (event.buttonName === 'cancel') {
onClose();
@@ -99,7 +90,7 @@ export default function(props: Props) {
<div className="form">
<div className="form-input-group">
<label>{_('Title')}</label>
<StyledInput type="text" ref={titleInputRef} value={folderTitle} onChange={onFolderTitleChange}/>
<StyledInput type="text" value={folderTitle} onChange={onFolderTitleChange}/>
</div>
<div className="form-input-group">

View File

@@ -5,7 +5,7 @@ export default function styles(themeId: number) {
return {
container: {
...theme.containerStyle,
// padding: theme.configScreenPadding,
padding: theme.configScreenPadding,
backgroundColor: theme.backgroundColor3,
},
actionsContainer: {

View File

@@ -67,7 +67,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
usePluginServiceRegistration(ref);
const { resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll, editor_resize, getLineScrollPercent,
const { resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll, editor_resize,
} = useScrollHandler(editorRef, webviewRef, props.onScroll);
const codeMirror_change = useCallback((newBody: string) => {
@@ -576,14 +576,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const arg0 = args && args.length >= 1 ? args[0] : null;
if (msg.indexOf('checkboxclick:') === 0) {
const { line, from, to } = shared.toggleCheckboxRange(msg, props.content);
const newBody = shared.toggleCheckbox(msg, props.content);
if (editorRef.current) {
// To cancel CodeMirror's layout drift, the scroll position
// is recorded before updated, and then it is restored.
// Ref. https://github.com/laurent22/joplin/issues/5890
const percent = getLineScrollPercent();
editorRef.current.replaceRange(line, from, to);
setEditorPercentScroll(percent);
editorRef.current.updateBody(newBody);
}
} else if (msg === 'percentScroll') {
const percent = arg0;

View File

@@ -7,7 +7,6 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
const ignoreNextEditorScrollTime_ = useRef(Date.now());
const ignoreNextEditorScrollEventCount_ = useRef(0);
const delayedSetEditorPercentScrollTimeoutID_ = useRef(null);
const lastResizeHeight_ = useRef(NaN);
// Ignores one next scroll event for a short time.
const ignoreNextEditorScrollEvent = () => {
@@ -91,7 +90,8 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
}, [scheduleOnScroll]);
const editor_scroll = useCallback(() => {
const ignored = isNextEditorScrollEventIgnored();
if (isNextEditorScrollEventIgnored()) return;
const cm = editorRef.current;
if (isCodeMirrorReady(cm)) {
const editorPercent = Math.max(0, Math.min(1, cm.getScrollPercent()));
@@ -104,9 +104,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
// calculates GUI-independent line-based percent
const percent = translateScrollPercentE2L(cm, editorPercent);
scrollPercent_.current = percent;
if (!ignored) {
setViewerPercentScroll(percent);
}
setViewerPercentScroll(percent);
}
}
}, [setViewerPercentScroll]);
@@ -119,29 +117,13 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
}, []);
const editor_resize = useCallback((cm) => {
if (isCodeMirrorReady(cm)) {
// Only when resized, the scroll position is restored.
const info = cm.getScrollInfo();
const height = info.height - info.clientHeight;
if (height !== lastResizeHeight_.current) {
restoreEditorPercentScroll();
lastResizeHeight_.current = height;
}
}
}, []);
const getLineScrollPercent = useCallback(() => {
const cm = editorRef.current;
if (isCodeMirrorReady(cm)) {
const ePercent = cm.getScrollPercent();
return translateScrollPercentE2L(cm, ePercent);
} else {
return scrollPercent_.current;
if (cm) {
restoreEditorPercentScroll();
}
}, []);
return {
resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll, editor_resize, getLineScrollPercent,
resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll, editor_resize,
};
}

View File

@@ -15,7 +15,6 @@ export const Root = styled.div`
position: relative;
display: flex;
width: 100%;
min-width: 30px;
`;
interface Props {

View File

@@ -1,15 +1,14 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const CommandService = require('@joplin/lib/services/CommandService').default;
class TagItemComponent extends React.Component {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.tagStyle);
const { title, id } = this.props;
const title = this.props.title;
return <button style={style} onClick={() => CommandService.instance().execute('openTag', id)}>{title}</button>;
return <span style={style}>{title}</span>;
}
}

View File

@@ -42,7 +42,6 @@ function TagList(props: Props) {
for (let i = 0; i < tags.length; i++) {
const props = {
title: tags[i].title,
id: tags[i].id,
key: tags[i].id,
};
output.push(<TagItem {...props} />);

View File

@@ -151,7 +151,7 @@ General classes
body, button {
color: var(--joplin-color);
font-size: 13px;
font-size: 16px;
}
div, span, a {
@@ -159,7 +159,7 @@ div, span, a {
}
h2 {
font-size: 20px;
font-size: 24px;
&.-no-top-margin {
margin-top: 0;
@@ -193,7 +193,7 @@ div.form,
p {
&.-small {
font-size: 11px;
font-size: 13px;
}
}

View File

@@ -10,12 +10,6 @@
# ./runForTesting.sh 1 createUsers,createData,reset,e2ee,sync && ./runForTesting.sh 2 reset,e2ee,sync && ./runForTesting.sh 1
# ----------------------------------------------------------------------------------
# First user has E2EE, but second one doesn't:
# ----------------------------------------------------------------------------------
# ./runForTesting.sh 1 createUsers,createData,reset,e2ee,sync && ./runForTesting.sh 2 reset,sync && ./runForTesting.sh 1
# ----------------------------------------------------------------------------------
# Without E2EE:
# ----------------------------------------------------------------------------------

View File

@@ -10,9 +10,6 @@
"keywords": [
"joplin-plugin"
],
"files": [
"publish"
],
"devDependencies": {
"@types/node": "^14.0.14",
"chalk": "^4.1.0",

View File

@@ -98,14 +98,14 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
// We could implement that here, but the above workaround saves some code.
static async checkConfig(options) {
const fileApi = await SyncTargetAmazonS3.newFileApi_(SyncTargetAmazonS3.id(), options);
fileApi.requestRepeatCount_ = 0;
const output = {
ok: false,
errorMessage: '',
};
try {
const fileApi = await SyncTargetAmazonS3.newFileApi_(SyncTargetAmazonS3.id(), options);
fileApi.requestRepeatCount_ = 0;
const headBucketReq = new Promise((resolve, reject) => {
fileApi.driver().api().send(

View File

@@ -35,7 +35,7 @@ export const runtime = (): CommandRuntime => {
return 'auth';
}
reg.logger().error('Not authenticated with sync target - please check your credentials.');
reg.logger().info('Not authentified with sync target - please check your credential.');
return 'error';
}
@@ -43,13 +43,8 @@ export const runtime = (): CommandRuntime => {
try {
sync = await reg.syncTarget().synchronizer();
} catch (error) {
reg.logger().error('Could not initialise synchroniser: ');
reg.logger().error(error);
error.message = `Could not initialise synchroniser: ${error.message}`;
utils.store.dispatch({
type: 'SYNC_REPORT_UPDATE',
report: { errors: [error] },
});
reg.logger().info('Could not acquire synchroniser:');
reg.logger().info(error);
return 'error';
}

View File

@@ -248,7 +248,7 @@ shared.toggleIsTodo_onPress = function(comp) {
comp.setState(newState);
};
function toggleCheckboxLine(ipcMessage, noteBody) {
shared.toggleCheckbox = function(ipcMessage, noteBody) {
const newBody = noteBody.split('\n');
const p = ipcMessage.split(':');
const lineIndex = Number(p[p.length - 1]);
@@ -281,18 +281,7 @@ function toggleCheckboxLine(ipcMessage, noteBody) {
} else {
line = line.replace(/- \[x\] /i, '- [ ] ');
}
return [newBody, lineIndex, line];
}
shared.toggleCheckboxRange = function(ipcMessage, noteBody) {
const [lineIndex, line] = toggleCheckboxLine(ipcMessage, noteBody).slice(1);
const from = { line: lineIndex, ch: 0 };
const to = { line: lineIndex, ch: line.length };
return { line, from, to };
};
shared.toggleCheckbox = function(ipcMessage, noteBody) {
const [newBody, lineIndex, line] = toggleCheckboxLine(ipcMessage, noteBody);
newBody[lineIndex] = line;
return newBody.join('\n');
};

View File

@@ -105,7 +105,7 @@ shared.synchronize_press = async function(comp) {
return 'auth';
}
reg.logger().error('Not authenticated with sync target - please check your credentials.');
reg.logger().info('Not authentified with sync target - please check your credential.');
return 'error';
}
@@ -113,13 +113,8 @@ shared.synchronize_press = async function(comp) {
try {
sync = await reg.syncTarget().synchronizer();
} catch (error) {
reg.logger().error('Could not initialise synchroniser: ');
reg.logger().error(error);
error.message = `Could not initialise synchroniser: ${error.message}`;
comp.props.dispatch({
type: 'SYNC_REPORT_UPDATE',
report: { errors: [error] },
});
reg.logger().info('Could not acquire synchroniser:');
reg.logger().info(error);
return 'error';
}

View File

@@ -1190,7 +1190,7 @@ class Setting extends BaseModel {
},
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically update the application') },
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
'sync.interval': {

View File

@@ -89,7 +89,6 @@ export default class OneDriveApi {
scope: 'files.readwrite offline_access sites.readwrite.all',
response_type: 'code',
redirect_uri: redirectUri,
prompt: 'login',
};
return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${stringify(query)}`;
}

View File

@@ -17,7 +17,7 @@ import { Command } from './types';
*
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts)
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts)
*
* To view what arguments are supported, you can open any of these files
* and look at the `execute()` command.

View File

@@ -227,7 +227,6 @@ function addExtraStyles(style: any) {
justifyContent: 'center',
marginRight: 8,
borderRadius: 100,
borderWidth: 0,
};
style.toolbarStyle = {

View File

@@ -1,116 +0,0 @@
# Individual Contributor License Agreement
Thank you for your interest in Joplin Server, owned by Cozic Ltd (the
"Company"). In order to clarify the intellectual property license granted with
Contributions from any person or entity, the Company must have a Contributor
License Agreement ("CLA") on file that has been signed by each Contributor,
indicating agreement to the license terms below. This license is for your
protection as a Contributor as well as the protection of the Company and its
users; it does not change your rights to use your own Contributions for any
other purpose.
Please complete and sign this Agreement, and then email a copy to
cla@joplinapp.org only (do not copy any other persons or lists). Read this
document carefully before signing and keep a copy for your records.
- Full name: **FULL NAME**
- Postal Address: **POSTAL ADDRESS**
- Country: **COUNTRY**
- E-Mail: **EMAIL**
- GitHub username: **GITHUB USERNAME**
You accept and agree to the following terms and conditions for Your present and
future Contributions submitted to the Company. In return, the Company shall not
use Your Contributions in a way that is contrary to the public benefit or
inconsistent with its bylaws in effect at the time of the Contribution. Except
for the license granted herein to the Company and recipients of software
distributed by the Company, You reserve all right, title, and interest in and to
Your Contributions.
1. Definitions.
* "You" (or "Your")
"You" (or "Your") shall mean the copyright owner or legal entity authorized
by the copyright owner that is making this Agreement with the Company. For
legal entities, the entity making a Contribution and all other entities
that control, are controlled by, or are under common control with that
entity are considered to be a single Contributor. For the purposes of this
definition, "control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or otherwise,
or (ii) ownership of fifty percent (50%) or more of the outstanding shares,
or (iii) beneficial ownership of such entity.
* "Contribution"
"Contribution" shall mean any original work of authorship, including any
modifications or additions to an existing work, that is intentionally
submitted by You to the Company for inclusion in, or documentation of, any
of the products owned or managed by the Company (the "Work"). For the
purposes of this definition, "submitted" means any form of electronic,
verbal, or written communication sent to the Company or its
representatives, including but not limited to communication on electronic
mailing lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Company for the purpose of discussing
and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by You as "Not a Contribution."
2. Grant of Copyright License. Subject to the terms and conditions of this
Agreement, You hereby grant to the Company and to recipients of software
distributed by the Company a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare derivative
works of, publicly display, publicly perform, sublicense, and distribute Your
Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this
Agreement, You hereby grant to the Company and to recipients of software
distributed by the Company a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this section) patent license
to make, have made, use, offer to sell, sell, import, and otherwise transfer
the Work, where such license applies only to those patent claims licensable
by You that are necessarily infringed by Your Contribution(s) alone or by
combination of Your Contribution(s) with the Work to which such
Contribution(s) was submitted. If any entity institutes patent litigation
against You or any other entity (including a cross-claim or counterclaim in a
lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then any
patent licenses granted to that entity under this Agreement for that
Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that you are legally entitled to grant the above license. If
your employer(s) has rights to intellectual property that you create that
includes your Contributions, you represent that you have received permission
to make Contributions on behalf of that employer, that your employer has
waived such rights for your Contributions to the Company, or that your
employer has executed a separate Corporate CLA with the Company.
5. You represent that each of Your Contributions is Your original creation (see
section 7 for submissions on behalf of others). You represent that Your
Contribution submissions include complete details of any third-party license
or other restriction (including, but not limited to, related patents and
trademarks) of which you are personally aware and which are associated with
any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the
extent You desire to provide support. You may provide support for free, for a
fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may
submit it to the Company separately from any Contribution, identifying the
complete details of its source and of any license or other restriction
(including, but not limited to, related patents, trademarks, and license
agreements) of which you are personally aware, and conspicuously marking the
work as "Submitted on behalf of a third-party: **NAME HERE**".
8. You agree to notify the Company of any facts or circumstances of which you
become aware that would make these representations inaccurate in any respect.
Please sign: **SIGNATURE** Date: **DATE**

View File

@@ -24,7 +24,6 @@ import { RouteResponseFormat, routeResponseFormat } from './utils/routeUtils';
import { parseEnv } from './env';
import storageConnectionCheck from './utils/storageConnectionCheck';
import { setLocale } from '@joplin/lib/locale';
import checkAdminHandler from './middleware/checkAdminHandler';
interface Argv {
env?: Env;
@@ -197,7 +196,6 @@ async function main() {
app.use(apiVersionHandler);
app.use(ownerHandler);
app.use(checkAdminHandler);
app.use(notificationHandler);
app.use(clickJackingHandler);
app.use(routeHandler);

View File

@@ -66,7 +66,7 @@ function mailerConfigFromEnv(env: EnvVariables): MailerConfig {
enabled: env.MAILER_ENABLED,
host: env.MAILER_HOST,
port: env.MAILER_PORT,
security: env.MAILER_SECURITY,
secure: env.MAILER_SECURE,
authUser: env.MAILER_AUTH_USER,
authPassword: env.MAILER_AUTH_PASSWORD,
noReplyName: env.MAILER_NOREPLY_NAME,

View File

@@ -1,46 +0,0 @@
import { afterAllTests, beforeAllDb, beforeEachDb, expectThrow } from './utils/testing/testUtils';
import { parseEnv } from './env';
describe('env', function() {
beforeAll(async () => {
await beforeAllDb('env');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
it('should parse env values', async function() {
const result = parseEnv({
DB_CLIENT: 'pg',
POSTGRES_PORT: '123',
MAILER_ENABLED: 'true',
SIGNUP_ENABLED: 'false',
TERMS_ENABLED: '0',
ACCOUNT_TYPES_ENABLED: '1',
});
expect(result.DB_CLIENT).toBe('pg');
expect(result.POSTGRES_PORT).toBe(123);
expect(result.MAILER_ENABLED).toBe(true);
expect(result.SIGNUP_ENABLED).toBe(false);
expect(result.TERMS_ENABLED).toBe(false);
expect(result.ACCOUNT_TYPES_ENABLED).toBe(true);
});
it('should overrides default values', async function() {
expect(parseEnv({}).POSTGRES_USER).toBe('joplin');
expect(parseEnv({}, { POSTGRES_USER: 'other' }).POSTGRES_USER).toBe('other');
});
it('should validate values', async function() {
await expectThrow(async () => parseEnv({ POSTGRES_PORT: 'notanumber' }));
await expectThrow(async () => parseEnv({ MAILER_ENABLED: 'TRUE' }));
});
});

View File

@@ -1,13 +1,7 @@
// The possible env variables and their defaults are listed below.
//
// The env variables can be of type string, integer or boolean. When the type is
// boolean, set the variable to "true", "false", "0" or "1" in your env file.
export enum MailerSecurity {
None = 'none',
Tls = 'tls',
Starttls = 'starttls',
}
// boolean, set the variable to "0" or "1" in your env file.
const defaultEnvValues: EnvVariables = {
// ==================================================
@@ -72,8 +66,8 @@ const defaultEnvValues: EnvVariables = {
MAILER_ENABLED: false,
MAILER_HOST: '',
MAILER_PORT: 465,
MAILER_SECURITY: MailerSecurity.Tls,
MAILER_PORT: 587,
MAILER_SECURE: true,
MAILER_AUTH_USER: '',
MAILER_AUTH_PASSWORD: '',
MAILER_NOREPLY_NAME: '',
@@ -126,7 +120,7 @@ export interface EnvVariables {
MAILER_ENABLED: boolean;
MAILER_HOST: string;
MAILER_PORT: number;
MAILER_SECURITY: MailerSecurity;
MAILER_SECURE: boolean;
MAILER_AUTH_USER: string;
MAILER_AUTH_PASSWORD: string;
MAILER_NOREPLY_NAME: string;
@@ -140,13 +134,7 @@ export interface EnvVariables {
STRIPE_WEBHOOK_SECRET: string;
}
const parseBoolean = (s: string): boolean => {
if (s === 'true' || s === '1') return true;
if (s === 'false' || s === '0') return false;
throw new Error(`Invalid boolean value: "${s}" (Must be one of "true", "false", "0, "1")`);
};
export function parseEnv(rawEnv: Record<string, string>, defaultOverrides: any = null): EnvVariables {
export function parseEnv(rawEnv: any, defaultOverrides: any = null): EnvVariables {
const output: EnvVariables = {
...defaultEnvValues,
...defaultOverrides,
@@ -157,21 +145,17 @@ export function parseEnv(rawEnv: Record<string, string>, defaultOverrides: any =
if (rawEnvValue === undefined) continue;
try {
if (typeof value === 'number') {
const v = Number(rawEnvValue);
if (isNaN(v)) throw new Error(`Invalid number value "${rawEnvValue}"`);
(output as any)[key] = v;
} else if (typeof value === 'boolean') {
(output as any)[key] = parseBoolean(rawEnvValue);
} else if (typeof value === 'string') {
(output as any)[key] = `${rawEnvValue}`;
} else {
throw new Error(`Invalid env default value type: ${typeof value}`);
}
} catch (error) {
error.message = `Could not parse key "${key}": ${error.message}`;
throw error;
if (typeof value === 'number') {
const v = Number(rawEnvValue);
if (isNaN(v)) throw new Error(`Invalid number value for env variable ${key} = ${rawEnvValue}`);
(output as any)[key] = v;
} else if (typeof value === 'boolean') {
if (rawEnvValue !== '0' && rawEnvValue !== '1') throw new Error(`Invalid boolean value for env variable ${key}: ${rawEnvValue} (Should be either "0" or "1")`);
(output as any)[key] = rawEnvValue === '1';
} else if (typeof value === 'string') {
(output as any)[key] = `${rawEnvValue}`;
} else {
throw new Error(`Invalid env default value type: ${typeof value}`);
}
}

View File

@@ -1,58 +0,0 @@
import { ErrorForbidden } from '../utils/errors';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError, createUserAndSession } from '../utils/testing/testUtils';
import checkAdminHandler from './checkAdminHandler';
describe('checkAdminHandler', function() {
beforeAll(async () => {
await beforeAllDb('checkAdminHandler');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should access /admin if the user is admin', async function() {
const { session } = await createUserAndSession(1, true);
const context = await koaAppContext({
sessionId: session.id,
request: {
method: 'GET',
url: '/admin/organizations',
},
});
await expectNotThrow(async () => checkAdminHandler(context, koaNext));
});
test('should not access /admin if the user is not admin', async function() {
const { session } = await createUserAndSession(1);
const context = await koaAppContext({
sessionId: session.id,
request: {
method: 'GET',
url: '/admin/organizations',
},
});
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
});
test('should not access /admin if the user is not logged in', async function() {
const context = await koaAppContext({
request: {
method: 'GET',
url: '/admin/organizations',
},
});
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
});
});

View File

@@ -1,12 +0,0 @@
import { AppContext, KoaNext } from '../utils/types';
import { isAdminRequest } from '../utils/requestUtils';
import { ErrorForbidden } from '../utils/errors';
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
if (isAdminRequest(ctx)) {
if (!ctx.joplin.owner) throw new ErrorForbidden();
if (!ctx.joplin.owner.is_admin) throw new ErrorForbidden();
}
return next();
}

View File

@@ -92,8 +92,7 @@ export default class NotificationModel extends BaseModel<Notification> {
return this.add(userId, NotificationKey.Any, NotificationLevel.Normal, message);
}
public async addError(userId: Uuid, error: string | Error) {
const message = typeof error === 'string' ? error : error.message;
public async addError(userId: Uuid, message: string) {
return this.add(userId, NotificationKey.Any, NotificationLevel.Error, message);
}

View File

@@ -34,7 +34,6 @@ router.get('api/users/:id/public_key', async (path: SubPath, ctx: AppContext) =>
if (!user) return ''; // Don't throw an error to prevent polling the end point
const ppk = await ctx.joplin.models.user().publicPrivateKey(user.id);
if (!ppk) return '';
return {
id: ppk.id,

View File

@@ -132,7 +132,7 @@ router.get('tasks', async (_path: SubPath, ctx: AppContext) => {
postUrl: makeUrl(UrlType.Tasks),
csrfTag: await createCsrfTag(ctx),
},
// cssFiles: ['index/tasks'],
cssFiles: ['index/tasks'],
};
});

View File

@@ -7,7 +7,6 @@ import { Email, EmailSender } from '../services/database/types';
import { errorToString } from '../utils/errors';
import EmailModel from '../models/EmailModel';
import { markdownBodyToHtml, markdownBodyToPlainText } from './email/utils';
import { MailerSecurity } from '../env';
const logger = Logger.create('EmailService');
@@ -26,16 +25,10 @@ export default class EmailService extends BaseService {
if (!this.senderInfo(EmailSender.NoReply).email) {
throw new Error('No-reply email must be set for email service to work (Set env variable MAILER_NOREPLY_EMAIL)');
}
// NodeMailer's TLS options are weird:
// https://nodemailer.com/smtp/#tls-options
const options: SMTPTransport.Options = {
host: this.config.mailer.host,
port: this.config.mailer.port,
secure: this.config.mailer.security === MailerSecurity.Tls,
ignoreTLS: this.config.mailer.security === MailerSecurity.None,
requireTLS: this.config.mailer.security === MailerSecurity.Starttls,
secure: this.config.mailer.secure,
};
if (this.config.mailer.authUser || this.config.mailer.authPassword) {
options.auth = {

View File

@@ -27,7 +27,6 @@ export interface View {
partials?: string[];
cssFiles?: string[];
jsFiles?: string[];
strings?: Record<string, string>; // List of translatable strings
}
interface GlobalParams {
@@ -47,7 +46,6 @@ interface GlobalParams {
isJoplinCloud?: boolean;
impersonatorAdminSessionId?: string;
csrfTag?: string;
s?: Record<string, string>; // List of translatable strings
}
export function isView(o: any): boolean {
@@ -188,6 +186,12 @@ export default class MustacheService {
...this.defaultLayoutOptions,
...globalParams,
userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null),
};
const contentHtml = await this.renderFileContent(filePath, view, globalParams);
const layoutView: any = {
global: globalParams,
s: {
home: _('Home'),
users: _('Users'),
@@ -197,12 +201,6 @@ export default class MustacheService {
help: _('Help'),
logout: _('Logout'),
},
};
const contentHtml = await this.renderFileContent(filePath, view, globalParams);
const layoutView: any = {
global: globalParams,
pageName: view.name,
pageTitle: view.titleOverride ? view.title : `${config().appName} - ${view.title}`,
contentHtml: contentHtml,

View File

@@ -71,10 +71,6 @@ export function isApiRequest(ctx: AppContext): boolean {
return ctx.path.indexOf('/api/') === 0;
}
export function isAdminRequest(ctx: AppContext): boolean {
return ctx.path.indexOf('/admin/') === 0;
}
export function userIp(ctx: AppContext): string {
if (ctx.headers['x-real-ip']) return ctx.headers['x-real-ip'];
return ctx.ip;

View File

@@ -1,4 +1,4 @@
import { findMatchingRoute, isValidOrigin, parseSubPath, splitItemPath } from './routeUtils';
import { isValidOrigin, parseSubPath, splitItemPath } from './routeUtils';
import { ItemAddressingType } from '../services/database/types';
import { RouteType } from './types';
@@ -26,58 +26,6 @@ describe('routeUtils', function() {
}
});
it('should find a matching route', async function() {
const testCases: any[] = [
['/admin/organizations', {
route: 1,
basePath: 'admin/organizations',
subPath: {
id: '',
link: '',
addressingType: 1,
raw: '',
schema: 'admin/organizations',
},
}],
['/api/users/123', {
route: 2,
basePath: 'api/users',
subPath: {
id: '123',
link: '',
addressingType: 1,
raw: '123',
schema: 'api/users/:id',
},
}],
['/help', {
route: 3,
basePath: 'help',
subPath: {
id: '',
link: '',
addressingType: 1,
raw: '',
schema: 'help',
},
}],
];
const routes: Record<string, any> = {
'admin/organizations': 1,
'api/users': 2,
'help': 3,
};
for (const testCase of testCases) {
const [path, expected] = testCase;
const actual = findMatchingRoute(path, routes);
expect(actual).toEqual(expected);
}
});
it('should split an item path', async function() {
const testCases: any[] = [
['root:/Documents/MyFile.md:', ['root', 'Documents', 'MyFile.md']],

View File

@@ -7,7 +7,6 @@ import { Account } from '../models/UserModel';
import { Services } from '../services/types';
import { Routers } from './routeUtils';
import { DbConnection } from '../db';
import { MailerSecurity } from '../env';
export enum Env {
Dev = 'dev',
@@ -75,7 +74,7 @@ export interface MailerConfig {
enabled: boolean;
host: string;
port: number;
security: MailerSecurity;
secure: boolean;
authUser: string;
authPassword: string;
noReplyName: string;

View File

@@ -10,27 +10,27 @@
{{#global.owner}}
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="{{{global.baseUrl}}}/home">{{global.s.home}}</a>
<a class="navbar-item" href="{{{global.baseUrl}}}/home">{{s.home}}</a>
{{#global.owner.is_admin}}
<a class="navbar-item" href="{{{global.baseUrl}}}/users">{{global.s.users}}</a>
<a class="navbar-item" href="{{{global.baseUrl}}}/users">{{s.users}}</a>
{{/global.owner.is_admin}}
{{#global.owner.is_admin}}
<a class="navbar-item" href="{{{global.baseUrl}}}/items">{{global.s.items}}</a>
<a class="navbar-item" href="{{{global.baseUrl}}}/items">{{s.items}}</a>
{{/global.owner.is_admin}}
{{#global.owner.is_admin}}
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">{{global.s.log}}</a>
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">{{s.log}}</a>
{{/global.owner.is_admin}}
{{#global.owner.is_admin}}
<a class="navbar-item" href="{{{global.baseUrl}}}/tasks">{{global.s.tasks}}</a>
<a class="navbar-item" href="{{{global.baseUrl}}}/tasks">{{s.tasks}}</a>
{{/global.owner.is_admin}}
</div>
<div class="navbar-end">
{{#global.isJoplinCloud}}
<a class="navbar-item" href="{{{global.baseUrl}}}/help">{{global.s.help}}</a>
<a class="navbar-item" href="{{{global.baseUrl}}}/help">{{s.help}}</a>
{{/global.isJoplinCloud}}
<div class="navbar-item">
<form method="post" action="{{{global.baseUrl}}}/logout">
<button class="button is-dark">{{global.s.logout}}</button>
<button class="button is-dark">{{s.logout}}</button>
</form>
</div>
<div class="navbar-item">

View File

@@ -347,7 +347,7 @@ function flagImageUrl(locale) {
if (locale === 'sv') return `${baseUrl}/country-4x3/se.png`;
if (locale === 'nb_NO') return `${baseUrl}/country-4x3/no.png`;
if (locale === 'ro') return `${baseUrl}/country-4x3/ro.png`;
if (locale === 'vi') return `${baseUrl}/country-4x3/vn.png`;
if (locale === 'vi') return `${baseUrl}/country-4x3/vi.png`;
if (locale === 'fa') return `${baseUrl}/country-4x3/ir.png`;
if (locale === 'eo') return `${baseUrl}/esperanto.png`;
return `${baseUrl}/country-4x3/${countryCodeOnly(locale).toLowerCase()}.png`;

View File

@@ -19,7 +19,6 @@ async function main() {
const argv = require('yargs').argv;
if (!argv.tagName) throw new Error('--tag-name not provided');
const dryRun = !!argv.dryRun;
const pushImages = !!argv.pushImages;
const tagName = argv.tagName;
const isPreRelease = getIsPreRelease(tagName);
@@ -48,13 +47,7 @@ async function main() {
console.info('isPreRelease:', isPreRelease);
console.info('Docker tags:', dockerTags.join(', '));
const dockerCommand = `docker build --progress=plain -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`;
if (dryRun) {
console.info(dockerCommand);
return;
}
await execCommand2(dockerCommand);
await execCommand2(`docker build --progress=plain -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`);
for (const tag of dockerTags) {
await execCommand2(`docker tag "joplin/server:${imageVersion}" "joplin/server:${tag}"`);

View File

@@ -305,7 +305,7 @@ msgstr "Utseende"
#: packages/lib/models/Setting.ts:2146
msgid "Application"
msgstr "Program"
msgstr "Avslutar programmet"
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.tsx:33
msgid "Apply"
@@ -560,7 +560,7 @@ msgstr ""
#: packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.ts:7
msgid "Change application layout"
msgstr "Ändra programmets layout"
msgstr "Ändra applikationslayout"
#: packages/lib/services/spellChecker/SpellCheckerService.ts:189
msgid "Change language"
@@ -1001,7 +1001,7 @@ msgstr "Ta bort rad"
#: packages/lib/models/Setting.ts:1186
msgid "Delete local data and re-download from sync target"
msgstr "Ta bort lokala data och hämta igen från synkroniseringsmålet"
msgstr "Ta bort lokal data och hämta igen från synkroniseringsmålet"
#: packages/lib/models/Note.ts:760
msgid "Delete note \"%s\"?"
@@ -1576,7 +1576,7 @@ msgid ""
"Fail-safe: Do not wipe out local data when sync target is empty (often the "
"result of a misconfiguration or bug)"
msgstr ""
"Felsäkert: Rensa inte lokala data när synkroniseringsmålet är tomt (beror "
"Felsäkert: Rensa inte lokal data när synkroniseringsmålet är tomt (beror "
"oftast på felkonfigurering eller en bugg)"
#: packages/app-cli/app/main.js:95
@@ -1888,7 +1888,7 @@ msgstr ""
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:142
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:129
msgid "In order to use the web clipper, you need to do the following:"
msgstr "För att kunna använda Web Clipper måste du göra följande:"
msgstr "För att kunna använda web Clipper måste du göra följande:"
#: packages/lib/Synchronizer.ts:305
msgid "In progress"
@@ -2894,7 +2894,7 @@ msgstr "Omkryptering"
#: packages/lib/models/Setting.ts:1175
msgid "Re-upload local data to sync target"
msgstr "Ladda upp lokala data igen till synkroniseringsmålet"
msgstr "Ladda upp lokal data igen för synkroniseringsmålet"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.tsx:439
msgid "Read more about it"
@@ -3584,7 +3584,7 @@ msgstr "Programmet har godkänts."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx:619
msgid "The application must be restarted for these changes to take effect."
msgstr ""
"Programmet måste startas om för att dessa ändringar ska träda i kraft."
"Applikationen måste startas om för att dessa ändringar ska träda i kraft."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.tsx:511
msgid ""
@@ -3611,13 +3611,13 @@ msgid ""
"is recommended that you apply it to your data."
msgstr ""
"Standard krypteringsmetod har ändrats till en säkrare och det är "
"rekommenderat att du tillämpar den på dina data."
"rekommenderat att du tillämpar den på din data."
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:603
msgid ""
"The default encryption method has been changed, you should re-encrypt your "
"data."
msgstr "Standardkrypteringsmetoden har ändrats, du bör omkryptera dina data."
msgstr "Standardkrypteringsmetoden har ändrats, du bör omkryptera din data."
#: packages/lib/models/Setting.ts:1228
msgid ""
@@ -3756,7 +3756,7 @@ msgstr "Web Clipper behöver din behörighet för att få åtkomst till dina dat
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:84
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:83
msgid "The web clipper service is enabled and set to auto-start."
msgstr "Web Clipper-tjänsten är aktiverad och inställd för automatisk start."
msgstr "Web clipper-tjänsten är aktiverad och inställd för automatisk start."
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:109
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:107
@@ -3782,7 +3782,7 @@ msgstr ""
#: packages/lib/services/interop/InteropService_Exporter_Jex.ts:35
msgid "There is no data to export."
msgstr "Det finns inga data att exportera."
msgstr "Det finns ingen data att exportera."
#: packages/lib/models/Resource.ts:412
msgid ""
@@ -4387,7 +4387,7 @@ msgid ""
"You may use the tool below to re-encrypt your data, for example if you know "
"that some of your notes are encrypted with an obsolete encryption method."
msgstr ""
"Du kan använda verktyget nedan för att omkryptera dina data, exempelvis om du "
"Du kan använda verktyget nedan för att omkryptera din data, exempelvis om du "
"vill veta om vissa av dina anteckningar är krypterade med en gammal "
"krypteringsmetod."
@@ -4397,7 +4397,7 @@ msgstr "Ditt val: "
#: packages/lib/components/EncryptionConfigScreen/utils.ts:71
msgid "Your data is going to be re-encrypted and synced again."
msgstr "Dina data kommer att omkrypteras och synkroniseras igen."
msgstr "Din data kommer att omkrypteras och synkroniseras igen."
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:271
msgid "Your master password is needed to decrypt some of your data."

View File

@@ -167,10 +167,6 @@ function makeHomePageMd() {
// while MarkdownIt doesn't and will in fact display the \. So we remove it here.
md = md.replace(/\\\| bash/g, '| bash');
// We strip-off the donate links because they are added back (with proper
// classes and CSS).
md = md.replace(donateLinksRegex_, '');
return md;
}
@@ -216,19 +212,14 @@ async function main() {
const assetUrls = await getAssetUrls();
const readmeMd = makeHomePageMd();
const donateLinksMd = await getDonateLinks();
// await updateDownloadPage(readmeMd);
// =============================================================
// HELP PAGE
// =============================================================
renderPageToHtml(readmeMd, `${docDir}/help/index.html`, {
sourceMarkdownFile: 'README.md',
donateLinksMd,
partials,
sponsors,
assetUrls,
});
renderPageToHtml(readmeMd, `${docDir}/help/index.html`, { sourceMarkdownFile: 'README.md', partials, sponsors, assetUrls });
// =============================================================
// FRONT PAGE
@@ -288,6 +279,7 @@ async function main() {
const mdFiles = glob.sync(`${readmeDir}/**/*.md`).map((f: string) => f.substr(rootDir.length + 1));
const sources = [];
const donateLinksMd = await getDonateLinks();
const makeTargetFilePath = (input: string): string => {
if (isNewsFile(input)) {

View File

@@ -121,35 +121,6 @@ Difficulty Level: Medium
Skills Required: React, Typescript, CSS.
## 11. Improve plugin search and discoverability
As there are more and more plugins it would be good to improve how they are discovered, and to improve search - in particular improve search relevance. We are open to hear ideas about this, but a few things that could be done, for example are:
- Improve the [page that lists all the plugin](https://github.com/joplin/plugins#readme) by adding a download count (based on stats.json) and make the list sortable by download count.
- In the app, use the info from stats.json to order the plugin - those with more downloads going on top for example
- Create a dynamically generated page (using GitHub Actions) under joplinapp.org that shows some recommended plugins, trending plugins, etc. similar to [Add-ons for Firefox](https://addons.mozilla.org/en-GB/firefox/)
Those are just ideas and we're open to hearing more from you.
Difficulty Level: Medium
Skills Required: Typescript, CSS, GitHub Actions.
## 12. Email plugin
Create a plugin to fetch mail via IMAP and convert messages to notes (including attachments). The plugin should be able to filter what messages it donwloads, e.g. based on the folder.
Additional features to consider:
- support more than one account
- convert HTML to Markdown
- delete/move received emails
Difficulty Level: Medium
Skills Required: TypeScript, JavaScript.
# More info
- Make sure you read the [Joplin Google Summer of Code Introduction](https://joplinapp.org/gsoc2022/index/)