1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

22 Commits

Author SHA1 Message Date
Laurent Cozic
ad34cee234 Desktop release v1.7.2 2021-01-19 23:03:14 +00:00
Laurent Cozic
ddf3e16ff0 Tools: Minor tweaks on desktop release script 2021-01-19 23:02:58 +00:00
Laurent Cozic
4e2e26f033 Desktop release v1.7.1 2021-01-19 22:59:19 +00:00
Laurent Cozic
f37d37e613 Desktop: Allow updating a plugin 2021-01-19 22:58:09 +00:00
Laurent Cozic
63e30f6ccb Plugins: Updated types 2021-01-19 16:48:13 +00:00
Laurent Cozic
9c718baf61 Merge branch 'dev' of github.com:laurent22/joplin into dev 2021-01-18 20:11:17 +00:00
Laurent Cozic
e8ae29adf2 Tools: Clean up and fix tool dependencies 2021-01-18 20:09:11 +00:00
eresytter
0c67805626 All: Translation: Update id_ID.po (#4387) 2021-01-18 14:16:58 -05:00
Laurent Cozic
e91c94baca Added version command 2021-01-18 15:12:21 +00:00
Laurent Cozic
aa74d05b24 Releasing sub-packages 2021-01-18 15:00:00 +00:00
Laurent Cozic
9a1f7f227d Build before publish 2021-01-18 14:59:16 +00:00
Laurent Cozic
40779d09c0 Releasing sub-packages 2021-01-18 14:49:39 +00:00
Laurent Cozic
351d8d94c5 Set version numbers 2021-01-18 14:49:14 +00:00
Laurent Cozic
6addd52ba4 Fix script 2021-01-18 14:48:42 +00:00
Laurent Cozic
ea5849855f Releasing sub-packages 2021-01-18 14:42:40 +00:00
Laurent Cozic
c81529dc45 Fix repo builder package.json 2021-01-18 14:42:23 +00:00
Laurent Cozic
b6d7971691 Set version numbers on sub-packages 2021-01-18 14:40:39 +00:00
Laurent Cozic
446db2d688 Releasing sub-packages 2021-01-18 14:38:23 +00:00
Laurent Cozic
52da072f9a Plugins: Updated plugin repo script 2021-01-18 14:37:27 +00:00
Laurent Cozic
0d2bf6d787 Server: Improved config and support for Docker 2021-01-18 10:13:26 +00:00
Roman Musin
59fe4a2193 Mobile: When attaching a file to a note set correct title and extension (#4373) 2021-01-17 11:40:24 +00:00
Caleb John
df6d146c84 SideBar -> Sidebar in app.ts (#4375) 2021-01-17 11:34:37 +00:00
108 changed files with 9459 additions and 976 deletions

View File

@@ -6,4 +6,5 @@ packages/app-desktop
packages/app-cli
packages/app-mobile
packages/app-clipper
packages/generator-joplin
packages/generator-joplin
packages/plugin-repo-cli

View File

@@ -1,9 +1,26 @@
# Example of local config, for development:
# =============================================================================
# PRODUCTION CONFIG EXAMPLE
# -----------------------------------------------------------------------------
# By default it will use SQLite, but that's mostly to test and evaluate the
# server. So you'll want to specify db connection settings to use Postgres.
# =============================================================================
#
# JOPLIN_BASE_URL=http://localhost:22300
# JOPLIN_PORT=22300
# APP_BASE_URL=https://example.com/joplin
# APP_PORT=22300
#
# DB_CLIENT=pg
# POSTGRES_PASSWORD=joplin
# POSTGRES_DATABASE=joplin
# POSTGRES_USER=joplin
# POSTGRES_PORT=5432
# POSTGRES_HOST=localhost
# Example of config for production:
# =============================================================================
# DEV CONFIG EXAMPLE
# -----------------------------------------------------------------------------
# Example of local config, for development. In dev mode, you would usually use
# SQLite so database settings are not needed.
# =============================================================================
#
# JOPLIN_BASE_URL=https://example.com/joplin
# JOPLIN_PORT=22300
# APP_BASE_URL=http://localhost:22300
# APP_PORT=22300

View File

@@ -111,6 +111,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
packages/app-cli/tests/services/plugins/RepositoryApi.js
packages/app-cli/tests/services/plugins/RepositoryApi.js.map
packages/app-cli/tests/services/plugins/api/JoplinSettings.d.ts
packages/app-cli/tests/services/plugins/api/JoplinSettings.js
packages/app-cli/tests/services/plugins/api/JoplinSettings.js.map
@@ -1371,6 +1374,12 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/plugin-repo-cli/dummy.test.d.ts
packages/plugin-repo-cli/dummy.test.js
packages/plugin-repo-cli/dummy.test.js.map
packages/plugin-repo-cli/index.d.ts
packages/plugin-repo-cli/index.js
packages/plugin-repo-cli/index.js.map
packages/plugins/ToggleSidebars/api/index.d.ts
packages/plugins/ToggleSidebars/api/index.js
packages/plugins/ToggleSidebars/api/index.js.map
@@ -1461,21 +1470,6 @@ packages/renderer/utils.js.map
packages/server/src/app.d.ts
packages/server/src/app.js
packages/server/src/app.js.map
packages/server/src/config-base.d.ts
packages/server/src/config-base.js
packages/server/src/config-base.js.map
packages/server/src/config-buildTypes.d.ts
packages/server/src/config-buildTypes.js
packages/server/src/config-buildTypes.js.map
packages/server/src/config-dev.d.ts
packages/server/src/config-dev.js
packages/server/src/config-dev.js.map
packages/server/src/config-prod.d.ts
packages/server/src/config-prod.js
packages/server/src/config-prod.js.map
packages/server/src/config-tests.d.ts
packages/server/src/config-tests.js
packages/server/src/config-tests.js.map
packages/server/src/config.d.ts
packages/server/src/config.js
packages/server/src/config.js.map
@@ -1686,10 +1680,13 @@ packages/server/src/utils/urlUtils.js.map
packages/server/src/utils/uuidgen.d.ts
packages/server/src/utils/uuidgen.js
packages/server/src/utils/uuidgen.js.map
packages/tools/build-plugin-repository.d.ts
packages/tools/build-plugin-repository.js
packages/tools/build-plugin-repository.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map
packages/tools/release-server.d.ts
packages/tools/release-server.js
packages/tools/release-server.js.map
packages/tools/tool-utils.d.ts
packages/tools/tool-utils.js
packages/tools/tool-utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

33
.gitignore vendored
View File

@@ -99,6 +99,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
packages/app-cli/tests/services/plugins/RepositoryApi.js
packages/app-cli/tests/services/plugins/RepositoryApi.js.map
packages/app-cli/tests/services/plugins/api/JoplinSettings.d.ts
packages/app-cli/tests/services/plugins/api/JoplinSettings.js
packages/app-cli/tests/services/plugins/api/JoplinSettings.js.map
@@ -1359,6 +1362,12 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/plugin-repo-cli/dummy.test.d.ts
packages/plugin-repo-cli/dummy.test.js
packages/plugin-repo-cli/dummy.test.js.map
packages/plugin-repo-cli/index.d.ts
packages/plugin-repo-cli/index.js
packages/plugin-repo-cli/index.js.map
packages/plugins/ToggleSidebars/api/index.d.ts
packages/plugins/ToggleSidebars/api/index.js
packages/plugins/ToggleSidebars/api/index.js.map
@@ -1449,21 +1458,6 @@ packages/renderer/utils.js.map
packages/server/src/app.d.ts
packages/server/src/app.js
packages/server/src/app.js.map
packages/server/src/config-base.d.ts
packages/server/src/config-base.js
packages/server/src/config-base.js.map
packages/server/src/config-buildTypes.d.ts
packages/server/src/config-buildTypes.js
packages/server/src/config-buildTypes.js.map
packages/server/src/config-dev.d.ts
packages/server/src/config-dev.js
packages/server/src/config-dev.js.map
packages/server/src/config-prod.d.ts
packages/server/src/config-prod.js
packages/server/src/config-prod.js.map
packages/server/src/config-tests.d.ts
packages/server/src/config-tests.js
packages/server/src/config-tests.js.map
packages/server/src/config.d.ts
packages/server/src/config.js
packages/server/src/config.js.map
@@ -1674,10 +1668,13 @@ packages/server/src/utils/urlUtils.js.map
packages/server/src/utils/uuidgen.d.ts
packages/server/src/utils/uuidgen.js
packages/server/src/utils/uuidgen.js.map
packages/tools/build-plugin-repository.d.ts
packages/tools/build-plugin-repository.js
packages/tools/build-plugin-repository.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map
packages/tools/release-server.d.ts
packages/tools/release-server.js
packages/tools/release-server.js.map
packages/tools/tool-utils.d.ts
packages/tools/tool-utils.js
packages/tools/tool-utils.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -1,3 +0,0 @@
FROM postgres:13.1
EXPOSE 5432

View File

@@ -16,8 +16,15 @@ WORKDIR /home/$user
RUN mkdir /home/$user/logs
# 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 ./
RUN npm install --ignore-scripts
# 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
# 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
@@ -27,19 +34,10 @@ RUN mkdir /home/$user/logs
# 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 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)
RUN npm install --ignore-scripts
COPY --chown=$user:$user packages/fork-sax/package*.json ./packages/fork-sax/
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
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/server/package*.json ./packages/server/
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
COPY --chown=$user:$user lerna.json .
COPY --chown=$user:$user tsconfig.json .
@@ -50,22 +48,29 @@ 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
RUN ls -la /home/$user
# Then bootstrap only, without compiling the TypeScript files
RUN npm run bootstrap
# We have a separate step for the server files because they are more likely to
# change.
COPY --chown=$user:$user packages/server/package*.json ./packages/server/
RUN npm run bootstrapServerOnly
# 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/lib ./packages/lib
COPY --chown=$user:$user packages/renderer ./packages/renderer
COPY --chown=$user:$user packages/tools ./packages/tools
COPY --chown=$user:$user packages/lib ./packages/lib
COPY --chown=$user:$user packages/server ./packages/server
# Finally build everything, in particular the TypeScript files.
RUN npm run build
EXPOSE ${JOPLIN_PORT}
ENV RUNNING_IN_DOCKER=1
EXPOSE ${APP_PORT}
CMD [ "npm", "--prefix", "packages/server", "start" ]

15
docker-compose.db-dev.yml Normal file
View File

@@ -0,0 +1,15 @@
# For development this compose file starts the database only. The app can then
# be started using `npm run start-dev`, which is useful for development, because
# it means the app Docker file doesn't have to be rebuilt on each change.
version: '3'
services:
db:
image: postgres:13.1
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin
- POSTGRES_DB=joplin

View File

@@ -1,28 +1,27 @@
# For development, the easiest might be to only start the Postgres container and
# run the app directly with `npm start`. Or use sqlite3.
# This compose file can be used in development to run both the database and app
# within Docker.
version: '3'
services:
# app:
# build:
# context: .
# dockerfile: Dockerfile.server-dev
# ports:
# - "22300:22300"
# # volumes:
# # - ./packages/server/:/var/www/joplin/packages/server/
# # - /var/www/joplin/packages/server/node_modules/
db:
app:
build:
context: .
dockerfile: Dockerfile.db
dockerfile: Dockerfile.server
ports:
- "22300:22300"
environment:
- DB_CLIENT=pg
- POSTGRES_PASSWORD=joplin
- POSTGRES_DATABASE=joplin
- POSTGRES_USER=joplin
- POSTGRES_PORT=5432
- POSTGRES_HOST=localhost
db:
image: postgres:13.1
ports:
- "5432:5432"
environment:
# TODO: Considering the database is only exposed to the
# application, and not to the outside world, is there a need to
# pick a secure password?
- POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin
- POSTGRES_DB=joplin
- POSTGRES_DB=joplin

View File

@@ -1,40 +1,34 @@
# This is a sample docker-compose file that can be used to run Joplin Server
# along with a PostgreSQL server.
#
# All environment variables are optional. If you don't set them, you will get a
# warning from docker-compose, however the app should use working defaults.
version: '3'
services:
app:
environment:
- JOPLIN_BASE_URL=${JOPLIN_BASE_URL}
- JOPLIN_PORT=${JOPLIN_PORT}
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile.server
ports:
- "${JOPLIN_PORT}:${JOPLIN_PORT}"
# volumes:
# # Mount the server directory so that it's possible to edit file
# # while the container is running. However don't mount the
# # node_modules directory which will be specific to the Docker
# # image (eg native modules will be built for Ubuntu, while the
# # container might be running in Windows)
# # https://stackoverflow.com/a/37898591/561309
# - ./packages/server:/home/joplin/packages/server
# - /home/joplin/packages/server/node_modules/
db:
restart: unless-stopped
# By default, the Postgres image saves the data to a Docker volume,
# so it persists whenever the server is restarted using
# `docker-compose up`. Note that it would however be deleted when
# running `docker-compose down`.
build:
context: .
dockerfile: Dockerfile.db
image: postgres:13.1
ports:
- "5432:5432"
restart: unless-stopped
environment:
# TODO: Considering the database is only exposed to the
# application, and not to the outside world, is there a need to
# pick a secure password?
- POSTGRES_PASSWORD=joplin
- POSTGRES_USER=joplin
- POSTGRES_DB=joplin
- APP_PORT=22300
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=${POSTGRES_DATABASE}
app:
image: joplin/server:latest
depends_on:
- db
ports:
- "22300:22300"
restart: unless-stopped
environment:
- APP_BASE_URL=${APP_BASE_URL}
- DB_CLIENT=pg
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DATABASE=${POSTGRES_DATABASE}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_HOST=db

View File

@@ -8,12 +8,12 @@
"license": "MIT",
"scripts": {
"bootstrap": "lerna bootstrap --no-ci",
"bootstrapServerOnly": "lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/server",
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
"build": "lerna run build && npm run tsc",
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
"buildDoc": "./packages/tools/build-all.sh",
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/",
"buildPluginRepo": "node packages/tools/build-plugin-repository.js",
"buildTranslations": "npm run tsc && node packages/tools/build-translation.js",
"buildTranslationsNoTsc": "node packages/tools/build-translation.js",
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc",
@@ -25,7 +25,7 @@
"linter-precommit": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"postinstall": "npm run bootstrap --no-ci && npm run build",
"publishAll": "git pull && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"publishAll": "git pull && npm run build && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroidClean": "node packages/tools/release-android.js",
"releaseAndroid": "export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
"releaseCli": "node packages/tools/release-cli.js",

View File

@@ -1,22 +1,8 @@
const gulp = require('gulp');
const fs = require('fs-extra');
const utils = require('@joplin/tools/gulp/utils');
const { setPackagePrivateField } = require('@joplin/tools/tool-utils');
const tasks = {
// compileExtensions: {
// fn: require('../Tools/gulp/tasks/compileExtensions.js'),
// },
// copyLib: require('../Tools/gulp/tasks/copyLib'),
// tsc: require('../Tools/gulp/tasks/tsc'),
// updateIgnoredTypeScriptBuild: require('../Tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
};
// async function makePackagePublic(filePath) {
// const text = await fs.readFile(filePath, 'utf8');
// const obj = JSON.parse(text);
// delete obj.private;
// await fs.writeFile(filePath, JSON.stringify(obj), 'utf8');
// }
const tasks = {};
tasks.prepareBuild = {
fn: async () => {
@@ -26,8 +12,7 @@ tasks.prepareBuild = {
});
await utils.copyFile(`${__dirname}/package.json`, `${buildDir}/package.json`);
// await makePackagePublic(`${buildDir}/package.json`);
await setPackagePrivateField(`${buildDir}/package.json`, false);
await utils.setPackagePrivateField(`${buildDir}/package.json`, false);
await utils.copyFile(`${__dirname}/package-lock.json`, `${buildDir}/package-lock.json`);
await utils.copyFile(`${__dirname}/gulpfile.js`, `${buildDir}/gulpfile.js`);
@@ -50,14 +35,6 @@ tasks.prepareTestBuild = {
],
});
// const rootDir = utils.rootDir();
// await utils.copyDir(`${rootDir}/packages/app-mobile/lib`, `${testBuildDir}/lib`, {
// excluded: [
// `${rootDir}/packages/renderer/node_modules`,
// ],
// });
// await utils.copyDir(`${rootDir}/packages/app-mobile/locales`, `${testBuildDir}/locales`);
await fs.mkdirp(`${testBuildDir}/data`);
},
};
@@ -67,12 +44,4 @@ utils.registerGulpTasks(gulp, tasks);
gulp.task('build', gulp.series([
'prepareBuild',
// 'compileExtensions',
// 'copyLib',
]));
// gulp.task('buildTests', gulp.series([
// // 'prepareTestBuild',
// // 'compileExtensions',
// // 'copyLib',
// ]));

View File

@@ -27,17 +27,22 @@ describe('InMemoryCache', function() {
await time.msleep(510);
expect(cache.value('test')).toBe(undefined);
// Check that the TTL is reset every time setValue is called
cache.setValue('test', 'something', 300);
await time.msleep(100);
cache.setValue('test', 'something', 300);
await time.msleep(100);
cache.setValue('test', 'something', 300);
await time.msleep(100);
cache.setValue('test', 'something', 300);
await time.msleep(100);
// This test can sometimes fail in some cases, probably because it
// sleeps for more than 100ms (when the computer is slow). Changing this
// to use higher values would slow down the test unit too much, so let's
// disable it for now.
expect(cache.value('test')).toBe('something');
// Check that the TTL is reset every time setValue is called
// cache.setValue('test', 'something', 300);
// await time.msleep(100);
// cache.setValue('test', 'something', 300);
// await time.msleep(100);
// cache.setValue('test', 'something', 300);
// await time.msleep(100);
// cache.setValue('test', 'something', 300);
// await time.msleep(100);
// expect(cache.value('test')).toBe('something');
});
it('should delete old records', async () => {

View File

@@ -0,0 +1,56 @@
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import shim from '@joplin/lib/shim';
import { setupDatabaseAndSynchronizer, switchClient, supportDir, createTempDir } from '../../test-utils';
async function newRepoApi(): Promise<RepositoryApi> {
const repo = new RepositoryApi(`${supportDir}/pluginRepo`, await createTempDir());
await repo.loadManifests();
return repo;
}
describe('services_plugins_RepositoryApi', function() {
beforeEach(async (done: Function) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should get the manifests', (async () => {
const api = await newRepoApi();
const manifests = await api.manifests();
expect(!!manifests.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
expect(!!manifests.find(m => m.id === 'org.joplinapp.plugins.ToggleSidebars')).toBe(true);
}));
it('should search', (async () => {
const api = await newRepoApi();
{
const results = await api.search('to');
expect(results.length).toBe(2);
expect(!!results.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
expect(!!results.find(m => m.id === 'org.joplinapp.plugins.ToggleSidebars')).toBe(true);
}
{
const results = await api.search('backlink');
expect(results.length).toBe(1);
expect(!!results.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
}
}));
it('should download a plugin', (async () => {
const api = await newRepoApi();
const pluginPath = await api.downloadPlugin('org.joplinapp.plugins.ToggleSidebars');
expect(await shim.fsDriver().exists(pluginPath)).toBe(true);
}));
it('should tell if a plugin can be updated', (async () => {
const api = await newRepoApi();
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.0')).toBe(true);
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.2')).toBe(false);
expect(await api.pluginCanBeUpdated('does.not.exist', '1.0.0')).toBe(false);
}));
});

View File

@@ -0,0 +1,25 @@
# Joplin Plugin Repository
This is the official Joplin Plugin Repository
## Installation
To install any of these plugins, open the desktop application, then go to the "Plugins" section in the Configuration screen. You can then search for any plugin and install it from there.
## Plugins
This repository contains the following plugins:
<!-- PLUGIN_LIST -->
&nbsp; | Name | Version | Description | Author
--- | --- | --- | --- | ---
[🏠](https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632) | Backlinks to note | 1.0.4 | Creates backlinks to opened note | a
[🏠](https://github.com/JackGruber/joplin-plugin-combine-notes) | Combine notes | 0.2.1 | Combine one or more notes | JackGruber
[🏠](https://github.com/JackGruber/joplin-plugin-copytags) | Copy Tags | 0.3.2 | Plugin to extend the Joplin tagging menu with a coppy all tags and tagging list with more control. | JackGruber
[🏠](https://discourse.joplinapp.org/t/go-to-note-tag-or-notebook-via-highlighting-text-in-editor/12731) | Create and go to #tags and @notebooks | 1.3.4 | Go to tag,notebook or note via links or via text | a
[🏠](https://github.com/benji300/joplin-favorites) | Favorites | 1.0.0 | Save any notebook, note, to-do, tag, or search as favorite in an extra panel view for quick access. (v1.0.0) | Benji300
[🏠](https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars) | Note list and side bar toggle buttons | 1.0.2 | Adds buttons to toggle note list and sidebar | Laurent Cozic
[🏠](https://github.com/JackGruber/joplin-plugin-note-overview) | Note overview | 1.0.0 | A note overview is created based on the defined search and the specified fields | JackGruber
[🏠](https://github.com/benji300/joplin-note-tabs) | Note Tabs | 1.1.1 | Allows to open several notes at once in tabs and pin them. (v1.1.1) | Benji300
[🏠](https://github.com/JackGruber/joplin-plugin-backup) | Simple Backup | 0.3.0 | Plugin to create manual and automatic backups | JackGruber
<!-- PLUGIN_LIST -->

View File

@@ -0,0 +1,29 @@
{
"joplin.plugin.ambrt.backlinksToNote": {
"manifest_version": 1,
"id": "joplin.plugin.ambrt.backlinksToNote",
"app_min_version": "1.5",
"version": "1.0.4",
"name": "Backlinks to note",
"description": "Creates backlinks to opened note",
"author": "a",
"homepage_url": "https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632",
"_publish_hash": "sha256:ab9c9bc776e167a3b1a9ec40a4927d93da14514c0508f46dd6e5ea3f8a3a6c3b",
"_publish_commit": "master:5b74f93c687463572c46200292fa911e0ba96033",
"_npm_package_name": "joplin-plugin-backlinks"
},
"org.joplinapp.plugins.ToggleSidebars": {
"manifest_version": 1,
"id": "org.joplinapp.plugins.ToggleSidebars",
"app_min_version": "1.6",
"version": "1.0.2",
"name": "Note list and side bar toggle buttons",
"description": "Adds buttons to toggle note list and sidebar",
"author": "Laurent Cozic",
"homepage_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
"repository_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
"_publish_hash": "sha256:e0d833b7ef1bb8f02ee4cb861ef1989621358c45a5614911071302dc0527a3b4",
"_publish_commit": "dev:1b5b2342fc25717b77ad9f1627c1a334e5bbae54",
"_npm_package_name": "@joplin/joplin-plugin-toggle-sidebars"
}
}

View File

@@ -0,0 +1,13 @@
{
"manifest_version": 1,
"id": "joplin.plugin.ambrt.backlinksToNote",
"app_min_version": "1.5",
"version": "1.0.4",
"name": "Backlinks to note",
"description": "Creates backlinks to opened note",
"author": "a",
"homepage_url": "https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632",
"_publish_hash": "sha256:ab9c9bc776e167a3b1a9ec40a4927d93da14514c0508f46dd6e5ea3f8a3a6c3b",
"_publish_commit": "master:5b74f93c687463572c46200292fa911e0ba96033",
"_npm_package_name": "joplin-plugin-backlinks"
}

View File

@@ -0,0 +1,14 @@
{
"manifest_version": 1,
"id": "org.joplinapp.plugins.ToggleSidebars",
"app_min_version": "1.6",
"version": "1.0.2",
"name": "Note list and side bar toggle buttons",
"description": "Adds buttons to toggle note list and sidebar",
"author": "Laurent Cozic",
"homepage_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
"repository_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
"_publish_hash": "sha256:e0d833b7ef1bb8f02ee4cb861ef1989621358c45a5614911071302dc0527a3b4",
"_publish_commit": "dev:1b5b2342fc25717b77ad9f1627c1a334e5bbae54",
"_npm_package_name": "@joplin/joplin-plugin-toggle-sidebars"
}

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -4,7 +4,7 @@
"app_min_version": "1.4",
"name": "Register Command Test",
"description": "To test registering commands",
"version": "1.0.0",
"version": "1.0.3",
"author": "Laurent Cozic",
"homepage_url": "https://joplinapp.org"
}

View File

@@ -0,0 +1,7 @@
#!/bin/bash
# - Update src/manifest.json with the new version number
# - Run the below command
# - Then the file /manifests.json also needs to be updated with the new manifest file
npm run dist && cp publish/org.joplinapp.plugins.RegisterCommandDemo.jpl ~/src/joplin-plugins-test/plugins/org.joplinapp.plugins.RegisterCommandDemo/plugin.jpl && cp publish/org.joplinapp.plugins.RegisterCommandDemo.json ~/src/joplin-plugins-test/plugins/org.joplinapp.plugins.RegisterCommandDemo/plugin.json

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -233,7 +233,7 @@ function main(processArgv) {
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: pluginConfig,
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
@@ -247,7 +247,7 @@ function main(processArgv) {
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: createArchiveConfig,
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
@@ -270,4 +270,10 @@ try {
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

View File

@@ -104,6 +104,7 @@ FileApiDriverLocal.fsDriver_ = fsDriver;
const logDir = `${__dirname}/../tests/logs`;
const baseTempDir = `${__dirname}/../tests/tmp/${suiteName_}`;
const supportDir = `${__dirname}/support`;
// We add a space in the data directory path as that will help uncover
// various space-in-path issues.
@@ -180,6 +181,7 @@ BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplintest-cli');
Setting.setConstant('appType', 'cli');
Setting.setConstant('tempDir', baseTempDir);
Setting.setConstant('cacheDir', baseTempDir);
Setting.setConstant('env', 'dev');
BaseService.logger_ = logger;
@@ -864,4 +866,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { waitForFolderCount, afterAllCleanUp, exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
export { supportDir, waitForFolderCount, afterAllCleanUp, exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@@ -78,7 +78,7 @@ const commands = [
require('./gui/NoteEditor/commands/showRevisions'),
require('./gui/NoteList/commands/focusElementNoteList'),
require('./gui/NoteListControls/commands/focusSearch'),
require('./gui/SideBar/commands/focusElementSideBar'),
require('./gui/Sidebar/commands/focusElementSideBar'),
];
// Commands that are not tied to any particular component.
@@ -527,7 +527,7 @@ class Application extends BaseApplication {
// time, however we only effectively uninstall the plugin the next
// time the app is started. What plugin should be uninstalled is
// stored in the settings.
const newSettings = await service.uninstallPlugins(pluginSettings);
const newSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings));
Setting.setValue('plugins.states', newSettings);
try {

View File

@@ -7,6 +7,7 @@ export enum ButtonLevel {
Secondary = 'secondary',
Tertiary = 'tertiary',
SidebarSecondary = 'sidebarSecondary',
Recommended = 'recommended',
}
interface Props {
@@ -121,6 +122,20 @@ const StyledButtonTertiary = styled(StyledButtonBase)`
}
`;
const StyledButtonRecommended = styled(StyledButtonBase)`
border: 1px solid ${(props: any) => props.theme.borderColor4};
background-color: ${(props: any) => props.theme.warningBackgroundColor};
${StyledIcon} {
color: ${(props: any) => props.theme.color};
}
${StyledTitle} {
color: ${(props: any) => props.theme.color};
opacity: 0.9;
}
`;
const StyledButtonSidebarSecondary = styled(StyledButtonBase)`
background: none;
border-color: ${(props: any) => props.theme.color2};
@@ -167,10 +182,11 @@ function buttonClass(level: ButtonLevel) {
if (level === ButtonLevel.Primary) return StyledButtonPrimary;
if (level === ButtonLevel.Tertiary) return StyledButtonTertiary;
if (level === ButtonLevel.SidebarSecondary) return StyledButtonSidebarSecondary;
if (level === ButtonLevel.Recommended) return StyledButtonRecommended;
return StyledButtonSecondary;
}
export default function Button(props: Props) {
function Button(props: Props) {
const iconOnly = props.iconName && !props.title;
const StyledButton = buttonClass(props.level);
@@ -197,3 +213,5 @@ export default function Button(props: Props) {
</StyledButton>
);
}
export default styled(Button)`${space}`;

View File

@@ -148,6 +148,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
const createSettingComponents = (advanced: boolean) => {
const output = [];
for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i];
if (!!md.advanced !== advanced) continue;
@@ -160,8 +161,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
const settingComps = createSettingComponents(false);
const advancedSettingComps = createSettingComponents(true);
const sectionWidths: Record<string, number> = {
plugins: 900,
const sectionWidths: Record<string, any> = {
plugins: '100%',
};
const sectionStyle: any = {
@@ -305,7 +306,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
private renderHeader(themeId: number, label: string) {
private renderHeader(themeId: number, label: string, style: any = null) {
const theme = themeStyle(themeId);
const labelStyle = Object.assign({}, theme.textStyle, {
@@ -314,6 +315,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
fontSize: theme.fontSize * 1.25,
fontWeight: 500,
marginBottom: theme.mainPadding,
...style,
});
return (
@@ -457,7 +459,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
// There's probably a better way to do this but can't figure it out.
return (
<div key={key + value.toString()} style={rowStyle}>
<div key={key + (`${value}`).toString()} style={rowStyle}>
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
<input
id={`setting_checkbox_${key}`}
@@ -564,7 +566,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
value={cmd[1]}
spellCheck={false}
/>
<div style={{ width: inputStyle.width }}>
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
@@ -593,7 +595,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
}}
spellCheck={false}
/>
<div style={{ width: inputStyle.width }}>
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>

View File

@@ -11,34 +11,47 @@ export enum InstallState {
Installed = 3,
}
export enum UpdateState {
Idle = 1,
CanUpdate = 2,
Updating = 3,
HasBeenUpdated = 4,
}
interface Props {
item?: PluginItem;
manifest?: PluginManifest;
installState?: InstallState;
updateState?: UpdateState;
themeId: number;
onToggle?: Function;
onDelete?: Function;
onInstall?: Function;
onUpdate?: Function;
}
function manifestToItem(manifest: PluginManifest): PluginItem {
return {
id: manifest.id,
name: manifest.name,
version: manifest.version,
description: manifest.description,
enabled: true,
deleted: false,
devMode: false,
hasBeenUpdated: false,
};
}
export interface PluginItem {
id: string;
name: string;
version: string;
description: string;
enabled: boolean;
deleted: boolean;
devMode: boolean;
hasBeenUpdated: boolean;
}
const CellRoot = styled.div`
@@ -50,7 +63,7 @@ const CellRoot = styled.div`
padding: 15px;
border: 1px solid ${props => props.theme.dividerColor};
border-radius: 6px;
width: 250px;
width: 320px;
margin-right: 20px;
margin-bottom: 20px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
@@ -90,6 +103,12 @@ const StyledName = styled.div`
flex: 1;
`;
const StyledVersion = styled.span`
margin-left: 5px;
color: ${props => props.theme.colorFaded};
font-size: ${props => props.theme.fontSize * 0.9}px;
`;
const StyledDescription = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.colorFaded};
@@ -138,6 +157,23 @@ export default function(props: Props) {
/>;
}
function renderUpdateButton() {
if (!props.onUpdate) return null;
let title = _('Update');
if (props.updateState === UpdateState.Updating) title = _('Updating...');
if (props.updateState === UpdateState.Idle) title = _('Updated');
if (props.updateState === UpdateState.HasBeenUpdated) title = _('Updated');
return <Button
ml={1}
level={ButtonLevel.Recommended}
onClick={() => props.onUpdate({ item })}
title={title}
disabled={props.updateState === UpdateState.HasBeenUpdated}
/>;
}
function renderFooter() {
if (item.devMode) return null;
@@ -145,6 +181,7 @@ export default function(props: Props) {
<CellFooter>
{renderDeleteButton()}
{renderInstallButton()}
{renderUpdateButton()}
<div style={{ display: 'flex', flex: 1 }}/>
</CellFooter>
);
@@ -153,7 +190,7 @@ export default function(props: Props) {
return (
<CellRoot>
<CellTop>
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''}</StyledName>
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''} <StyledVersion>v{item.version}</StyledVersion></StyledName>
{renderToggleButton()}
</CellTop>
<CellContent>

View File

@@ -1,18 +1,21 @@
import * as React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { _ } from '@joplin/lib/locale';
import styled from 'styled-components';
import SearchPlugins from './SearchPlugins';
import PluginBox from './PluginBox';
import PluginBox, { UpdateState } from './PluginBox';
import Button, { ButtonLevel } from '../../../Button/Button';
import bridge from '../../../../services/bridge';
import produce from 'immer';
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
import { PluginItem } from './PluginBox';
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import Setting from '@joplin/lib/models/Setting';
import useOnInstallHandler, { OnPluginSettingChangeEvent } from './useOnInstallHandler';
const { space } = require('styled-system');
const maxWidth: number = 250;
const maxWidth: number = 320;
const Root = styled.div`
display: flex;
@@ -26,7 +29,7 @@ const UserPluginsRoot = styled.div`
`;
const ToolsButton = styled(Button)`
margin-right: 2px;
margin-right: 6px;
`;
interface Props {
@@ -38,6 +41,15 @@ interface Props {
renderHeader: Function;
}
let repoApi_: RepositoryApi = null;
function repoApi(): RepositoryApi {
if (repoApi_) return repoApi_;
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
// repoApi_ = new RepositoryApi('/Users/laurent/src/joplin-plugins-test', Setting.value('tempDir'));
return repoApi_;
}
function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] {
return useMemo(() => {
const output: PluginItem[] = [];
@@ -53,10 +65,12 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[
output.push({
id: pluginId,
name: plugin.manifest.name,
version: plugin.manifest.version,
description: plugin.manifest.description,
enabled: setting.enabled,
deleted: setting.deleted,
devMode: plugin.devMode,
hasBeenUpdated: setting.hasBeenUpdated,
});
}
@@ -70,6 +84,9 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[
export default function(props: Props) {
const [searchQuery, setSearchQuery] = useState('');
const [manifestsLoaded, setManifestsLoaded] = useState<boolean>(false);
const [updatingPluginsIds, setUpdatingPluginIds] = useState<Record<string, boolean>>({});
const [canBeUpdatedPluginIds, setCanBeUpdatedPluginIds] = useState<Record<string, boolean>>({});
const pluginService = PluginService.instance();
@@ -77,6 +94,43 @@ export default function(props: Props) {
return pluginService.unserializePluginSettings(props.value);
}, [props.value]);
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
useEffect(() => {
let cancelled = false;
async function fetchManifests() {
await repoApi().loadManifests();
if (cancelled) return;
setManifestsLoaded(true);
}
void fetchManifests();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!manifestsLoaded) return () => {};
let cancelled = false;
async function fetchPluginIds() {
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems as any);
if (cancelled) return;
const conv: Record<string, boolean> = {};
pluginIds.forEach(id => conv[id] = true);
setCanBeUpdatedPluginIds(conv);
}
void fetchPluginIds();
return () => {
cancelled = true;
};
}, [manifestsLoaded, pluginItems]);
const onDelete = useCallback(async (event: any) => {
const item: PluginItem = event.item;
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.name));
@@ -118,6 +172,12 @@ export default function(props: Props) {
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
}, []);
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
const onToolsClick = useCallback(async () => {
const template = [];
@@ -144,12 +204,22 @@ export default function(props: Props) {
for (const item of items) {
if (item.deleted) continue;
const isUpdating = updatingPluginsIds[item.id];
const onUpdateHandler = canBeUpdatedPluginIds[item.id] ? onUpdate : null;
let updateState = UpdateState.Idle;
if (onUpdateHandler) updateState = UpdateState.CanUpdate;
if (isUpdating) updateState = UpdateState.Updating;
if (item.hasBeenUpdated) updateState = UpdateState.HasBeenUpdated;
output.push(<PluginBox
key={item.id}
item={item}
themeId={props.themeId}
updateState={updateState}
onDelete={onDelete}
onToggle={onToggle}
onUpdate={onUpdateHandler}
/>);
}
@@ -174,13 +244,11 @@ export default function(props: Props) {
}
}
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
return (
<Root>
function renderSearchArea() {
return (
<div style={{ marginBottom: 20 }}>
{props.renderHeader(props.themeId, _('Search for plugins'))}
<SearchPlugins
disabled={!manifestsLoaded}
maxWidth={maxWidth}
themeId={props.themeId}
searchQuery={searchQuery}
@@ -188,18 +256,32 @@ export default function(props: Props) {
onSearchQueryChange={onSearchQueryChange}
onPluginSettingsChange={onSearchPluginSettingsChange}
renderDescription={props.renderDescription}
repoApi={repoApi}
/>
</div>
);
}
function renderBottomArea() {
if (searchQuery) return null;
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
<div style={{ display: 'flex', flex: 1 }}>
{props.renderHeader(props.themeId, _('Manage your plugins'))}
</div>
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Primary} onClick={onToolsClick}/>
</div>
{renderUserPlugins(pluginItems)}
</div>
);
}
return (
<Root>
{renderSearchArea()}
{renderBottomArea()}
</Root>
);
}

View File

@@ -6,7 +6,6 @@ import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
import PluginBox, { InstallState } from './PluginBox';
import Setting from '@joplin/lib/models/Setting';
import { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { _ } from '@joplin/lib/locale';
import useOnInstallHandler from './useOnInstallHandler';
@@ -27,24 +26,18 @@ interface Props {
onPluginSettingsChange(event: any): void;
renderDescription: Function;
maxWidth: number;
}
let repoApi_: RepositoryApi = null;
function repoApi(): RepositoryApi {
if (repoApi_) return repoApi_;
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
return repoApi_;
repoApi(): RepositoryApi;
disabled: boolean;
}
export default function(props: Props) {
const [searchStarted, setSearchStarted] = useState(false);
const [manifests, setManifests] = useState<PluginManifest[]>([]);
const asyncSearchQueue = useRef(new AsyncActionQueue(200));
const asyncSearchQueue = useRef(new AsyncActionQueue(10));
const [installingPluginsIds, setInstallingPluginIds] = useState<Record<string, boolean>>({});
const [searchResultCount, setSearchResultCount] = useState(null);
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, repoApi, props.onPluginSettingsChange);
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, props.repoApi, props.onPluginSettingsChange, false);
useEffect(() => {
setSearchResultCount(null);
@@ -53,7 +46,7 @@ export default function(props: Props) {
setManifests([]);
setSearchResultCount(null);
} else {
const r = await repoApi().search(props.searchQuery);
const r = await props.repoApi().search(props.searchQuery);
setManifests(r);
setSearchResultCount(r.length);
}
@@ -107,6 +100,8 @@ export default function(props: Props) {
onChange={onChange}
onSearchButtonClick={onSearchButtonClick}
searchStarted={searchStarted}
placeholder={_('Search for plugins...')}
disabled={props.disabled}
/>
</div>

View File

@@ -6,7 +6,13 @@ import Logger from '@joplin/lib/Logger';
const logger = Logger.create('useOnInstallHandler');
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: Function) {
export interface OnPluginSettingChangeEvent {
value: PluginSettings;
}
type OnPluginSettingChangeHandler = (event: OnPluginSettingChangeEvent)=> void;
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: OnPluginSettingChangeHandler, isUpdate: boolean) {
return useCallback(async (event: any) => {
const pluginId = event.item.id;
@@ -19,7 +25,11 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
let installError = null;
try {
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
if (isUpdate) {
await PluginService.instance().updatePluginFromRepo(repoApi(), pluginId);
} else {
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
}
} catch (error) {
installError = error;
logger.error(error);
@@ -28,6 +38,7 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
if (!installError) {
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
draft[pluginId] = defaultPluginSetting();
if (isUpdate) draft[pluginId].hasBeenUpdated = true;
});
onPluginSettingsChange({ value: newSettings });

View File

@@ -41,6 +41,8 @@ interface Props {
onKeyDown?: Function;
onSearchButtonClick: Function;
searchStarted: boolean;
placeholder?: string;
disabled?: boolean;
}
export interface OnChangeEvent {
@@ -60,12 +62,13 @@ export default function(props: Props) {
ref={props.inputRef}
value={props.value}
type="text"
placeholder={_('Search...')}
placeholder={props.placeholder || _('Search...')}
onChange={onChange}
onFocus={props.onFocus}
onBlur={props.onBlur}
onKeyDown={props.onKeyDown}
spellCheck={false}
disabled={props.disabled}
/>
<SearchButton onClick={props.onSearchButtonClick}>
<SearchButtonIcon className={iconName}/>

View File

@@ -1,6 +1,5 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
const fs = require('fs-extra');
const tasks = {
compileScripts: {
@@ -18,55 +17,18 @@ const tasks = {
electronRebuild: {
fn: require('./tools/electronRebuild.js'),
},
// compileExtensions: {
// fn: require('@joplin/tools/gulp/tasks/compileExtensions.js'),
// },
// copyLib: require('@joplin/tools/gulp/tasks/copyLib'),
tsc: require('@joplin/tools/gulp/tasks/tsc'),
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
linkReact: {
fn: async () => {
// React is a dependency of both the lib and app-desktop
// packages, which cause a duplicate React issue. To go around
// this, one way is to manually link the package.
//
// Note that React must also be unlinked in preinstall step
// otherwise there will be permission errors when running
// `lerna bootstrap`
//
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
process.chdir(__dirname);
await fs.remove('./node_modules/react');
await fs.remove('./node_modules/react-dom');
await utils.execCommand('npm link ../lib/node_modules/react');
await utils.execCommand('npm link ../lib/node_modules/react-dom');
},
},
};
utils.registerGulpTasks(gulp, tasks);
// const buildSeries = [
// // 'compileExtensions',
// // 'copyLib',
// ];
// On Windows also run tsc because `npm run watch` locks some folders
// which makes the copyPluginAssets command fail. For that reason,
// it's not possible to run watch on Windows while testing the desktop app.
// if (require('os').platform() === 'win32') {
// buildSeries.push('tsc');
// }
const buildParallel = [
// gulp.series(...buildSeries),
'compileScripts',
'compilePackageInfo',
'copyPluginAssets',
'copyTinyMceLangs',
'updateIgnoredTypeScriptBuild',
// 'linkReact',
];
gulp.task('build', gulp.parallel(...buildParallel));

View File

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

View File

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

View File

@@ -635,8 +635,8 @@ class NoteScreenComponent extends BaseScreenComponent {
let resource = Resource.new();
resource.id = uuid.create();
resource.mime = mimeType;
resource.title = pickerResponse.fileName ? pickerResponse.fileName : '';
resource.file_extension = safeFileExtension(fileExtension(pickerResponse.fileName ? pickerResponse.fileName : localFilePath));
resource.title = pickerResponse.name ? pickerResponse.name : '';
resource.file_extension = safeFileExtension(fileExtension(pickerResponse.name ? pickerResponse.name : localFilePath));
if (!resource.mime) resource.mime = 'application/octet-stream';

View File

@@ -1,5 +1,4 @@
const gulp = require('gulp');
const fs = require('fs-extra');
const utils = require('@joplin/tools/gulp/utils');
const tasks = {
@@ -12,23 +11,6 @@ const tasks = {
podInstall: {
fn: require('./tools/podInstall'),
},
prepareRelease: {
fn: require('./tools/prepareRelease'),
},
// clean: {
// fn: require('./tools/clean'),
// },
linkReact: {
fn: async () => {
// React is a dependency of both the lib and app-desktop
// packages, which cause a duplicate React issue. To go around
// this, one way is to manually link the package.
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
process.chdir(__dirname);
await fs.remove('./node_modules/react');
await utils.execCommand('npm link ../lib/node_modules/react');
},
},
};
utils.registerGulpTasks(gulp, tasks);
@@ -36,6 +18,5 @@ utils.registerGulpTasks(gulp, tasks);
gulp.task('build', gulp.series(
'buildReactNativeInjectedJs',
'encodeAssets',
// 'linkReact',
'podInstall'
));

View File

@@ -1,32 +0,0 @@
// This is to replace the symlinks inside node_modules with the actual packages
// as I assumed it was needed to build the final release. However it seems
// Android `assembleRelease` handles symlinks properly so maybe this is not
// needed after all ¯\_(ツ)_/¯
const { copyDir } = require('@joplin/tools/gulp/utils');
const { rootDir, deleteLink, toSystemSlashes } = require('@joplin/tools/tool-utils');
const mobileDir = `${rootDir}/packages/app-mobile`;
module.exports = async function() {
const dirsToCopy = [
'fork-htmlparser2',
'fork-sax',
'lib',
'renderer',
];
const destDir = `${mobileDir}/node_modules/@joplin`;
for (const dir of dirsToCopy) {
const destPath = toSystemSlashes(`${destDir}/${dir}`);
const sourcePath = toSystemSlashes(`${rootDir}/packages/${dir}`);
console.info(`Copying ${sourcePath} => ${destPath}`);
// TODO: copy symlink so that it can be restored
await deleteLink(destPath);
await copyDir(sourcePath, destPath, {
excluded: ['node_modules'],
});
}
};

View File

@@ -1,31 +0,0 @@
// const fs = require('fs');
// const { execSync } = require("child_process");
// const isWindows = process.platform === "win32";
// function toSystemSlashes(path) {
// const os = process.platform;
// if (os === 'win32') return path.replace(/\//g, '\\');
// return path.replace(/\\/g, '/');
// }
// const nodeModulesPath = `${__dirname}/../node_modules`;
// function deleteLink(path) {
// if (isWindows) {
// try {
// execSync(`rmdir "${toSystemSlashes(path)}"`, { stdio : 'pipe' });
// } catch (error) {
// // console.info('Error: ' + error.message);
// }
// } else {
// try {
// fs.unlinkSync(toSystemSlashes(path));
// } catch (error) {
// }
// }
// }
// deleteLink(nodeModulesPath + '/react');
// deleteLink(nodeModulesPath + '/react-dom');

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-htmlparser2",
"version": "4.1.15",
"version": "4.1.19",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@joplin/fork-htmlparser2",
"description": "Fast & forgiving HTML/XML/RSS parser",
"version": "4.1.15",
"version": "4.1.19",
"author": "Felix Boehm <me@feedic.com>",
"publishConfig": {
"access": "public"
@@ -52,7 +52,7 @@
"coveralls": "^3.0.1",
"eslint": "^6.0.0",
"eslint-config-prettier": "^6.0.0",
"jest": "^24.8.0",
"jest": "^26.6.3",
"prettier": "^1.18.2",
"ts-jest": "^24.0.2",
"typescript": "^3.5.3"

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-sax",
"version": "1.2.19",
"version": "1.2.23",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -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.19",
"version": "1.2.23",
"main": "lib/sax.js",
"publishConfig": {
"access": "public"

View File

@@ -34,4 +34,4 @@
"repository": "https://github.com/laurent22/generator-joplin",
"license": "MIT",
"private": true
}
}

View File

@@ -667,6 +667,7 @@ export default class BaseApplication {
const resourceDirName = 'resources';
const resourceDir = `${profileDir}/${resourceDirName}`;
const tempDir = `${profileDir}/tmp`;
const cacheDir = `${profileDir}/cache`;
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
@@ -674,6 +675,7 @@ export default class BaseApplication {
Setting.setConstant('resourceDirName', resourceDirName);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
Setting.setConstant('cacheDir', cacheDir);
Setting.setConstant('pluginDir', `${profileDir}/plugins`);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
@@ -695,6 +697,7 @@ export default class BaseApplication {
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);
await fs.mkdirp(cacheDir, 0o755);
// Clean up any remaining watched files (they start with "edit-")
await shim.fsDriver().removeAllThatStartWith(profileDir, 'edit-');

View File

@@ -1659,6 +1659,7 @@ Setting.constants_ = {
profileDir: '',
templateDir: '',
tempDir: '',
cacheDir: '',
pluginDir: '',
flagOpenDevTools: false,
syncVersion: 2,

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "1.0.16",
"version": "1.7.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "1.0.16",
"version": "1.7.2",
"description": "Joplin Core library",
"author": "Laurent Cozic",
"homepage": "",
@@ -22,11 +22,11 @@
"typescript": "^4.0.5"
},
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.15",
"@joplin/fork-sax": "^1.2.19",
"@joplin/renderer": "^1.0.24",
"@joplin/turndown": "^4.0.37",
"@joplin/turndown-plugin-gfm": "^1.0.19",
"@joplin/fork-htmlparser2": "^4.1.19",
"@joplin/fork-sax": "^1.2.23",
"@joplin/renderer": "^1.7.2",
"@joplin/turndown": "^4.0.41",
"@joplin/turndown-plugin-gfm": "^1.0.23",
"async-mutex": "^0.1.3",
"aws-sdk": "^2.588.0",
"base-64": "^0.1.0",

View File

@@ -8,6 +8,7 @@ import { filename, dirname, rtrimSlashes } from '../../path-utils';
import Setting from '../../models/Setting';
import Logger from '../../Logger';
import RepositoryApi from './RepositoryApi';
import produce from 'immer';
const compareVersions = require('compare-versions');
const uslug = require('uslug');
const md5File = require('md5-file/promise');
@@ -31,12 +32,19 @@ export interface Plugins {
export interface PluginSetting {
enabled: boolean;
deleted: boolean;
// After a plugin has been updated, the user needs to restart the app before
// loading the new version. In the meantime, we set this property to `true`
// so that we know the plugin has been updated. It is used for example to
// disable the Update button.
hasBeenUpdated: boolean;
}
export function defaultPluginSetting(): PluginSetting {
return {
enabled: true,
deleted: false,
hasBeenUpdated: false,
};
}
@@ -92,6 +100,10 @@ export default class PluginService extends BaseService {
delete this.plugins_[pluginId];
}
private async deletePluginFiles(plugin: Plugin) {
await shim.fsDriver().remove(plugin.baseDir);
}
public pluginById(id: string): Plugin {
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
@@ -173,7 +185,7 @@ export default class PluginService extends BaseService {
const fname = filename(path);
const hash = await md5File(path);
const unpackDir = `${Setting.value('tempDir')}/${fname}`;
const unpackDir = `${Setting.value('cacheDir')}/${fname}`;
const manifestFilePath = `${unpackDir}/manifest.json`;
let manifest: any = await this.loadManifestToObject(manifestFilePath);
@@ -194,7 +206,7 @@ export default class PluginService extends BaseService {
manifest._package_hash = hash;
await shim.fsDriver().writeFile(manifestFilePath, JSON.stringify(manifest), 'utf8');
await shim.fsDriver().writeFile(manifestFilePath, JSON.stringify(manifest, null, '\t'), 'utf8');
}
return this.loadPluginFromPath(unpackDir);
@@ -345,6 +357,10 @@ export default class PluginService extends BaseService {
return plugin;
}
public async updatePluginFromRepo(repoApi: RepositoryApi, pluginId: string): Promise<Plugin> {
return this.installPluginFromRepo(repoApi, pluginId);
}
public async installPlugin(jplPath: string): Promise<Plugin> {
logger.info(`Installing plugin: "${jplPath}"`);
@@ -352,6 +368,7 @@ export default class PluginService extends BaseService {
// from where it is now to check that it is valid and to retrieve
// the plugin ID.
const preloadedPlugin = await this.loadPluginFromPath(jplPath);
await this.deletePluginFiles(preloadedPlugin);
const destPath = `${Setting.value('pluginDir')}/${preloadedPlugin.id}.jpl`;
await shim.fsDriver().copy(jplPath, destPath);
@@ -402,6 +419,16 @@ export default class PluginService extends BaseService {
return newSettings;
}
// On startup the "hasBeenUpdated" prop can be cleared since the new version
// of the plugin has now been loaded.
public clearUpdateState(settings: PluginSettings): PluginSettings {
return produce(settings, (draft: PluginSettings) => {
for (const pluginId in draft) {
if (draft[pluginId].hasBeenUpdated) draft[pluginId].hasBeenUpdated = false;
}
});
}
public async destroy() {
await this.runner_.waitForSandboxCalls();
}

View File

@@ -1,11 +1,15 @@
import shim from '../../shim';
import { PluginManifest } from './utils/types';
const md5 = require('md5');
const compareVersions = require('compare-versions');
export default class RepositoryApi {
// For now, it's assumed that the baseUrl is a GitHub repo URL, such as
// https://github.com/joplin/plugins
// As a base URL, this class can support either a remote repository or a
// local one (a directory path), which is useful for testing.
//
// For now, if the baseUrl is an actual URL it's assumed it's a GitHub repo
// URL, such as https://github.com/joplin/plugins
//
// Later on, other repo types could be supported.
private baseUrl_: string;
@@ -17,8 +21,29 @@ export default class RepositoryApi {
this.tempDir_ = tempDir;
}
public async loadManifests() {
const manifestsText = await this.fetchText('manifests.json');
try {
const manifests = JSON.parse(manifestsText);
if (!manifests) throw new Error('Invalid or missing JSON');
this.manifests_ = Object.keys(manifests).map(id => {
return manifests[id];
});
} catch (error) {
throw new Error(`Could not parse JSON: ${error.message}`);
}
}
private get isLocalRepo(): boolean {
return this.baseUrl_.indexOf('http') !== 0;
}
private get contentBaseUrl(): string {
return `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
if (this.isLocalRepo) {
return this.baseUrl_;
} else {
return `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
}
}
private fileUrl(relativePath: string): string {
@@ -26,7 +51,11 @@ export default class RepositoryApi {
}
private async fetchText(path: string): Promise<string> {
return shim.fetchText(this.fileUrl(path));
if (this.isLocalRepo) {
return shim.fsDriver().readFile(this.fileUrl(path), 'utf8');
} else {
return shim.fetchText(this.fileUrl(path));
}
}
public async search(query: string): Promise<PluginManifest[]> {
@@ -60,27 +89,40 @@ export default class RepositoryApi {
const fileUrl = this.fileUrl(`plugins/${manifest.id}/plugin.jpl`);
const hash = md5(Date.now() + Math.random());
const targetPath = `${this.tempDir_}/${hash}_${manifest.id}.jpl`;
const response = await shim.fetchBlob(fileUrl, {
path: targetPath,
});
if (!response.ok) throw new Error(`Could not download plugin "${pluginId}" from "${fileUrl}"`);
if (this.isLocalRepo) {
await shim.fsDriver().copy(fileUrl, targetPath);
} else {
const response = await shim.fetchBlob(fileUrl, {
path: targetPath,
});
if (!response.ok) throw new Error(`Could not download plugin "${pluginId}" from "${fileUrl}"`);
}
return targetPath;
}
private async manifests(): Promise<PluginManifest[]> {
if (this.manifests_) return this.manifests_;
const manifestsText = await this.fetchText('manifests.json');
try {
const manifests = JSON.parse(manifestsText);
if (!manifests) throw new Error('Invalid or missing JSON');
this.manifests_ = Object.keys(manifests).map(id => {
return manifests[id];
});
return this.manifests_;
} catch (error) {
throw new Error(`Could not parse JSON: ${error.message}`);
public async manifests(): Promise<PluginManifest[]> {
if (!this.manifests_) throw new Error('Manifests have no been loaded!');
return this.manifests_;
}
public async canBeUpdatedPlugins(installedManifests: PluginManifest[]): Promise<string[]> {
const output = [];
for (const manifest of installedManifests) {
const canBe = await this.pluginCanBeUpdated(manifest.id, manifest.version);
if (canBe) output.push(manifest.id);
}
return output;
}
public async pluginCanBeUpdated(pluginId: string, installedVersion: string): Promise<boolean> {
const manifest = (await this.manifests()).find(m => m.id === pluginId);
if (!manifest) return false;
return compareVersions(installedVersion, manifest.version) < 0;
}
}

View File

@@ -0,0 +1,9 @@
// Dummy test because the Jest setup is done but there's for now no test.
describe('dummy', () => {
it('should pass', () => {
expect(1).toBe(1);
});
});

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env node
import * as fs from 'fs-extra';
import * as path from 'path';
import * as process from 'process';
import validatePluginId from '@joplin/lib/services/plugins/utils/validatePluginId';
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '@joplin/lib/markdownUtils';
const { execCommand, execCommandVerbose, rootDir, resolveRelativePathWithinDir, gitPullTry } = require('./tool-utils.js');
import { execCommand2, resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js';
interface NpmPackage {
name: string;
@@ -45,6 +47,7 @@ async function checkPluginRepository(dirPath: string) {
const previousDir = process.cwd();
process.chdir(dirPath);
await gitRepoCleanTry();
await gitPullTry();
process.chdir(previousDir);
}
@@ -70,13 +73,13 @@ async function extractPluginFilesFromPackage(existingManifests: any, workDir: st
const previousDir = process.cwd();
process.chdir(workDir);
await execCommandVerbose('npm', ['install', packageName, '--save', '--ignore-scripts']);
await execCommand2(`npm install ${packageName} --save --ignore-scripts`, { showOutput: false });
const pluginDir = resolveRelativePathWithinDir(workDir, 'node_modules', packageName, 'publish');
const files = await fs.readdir(pluginDir);
const manifestFilePath = path.resolve(pluginDir, files.find(f => path.extname(f) === '.json'));
const pluginFilePath = path.resolve(pluginDir, files.find(f => path.extname(f) === '.jpl'));
const manifestFilePath = path.resolve(pluginDir, files.find((f: any) => path.extname(f) === '.json'));
const pluginFilePath = path.resolve(pluginDir, files.find((f: any) => path.extname(f) === '.jpl'));
if (!(await fs.pathExists(manifestFilePath))) throw new Error(`Could not find manifest file at ${manifestFilePath}`);
if (!(await fs.pathExists(pluginFilePath))) throw new Error(`Could not find plugin file at ${pluginFilePath}`);
@@ -155,23 +158,41 @@ async function updateReadme(readmePath: string, manifests: any) {
const tableRegex = /<!-- PLUGIN_LIST -->([^]*)<!-- PLUGIN_LIST -->/;
const content = await fs.readFile(readmePath, 'utf8');
const content = await fs.pathExists(readmePath) ? await fs.readFile(readmePath, 'utf8') : '<!-- PLUGIN_LIST -->\n<!-- PLUGIN_LIST -->';
const newContent = content.replace(tableRegex, `<!-- PLUGIN_LIST -->\n${mdTable}\n<!-- PLUGIN_LIST -->`);
await fs.writeFile(readmePath, newContent, 'utf8');
}
async function main() {
// We assume that the repository is located in a directory next to the main
// Joplin monorepo.
const repoDir = path.resolve(path.dirname(rootDir), 'joplin-plugins');
interface CommandBuildArgs {
pluginRepoDir: string;
}
enum ProcessingActionType {
Add = 1,
Update = 2,
}
function commitMessage(actionType: ProcessingActionType, npmPackage: NpmPackage): string {
const output: string[] = [];
if (actionType === ProcessingActionType.Add) {
output.push('New');
} else {
output.push('Update');
}
output.push(`${npmPackage.name}@${npmPackage.version}`);
return output.join(': ');
}
async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
const tempDir = `${repoDir}/temp`;
const pluginManifestsPath = path.resolve(repoDir, 'manifests.json');
const obsoleteManifestsPath = path.resolve(repoDir, 'obsoletes.json');
const errorsPath = path.resolve(repoDir, 'errors.json');
await checkPluginRepository(repoDir);
await fs.mkdirp(tempDir);
const originalPluginManifests = await readJsonFile(pluginManifestsPath, {});
@@ -181,31 +202,31 @@ async function main() {
...obsoleteManifests,
};
const searchResults = (await execCommand('npm search joplin-plugin --searchlimit 5000 --json')).trim();
const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults));
const packageTempDir = `${tempDir}/packages`;
await fs.mkdirp(packageTempDir);
const previousDir = process.cwd();
process.chdir(packageTempDir);
await execCommand('npm init --yes --loglevel silent');
await execCommand2('npm init --yes --loglevel silent', { quiet: true });
const errors: any[] = [];
const errors: any = await readJsonFile(errorsPath, {});
delete errors[npmPackage.name];
let actionType: ProcessingActionType = ProcessingActionType.Update;
let manifests: any = {};
// TODO: validate plugin ID when publishing
try {
const destDir = `${repoDir}/plugins/`;
const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, npmPackage.name, destDir);
for (const npmPackage of npmPackages) {
try {
const packageName = npmPackage.name;
const destDir = `${repoDir}/plugins/`;
const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, packageName, destDir);
if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest;
} catch (error) {
console.error(error);
errors.push(error);
if (!existingManifests[manifest.id]) {
actionType = ProcessingActionType.Add;
}
if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest;
} catch (error) {
console.error(error);
errors[npmPackage.name] = error.message || '';
}
// We preserve the original manifests so that if a plugin has been removed
@@ -219,20 +240,91 @@ async function main() {
await fs.writeFile(pluginManifestsPath, JSON.stringify(manifests, null, '\t'), 'utf8');
if (errors.length) {
const toWrite = errors.map((e: any) => {
return {
message: e.message || '',
};
});
await fs.writeFile(errorsPath, JSON.stringify(toWrite, null, '\t'), 'utf8');
if (Object.keys(errors).length) {
await fs.writeFile(errorsPath, JSON.stringify(errors, null, '\t'), 'utf8');
} else {
await fs.remove(errorsPath);
}
await updateReadme(`${repoDir}/README.md`, manifests);
process.chdir(previousDir);
await fs.remove(tempDir);
process.chdir(repoDir);
if (!(await gitRepoClean())) {
await execCommand2('git add -A', { showOutput: false });
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, npmPackage)], { showOutput: false });
} else {
console.info('Nothing to commit');
}
}
async function commandBuild(args: CommandBuildArgs) {
console.info(new Date(), 'Building repository...');
const repoDir = args.pluginRepoDir;
await checkPluginRepository(repoDir);
const searchResults = (await execCommand2('npm search joplin-plugin --searchlimit 5000 --json', { showOutput: false })).trim();
const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults));
for (const npmPackage of npmPackages) {
await processNpmPackage(npmPackage, repoDir);
}
await execCommand2('git push');
}
async function commandVersion() {
const p = await readJsonFile(path.resolve(__dirname, 'package.json'));
console.info(`Version ${p.version}`);
}
async function main() {
const scriptName: string = 'plugin-repo-cli';
const commands: Record<string, Function> = {
build: commandBuild,
version: commandVersion,
};
let selectedCommand: string = '';
let selectedCommandArgs: string = '';
function setSelectedCommand(name: string, args: any) {
selectedCommand = name;
selectedCommandArgs = args;
}
require('yargs')
.scriptName(scriptName)
.usage('$0 <cmd> [args]')
.command('build <plugin-repo-dir>', 'Build the plugin repository', (yargs: any) => {
yargs.positional('plugin-repo-dir', {
type: 'string',
describe: 'Directory where the plugin repository is located',
});
}, (args: any) => setSelectedCommand('build', args))
.command('version', 'Gives version info', () => {}, (args: any) => setSelectedCommand('version', args))
.help()
.argv;
if (!selectedCommand) {
console.error(`Please provide a command name or type \`${scriptName} --help\` for help`);
process.exit(1);
}
if (!commands[selectedCommand]) {
console.error(`No such command: ${selectedCommand}`);
process.exit(1);
}
await commands[selectedCommand](selectedCommandArgs);
}
main().catch((error) => {

7815
packages/plugin-repo-cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "1.7.4",
"description": "",
"main": "index.js",
"bin": {
"plugin-repo-cli": "./index.js"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"tsc": "tsc --project tsconfig.json",
"watch": "tsc --watch --project tsconfig.json",
"test": "jest",
"test-ci": "npm run test"
},
"author": "",
"license": "MIT",
"dependencies": {
"@joplin/lib": "^1.7.2",
"@joplin/tools": "^1.7.2",
"fs-extra": "^9.0.1",
"yargs": "^16.0.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"jest": "^26.6.3",
"typescript": "^4.1.3"
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"**/node_modules",
],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "1.0.24",
"version": "1.7.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "1.0.24",
"version": "1.7.2",
"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,7 +24,7 @@
"typescript": "^4.0.5"
},
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.15",
"@joplin/fork-htmlparser2": "^4.1.19",
"font-awesome-filetypes": "^2.1.0",
"fs-extra": "^8.1.0",
"highlight.js": "^10.2.1",

View File

@@ -4,64 +4,86 @@
First copy `.env-sample` to `.env` and edit the values in there:
- `JOPLIN_BASE_URL`: This is the base public URL where the service will be running. For example, if you want it to run from `https://example.com/joplin`, this is what you should set the URL to. The base URL can include the port.
- `JOPLIN_PORT`: The local port on which the Docker container will listen. You would typically map this port to 443 (TLS) with a reverse proxy.
- `APP_BASE_URL`: This is the base public URL where the service will be running. For example, if you want it to run from `https://example.com/joplin`, this is what you should set the URL to. The base URL can include the port.
- `APP_PORT`: The local port on which the Docker container will listen. You would typically map this port to 443 (TLS) with a reverse proxy.
## Install application
## Running the server
To start the server with default configuration, run:
```shell
wget https://github.com/laurent22/joplin/archive/server-v1.6.4.tar.gz
tar xzvf server-v1.6.4.tar.gz
mv joplin-server-v1.6.4 joplin-server
cd joplin-server
docker-compose --file docker-compose.server.yml up --detach
docker run --env-file .env -p 22300:22300 joplin/server:latest
```
This will start the server, which will listen on port **22300** on **localhost**.
This will start the server, which will listen on port **22300** on **localhost**. By default it will use SQLite, which allows you to test the app without setting up a database. To run it for production though, you'll want to connect the container to a database, as described below.
Due to the restart policy defined in the docker-compose file, the server will be restarted automatically whenever the host reboots.
## Setup the database
You can setup the container to either use an existing PostgreSQL server, or connect it to a new one using docker-compose
### Using an existing PostgreSQL server
To use an existing PostgresSQL server, set the following environment variables in the .env file:
```conf
DB_CLIENT=pg
POSTGRES_PASSWORD=joplin
POSTGRES_DATABASE=joplin
POSTGRES_USER=joplin
POSTGRES_PORT=5432
POSTGRES_HOST=localhost
```
Make sure that the provided database and user exist as the server will not create them.
### Using docker-compose
A [sample docker-compose file](https://github.com/laurent22/joplin/blob/dev/docker-compose.server.yml
) is available to show how to use Docker to install both the database and server and connect them:
## Setup reverse proxy
You will then need to expose this server to the internet by setting up a reverse proxy, and that will depend on how your server is currently configured, and whether you already have Nginx or Apache running:
Once Joplin Server is running, you will then need to expose it to the internet by setting up a reverse proxy, and that will depend on how your server is currently configured, and whether you already have Nginx or Apache running:
- [Apache Reverse Proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html)
- [Nginx Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
## Setup admin user
## Setup the website
For the following instructions, we'll assume that the Joplin server is running on `https://example.com/joplin`.
Once the server is exposed to the internet, you can open the admin UI and get it ready for synchronisation. For the following instructions, we'll assume that the Joplin server is running on `https://example.com/joplin`.
By default, the instance will be setup with an admin user with email **admin@localhost** and password **admin** and you should change this by opening the admin UI. To do so, open `https://example.com/joplin/login`. From there, go to Profile and change the admin password.
### Secure the admin user
## Setup a user for sync
By default, the instance will be setup with an admin user with email **admin@localhost** and password **admin** and you should change this. To do so, open `https://example.com/joplin/login` and login as admin. Then go to the Profile section and change the admin password.
While the admin user can be used for synchronisation, it is recommended to create a separate non-admin user for it. To do, open the admin UI and navigate to the Users page - from there you can create a new user.
### Create a user for sync
Once this is done, you can use the email and password you specified to sync this user account with your Joplin clients.
While the admin user can be used for synchronisation, it is recommended to create a separate non-admin user for it. To do so, navigate to the Users page - from there you can create a new user. Once this is done, you can use the email and password you specified to sync this user account with your Joplin clients.
## Checking the logs
Checking the log can be done the standard Docker way:
```shell
```bash
# With Docker:
docker logs --follow CONTAINER
# With docker-compose:
docker-compose --file docker-compose.server.yml logs
```
# Set up for development
# Setup for development
## Setting up the database
## Setup up the database
### SQLite
The server supports SQLite for development and test units. To use it, open `src/config-dev.ts` and uncomment the sqlite3 config.
By default the server supports SQLite for development, so nothing needs to be setup.
### PostgreSQL
It's best to use PostgreSQL as this is what is used in production, however it requires Docker.
To use it, from the monorepo root, run `docker-compose --file docker-compose.server-dev.yml up`, which will start the PostgreSQL database.
To use Postgres, from the monorepo root, run `docker-compose --file docker-compose.server-dev.yml up`, which will start the PostgreSQL database.
## Starting the server
From `packages/server`, run `npm run start-dev`
From `packages/server`, run `npm run start-dev`

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "1.7.0",
"version": "1.7.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -5938,6 +5938,11 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-env-file": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/node-env-file/-/node-env-file-0.1.8.tgz",
"integrity": "sha1-/Mt7BQ9zW1oz2p65N89vGrRX+2k="
},
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "1.7.0",
"version": "1.7.1",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json dist/app.js --env dev",
@@ -26,6 +26,7 @@
"markdown-it": "^12.0.4",
"mustache": "^3.1.0",
"nanoid": "^2.1.1",
"node-env-file": "^0.1.8",
"nodemon": "^2.0.6",
"pg": "^8.5.1",
"query-string": "^6.8.3",

View File

@@ -5,32 +5,30 @@ import * as Koa from 'koa';
import * as fs from 'fs-extra';
import { argv } from 'yargs';
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, baseUrl } from './config';
import configDev from './config-dev';
import configProd from './config-prod';
import configBuildTypes from './config-buildTypes';
import config, { initConfig, runningInDocker, EnvVariables } from './config';
import { createDb, dropDb } from './tools/dbTools';
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection } from './db';
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteFilePath } from './db';
import modelFactory from './models/factory';
import { AppContext, Config, Env } from './utils/types';
import { AppContext, Env } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
import routeHandler from './middleware/routeHandler';
import notificationHandler from './middleware/notificationHandler';
import ownerHandler from './middleware/ownerHandler';
const nodeEnvFile = require('node-env-file');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit();
const env: Env = argv.env as Env || Env.Prod;
interface Configs {
[name: string]: Config;
}
const configs: Configs = {
dev: configDev,
prod: configProd,
buildTypes: configBuildTypes,
const envVariables: Record<Env, EnvVariables> = {
dev: {
SQLITE_DATABASE: 'dev',
},
buildTypes: {
SQLITE_DATABASE: 'buildTypes',
},
prod: {}, // Actually get the env variables from the environment
};
let appLogger_: LoggerWrapper = null;
@@ -52,11 +50,31 @@ app.use(ownerHandler);
app.use(notificationHandler);
app.use(routeHandler);
async function main() {
const configObject: Config = configs[env];
if (!configObject) throw new Error(`Invalid env: ${env}`);
function markPasswords(o: Record<string, any>): Record<string, any> {
const output: Record<string, any> = {};
initConfig(configObject);
for (const k of Object.keys(o)) {
if (k.toLowerCase().includes('password')) {
output[k] = '********';
} else {
output[k] = o[k];
}
}
return output;
}
async function main() {
if (argv.envFile) {
nodeEnvFile(argv.envFile);
}
if (!envVariables[env]) throw new Error(`Invalid env: ${env}`);
initConfig({
...envVariables[env],
...process.env,
});
await fs.mkdirp(config().logDir);
Logger.fsDriver_ = new FsDriverNode();
@@ -90,8 +108,11 @@ async function main() {
await createDb(config().database);
} else {
appLogger().info(`Starting server (${env}) on port ${config().port} and PID ${process.pid}...`);
appLogger().info('Public base URL:', baseUrl());
appLogger().info('DB Config:', config().database);
appLogger().info('Running in Docker:', runningInDocker());
appLogger().info('Public base URL:', config().baseUrl);
appLogger().info('Log dir:', config().logDir);
appLogger().info('DB Config:', markPasswords(config().database));
if (config().database.client === 'sqlite3') appLogger().info('DB file:', sqliteFilePath(config().database.name));
const appContext = app.context as AppContext;
@@ -104,13 +125,13 @@ async function main() {
appLogger().info('Connection check:', connectionCheckLogInfo);
appContext.env = env;
appContext.db = connectionCheck.connection;
appContext.models = modelFactory(appContext.db, baseUrl());
appContext.models = modelFactory(appContext.db, config().baseUrl);
appContext.appLogger = appLogger;
appLogger().info('Migrating database...');
await migrateDb(appContext.db);
appLogger().info(`Call this for testing: \`curl ${baseUrl()}/api/ping\``);
appLogger().info(`Call this for testing: \`curl ${config().baseUrl}/api/ping\``);
app.listen(config().port);
}

View File

@@ -1,21 +0,0 @@
import { Config } from './utils/types';
import * as pathUtils from 'path';
const rootDir = pathUtils.dirname(__dirname);
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
const envPort = Number(process.env.JOPLIN_PORT);
const config: Config = {
port: (envPort && !isNaN(envPort)) ? envPort : 22300,
viewDir: viewDir,
rootDir: rootDir,
layoutDir: `${viewDir}/layouts`,
logDir: `${rootDir}/logs`,
database: {
client: 'pg',
name: 'joplin',
},
};
export default config;

View File

@@ -1,13 +0,0 @@
import { Config } from './utils/types';
import configBase from './config-base';
const config: Config = {
...configBase,
database: {
name: 'buildTypes',
client: 'sqlite3',
asyncStackTraces: true,
},
};
export default config;

View File

@@ -1,22 +0,0 @@
import { Config } from './utils/types';
import configBase from './config-base';
const config: Config = {
...configBase,
database: {
name: 'dev',
client: 'sqlite3',
asyncStackTraces: true,
},
// database: {
// client: 'pg',
// name: 'joplin',
// user: 'joplin',
// host: 'localhost',
// port: 5432,
// password: 'joplin',
// asyncStackTraces: true,
// },
};
export default config;

View File

@@ -1,20 +0,0 @@
import { Config } from './utils/types';
import configBase from './config-base';
const rootDir = '/home/joplin/';
const config: Config = {
...configBase,
rootDir: rootDir,
logDir: `${rootDir}/logs`,
database: {
client: 'pg',
name: 'joplin',
user: 'joplin',
host: 'db',
port: 5432,
password: 'joplin',
},
};
export default config;

View File

@@ -1,13 +0,0 @@
import { Config } from './utils/types';
import configBase from './config-base';
const config: Config = {
...configBase,
database: {
name: 'DYNAMIC',
client: 'sqlite3',
asyncStackTraces: true,
},
};
export default config;

View File

@@ -1,28 +1,93 @@
import { rtrimSlashes } from '@joplin/lib/path-utils';
import { Config } from './utils/types';
import { Config, DatabaseConfig, DatabaseConfigClient } from './utils/types';
import * as pathUtils from 'path';
let baseConfig_: Config = null;
let baseUrl_: string = null;
export interface EnvVariables {
APP_BASE_URL?: string;
APP_PORT?: string;
DB_CLIENT?: string;
RUNNING_IN_DOCKER?: string;
export function initConfig(baseConfig: Config) {
baseConfig_ = baseConfig;
POSTGRES_PASSWORD?: string;
POSTGRES_DATABASE?: string;
POSTGRES_USER?: string;
POSTGRES_HOST?: string;
POSTGRES_PORT?: string;
SQLITE_DATABASE?: string;
}
let runningInDocker_: boolean = false;
export function runningInDocker(): boolean {
return runningInDocker_;
}
function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): string {
if (env.POSTGRES_HOST) {
// When running within Docker, the app localhost is different from the
// host's localhost. To access the latter, Docker defines a special host
// called "host.docker.internal", so here we swap the values if necessary.
if (runningInDocker && ['localhost', '127.0.0.1'].includes(env.POSTGRES_HOST)) {
return 'host.docker.internal';
} else {
return env.POSTGRES_HOST;
}
}
return null;
}
function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): DatabaseConfig {
if (env.DB_CLIENT === 'pg') {
return {
client: DatabaseConfigClient.PostgreSQL,
name: env.POSTGRES_DATABASE || 'joplin',
user: env.POSTGRES_USER || 'joplin',
password: env.POSTGRES_PASSWORD || 'joplin',
port: env.POSTGRES_PORT ? Number(env.POSTGRES_PORT) : 5432,
host: databaseHostFromEnv(runningInDocker, env) || 'localhost',
};
}
return {
client: DatabaseConfigClient.SQLite,
name: env.SQLITE_DATABASE || 'prod',
asyncStackTraces: true,
};
}
function baseUrlFromEnv(env: any, appPort: number): string {
if (env.APP_BASE_URL) {
return rtrimSlashes(env.APP_BASE_URL);
} else {
return `http://localhost:${appPort}`;
}
}
let config_: Config = null;
export function initConfig(env: EnvVariables) {
runningInDocker_ = !!env.RUNNING_IN_DOCKER;
const rootDir = pathUtils.dirname(__dirname);
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
config_ = {
rootDir: rootDir,
viewDir: viewDir,
layoutDir: `${viewDir}/layouts`,
logDir: `${rootDir}/logs`,
database: databaseConfigFromEnv(runningInDocker_, env),
port: appPort,
baseUrl: baseUrlFromEnv(env, appPort),
};
}
function config(): Config {
if (!baseConfig_) throw new Error('Config has not been initialized!');
return baseConfig_;
}
export function baseUrl() {
if (baseUrl_) return baseUrl_;
if (process.env.JOPLIN_BASE_URL) {
baseUrl_ = rtrimSlashes(process.env.JOPLIN_BASE_URL);
} else {
baseUrl_ = `http://localhost:${config().port}`;
}
return baseUrl_;
if (!config_) throw new Error('Config has not been initialized!');
return config_;
}
export default config;

View File

@@ -47,15 +47,15 @@ export interface ConnectionCheckResult {
connection: DbConnection;
}
export function sqliteFilePath(dbConfig: DatabaseConfig): string {
return `${sqliteDbDir}/db-${dbConfig.name}.sqlite`;
export function sqliteFilePath(name: string): string {
return `${sqliteDbDir}/db-${name}.sqlite`;
}
export function makeKnexConfig(dbConfig: DatabaseConfig): KnexDatabaseConfig {
const connection: DbConfigConnection = {};
if (dbConfig.client === 'sqlite3') {
connection.filename = sqliteFilePath(dbConfig);
connection.filename = sqliteFilePath(dbConfig.name);
} else {
connection.database = dbConfig.name;
connection.host = dbConfig.host;

View File

@@ -4,47 +4,80 @@ import { defaultAdminEmail, defaultAdminPassword, NotificationLevel } from '../d
import { _ } from '@joplin/lib/locale';
import Logger from '@joplin/lib/Logger';
import * as MarkdownIt from 'markdown-it';
import config from '../config';
const logger = Logger.create('notificationHandler');
async function handleChangeAdminPasswordNotification(ctx: AppContext) {
if (!ctx.owner.is_admin) return;
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
const notificationModel = ctx.models.notification({ userId: ctx.owner.id });
if (defaultAdmin) {
await notificationModel.add(
'change_admin_password',
NotificationLevel.Important,
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
);
} else {
await notificationModel.markAsRead('change_admin_password');
}
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
await notificationModel.add(
'using_sqlite_in_prod',
NotificationLevel.Important,
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
);
}
}
async function handleSqliteInProdNotification(ctx: AppContext) {
if (!ctx.owner.is_admin) return;
const notificationModel = ctx.models.notification({ userId: ctx.owner.id });
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
await notificationModel.add(
'using_sqlite_in_prod',
NotificationLevel.Important,
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
);
}
}
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
const markdownIt = new MarkdownIt();
const notificationModel = ctx.models.notification({ userId: ctx.owner.id });
const notifications = await notificationModel.allUnreadByUserId(ctx.owner.id);
const views: NotificationView[] = [];
for (const n of notifications) {
views.push({
id: n.id,
messageHtml: markdownIt.render(n.message),
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
closeUrl: notificationModel.closeUrl(n.id),
});
}
return views;
}
// The role of this middleware is to inspect the system and to generate
// notifications for any issue it finds. It is only active for logged in users
// on the website. It is inactive for API calls.
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
ctx.notifications = [];
try {
if (isApiRequest(ctx)) return next();
if (!ctx.owner) return next();
const user = ctx.owner;
if (!user) return next();
const notificationModel = ctx.models.notification({ userId: user.id });
if (user.is_admin) {
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
if (defaultAdmin) {
await notificationModel.add(
'change_admin_password',
NotificationLevel.Important,
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
);
} else {
await notificationModel.markAsRead('change_admin_password');
}
}
const markdownIt = new MarkdownIt();
const notifications = await notificationModel.allUnreadByUserId(user.id);
const views: NotificationView[] = [];
for (const n of notifications) {
views.push({
id: n.id,
messageHtml: markdownIt.render(n.message),
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
closeUrl: notificationModel.closeUrl(n.id),
});
}
ctx.notifications = views;
await handleChangeAdminPasswordNotification(ctx);
await handleSqliteInProdNotification(ctx);
ctx.notifications = await makeNotificationViews(ctx);
} catch (error) {
logger.error(error);
}

View File

@@ -26,6 +26,7 @@ export default async function(ctx: AppContext) {
ctx.response.status = 200;
ctx.response.body = await mustacheService.renderView(responseObject, {
notifications: ctx.notifications || [],
hasNotifications: !!ctx.notifications && !!ctx.notifications.length,
owner: ctx.owner,
});
} else {

View File

@@ -39,6 +39,10 @@ async function findLocalFile(path: string): Promise<string> {
const router = new Router();
router.public = true;
// Used to serve static files, so it needs to be public because for example the
// login page, which is public, needs access to the CSS files.
router.get('', async (path: SubPath, ctx: Koa.Context) => {
const localPath = await findLocalFile(path.raw);

View File

@@ -6,7 +6,7 @@ import { ErrorNotFound } from '../../utils/errors';
import { File } from '../../db';
import { createPaginationLinks, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
import { setQueryParameters } from '../../utils/urlUtils';
import { baseUrl } from '../../config';
import config from '../../config';
import { formatDateTime } from '../../utils/time';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
@@ -51,7 +51,7 @@ router.get('files/:id', async (path: SubPath, ctx: AppContext) => {
async function fileToViewItem(file: File, fileFullPaths: Record<string, string>): Promise<any> {
const filePath = fileFullPaths[file.id];
let url = `${baseUrl()}/files/${filePath}`;
let url = `${config().baseUrl}/files/${filePath}`;
if (!file.is_directory) {
url += '/content';
} else {
@@ -88,7 +88,7 @@ router.get('files/:id', async (path: SubPath, ctx: AppContext) => {
const view: View = defaultView('files');
view.content.paginatedFiles = { ...paginatedFiles, items: files };
view.content.paginationLinks = paginationLinks;
view.content.postUrl = `${baseUrl()}/files`;
view.content.postUrl = `${config().baseUrl}/files`;
view.content.parentId = parent.id;
view.cssFiles = ['index/files'];
view.partials.push('pagination');

View File

@@ -2,7 +2,7 @@ import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { AppContext } from '../../utils/types';
import { formParse } from '../../utils/requestUtils';
import { baseUrl } from '../../config';
import config from '../../config';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
@@ -27,7 +27,7 @@ router.post('login', async (_path: SubPath, ctx: AppContext) => {
const session = await ctx.models.session().authenticate(body.fields.email, body.fields.password);
ctx.cookies.set('sessionId', session.id);
return redirect(ctx, `${baseUrl()}/home`);
return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) {
return makeView(error);
}

View File

@@ -1,7 +1,7 @@
import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { AppContext } from '../../utils/types';
import { baseUrl } from '../../config';
import config from '../../config';
import { contextSessionId } from '../../utils/requestUtils';
const router = new Router();
@@ -10,7 +10,7 @@ router.post('logout', async (_path: SubPath, ctx: AppContext) => {
const sessionId = contextSessionId(ctx, false);
ctx.cookies.set('sessionId', '');
await ctx.models.session().logout(sessionId);
return redirect(ctx, `${baseUrl()}/login`);
return redirect(ctx, `${config().baseUrl}/login`);
});
export default router;

View File

@@ -4,7 +4,7 @@ import { AppContext, HttpMethod } from '../../utils/types';
import { formParse } from '../../utils/requestUtils';
import { ErrorUnprocessableEntity } from '../../utils/errors';
import { User } from '../../db';
import { baseUrl } from '../../config';
import config from '../../config';
import { View } from '../../services/MustacheService';
import defaultView from '../../utils/defaultView';
@@ -55,11 +55,11 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
let postUrl = '';
if (isNew) {
postUrl = `${baseUrl()}/users/new`;
postUrl = `${config().baseUrl}/users/new`;
} else if (isMe) {
postUrl = `${baseUrl()}/users/me`;
postUrl = `${config().baseUrl}/users/me`;
} else {
postUrl = `${baseUrl()}/users/${user.id}`;
postUrl = `${config().baseUrl}/users/${user.id}`;
}
const view: View = defaultView('user');
@@ -100,7 +100,7 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
throw new Error('Invalid form button');
}
return redirect(ctx, `${baseUrl()}/users${userIsMe(path) ? '/me' : ''}`);
return redirect(ctx, `${config().baseUrl}/users${userIsMe(path) ? '/me' : ''}`);
} catch (error) {
const endPoint = router.findEndPoint(HttpMethod.GET, 'users/:id');
return endPoint(path, ctx, user, error);

View File

@@ -1,6 +1,6 @@
import * as Mustache from 'mustache';
import * as fs from 'fs-extra';
import config, { baseUrl } from '../config';
import config from '../config';
export interface RenderOptions {
partials?: any;
@@ -30,7 +30,7 @@ class MustacheService {
private get defaultLayoutOptions(): any {
return {
baseUrl: baseUrl(),
baseUrl: config().baseUrl,
};
}
@@ -41,7 +41,7 @@ class MustacheService {
private resolvesFilePaths(type: string, paths: string[]): string[] {
const output: string[] = [];
for (const path of paths) {
output.push(`${baseUrl()}/${type}/${path}.${type}`);
output.push(`${config().baseUrl}/${type}/${path}.${type}`);
}
return output;
}

View File

@@ -33,7 +33,7 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
await execCommand(cmd.join(' '));
} else if (config.client === 'sqlite3') {
const filePath = sqliteFilePath(config);
const filePath = sqliteFilePath(config.name);
if (await fs.pathExists(filePath)) {
if (options.dropIfExists) {
@@ -71,6 +71,6 @@ export async function dropDb(config: DatabaseConfig, options: DropDbOptions = nu
throw error;
}
} else if (config.client === 'sqlite3') {
await fs.remove(sqliteFilePath(config));
await fs.remove(sqliteFilePath(config.name));
}
}

View File

@@ -1,9 +1,8 @@
import { User, Session, DbConnection, connectDb, disconnectDb, File, truncateTables } from '../../db';
import { User, Session, DbConnection, connectDb, disconnectDb, File, truncateTables, sqliteFilePath } from '../../db';
import { createDb } from '../../tools/dbTools';
import modelFactory from '../../models/factory';
import baseConfig from '../../config-tests';
import { AppContext, Config, Env } from '../types';
import { initConfig } from '../../config';
import { AppContext, Env } from '../types';
import config, { initConfig } from '../../config';
import FileModel from '../../models/FileModel';
import Logger from '@joplin/lib/Logger';
import FakeCookies from './koa/FakeCookies';
@@ -34,18 +33,16 @@ export async function tempDir(): Promise<string> {
return tempDir_;
}
let createdDbName_: string = null;
export async function beforeAllDb(unitName: string) {
const config: Config = {
...baseConfig,
database: {
...baseConfig.database,
name: unitName,
},
};
createdDbName_ = unitName;
initConfig(config);
await createDb(config.database, { dropIfExists: true });
db_ = await connectDb(config.database);
initConfig({
SQLITE_DATABASE: createdDbName_,
});
await createDb(config().database, { dropIfExists: true });
db_ = await connectDb(config().database);
}
export async function afterAllTests() {
@@ -58,6 +55,12 @@ export async function afterAllTests() {
await fs.remove(tempDir_);
tempDir_ = null;
}
if (createdDbName_) {
const filePath = sqliteFilePath(createdDbName_);
await fs.remove(filePath);
createdDbName_ = null;
}
}
export async function beforeEachDb() {

View File

@@ -25,8 +25,13 @@ export interface AppContext extends Koa.Context {
owner: User;
}
export enum DatabaseConfigClient {
PostgreSQL = 'pg',
SQLite = 'sqlite3',
}
export interface DatabaseConfig {
client: string;
client: DatabaseConfigClient;
name: string;
host?: string;
port?: number;
@@ -40,8 +45,11 @@ export interface Config {
rootDir: string;
viewDir: string;
layoutDir: string;
// Not that, for now, nothing is being logged to file. Log is just printed
// to stdout, which is then handled by Docker own log mechanism
logDir: string;
database: DatabaseConfig;
baseUrl: string;
}
export enum HttpMethod {

View File

@@ -13,8 +13,8 @@
</head>
<body class="page-{{{pageName}}}">
{{> navbar}}
{{> notifications}}
<main class="main">
{{> notifications}}
{{{contentHtml}}}
</main>
</body>

View File

@@ -1,9 +1,11 @@
{{#global.notifications}}
<div class="notification is-{{level}}" id="notification-{{id}}">
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
{{{messageHtml}}}
</div>
{{/global.notifications}}
{{#global.hasNotifications}}
{{#global.notifications}}
<div class="notification is-{{level}}" id="notification-{{id}}">
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
{{{messageHtml}}}
</div>
{{/global.notifications}}
{{/global.hasNotifications}}
<script>
onDocumentReady(function() {

View File

@@ -155,4 +155,15 @@ utils.registerGulpTasks = function(gulp, tasks) {
}
};
utils.setPackagePrivateField = async function(filePath, value) {
const text = await fs.readFile(filePath, 'utf8');
const obj = JSON.parse(text);
if (!value) {
delete obj.private;
} else {
obj.private = true;
}
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
};
module.exports = utils;

View File

@@ -0,0 +1,40 @@
// // npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest && npx lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/plugin-repo-builder
// import { execCommand2, rootDir, gitPullTry } from './tool-utils.js';
// async function main() {
// const argv = require('yargs').argv;
// console.info(process.argv);
// const args = [];
// if (argv.D) args.push('-D');
// await execCommand2('npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest');
// //npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest && npx lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/plugin-repo-builder
// // process.chdir(rootDir);
// // const version = (await execCommand2('npm version patch')).trim();
// // const versionShort = version.substr(1);
// // const tagName = `server-${version}`;
// // process.chdir(rootDir);
// // console.info(`Running from: ${process.cwd()}`);
// // await execCommand2(`docker build -t "joplin/server:${versionShort}" -f Dockerfile.server .`);
// // await execCommand2(`docker tag "joplin/server:${versionShort}" "joplin/server:latest"`);
// // await execCommand2(`docker push joplin/server:${versionShort}`);
// // await execCommand2('docker push joplin/server:latest');
// // await execCommand2('git add -A');
// // await execCommand2(`git commit -m 'Server release ${version}'`);
// // await execCommand2(`git tag ${tagName}`);
// // await execCommand2('git push');
// // await execCommand2('git push --tags');
// }
// main().catch((error) => {
// console.error('Fatal error');
// console.error(error);
// process.exit(1);
// });

View File

@@ -14,7 +14,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.3.1\n"
"X-Generator: Poedit 2.4.2\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: packages/app-desktop/bridge.js:106 packages/app-desktop/bridge.js:110
#: packages/app-desktop/bridge.js:126 packages/app-desktop/bridge.js:134
@@ -61,16 +63,17 @@ msgstr "Batal"
msgid ""
"The app is now going to close. Please relaunch it to complete the process."
msgstr ""
"Aplikasi akan ditutup. Silakan luncurkan ulang untuk menyelesaikan prosesnya."
#: packages/app-desktop/plugins/GotoAnything.js:431
#, fuzzy
msgid ""
"Type a note title or part of its content to jump to it. Or type # followed "
"by a tag name, or @ followed by a notebook name. Or type : to search for "
"commands."
msgstr ""
"Ketik judul catatan atau bagian dari isinya untuk melompat ke sana. Atau "
"ketik # diikuti dengan nama label, atau @ diikuti dengan nama buku catatan."
"Ketik judul catatan atau bagian di isinya untuk temukan catatan yang dicari. "
"Atau ketik # diikuti dengan nama label, atau @ diikuti dengan nama buku "
"catatan. Atau ketik : untuk mencari perintah."
#: packages/app-desktop/plugins/GotoAnything.js:456
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:20
@@ -116,9 +119,8 @@ msgid "New version: %s"
msgstr "Versi baru: %s"
#: packages/app-desktop/checkForUpdates.js:154
#, fuzzy
msgid "Download"
msgstr "Terunduh"
msgstr "Unduh"
#: packages/app-desktop/checkForUpdates.js:154
msgid "Full Release Notes"
@@ -277,7 +279,6 @@ msgid "strong text"
msgstr "teks yang ditekankan lebih kuat (strong text)"
#: packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:179
#, fuzzy
msgid "emphasised text"
msgstr "teks yang ditekankan (emphasized text)"
@@ -324,7 +325,6 @@ msgid "Paste"
msgstr "Tempel"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
#, fuzzy
msgid "Checkbox list"
msgstr "Kotak centang"
@@ -355,7 +355,7 @@ msgstr "Masukkan Waktu dan Tanggal"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:1003
msgid "Drop notes or files here"
msgstr ""
msgstr "Taruh catatan atau file di sini"
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:1003
#, fuzzy
@@ -368,6 +368,8 @@ msgid ""
"Please wait for all attachments to be downloaded and decrypted. You may also "
"switch to %s to edit the note."
msgstr ""
"Silakan tunggu sampai semua lampiran sudah diunduh dan didekripsi. Anda juga "
"bisa beralih ke %s untuk mengedit catatan."
#: packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js:73
msgid "There was an error downloading this attachment:"
@@ -388,7 +390,7 @@ msgstr "Simpan sebagai..."
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.js:74
msgid "Reveal file in folder"
msgstr ""
msgstr "Buka file di berkas"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.js:82
msgid "Copy path to clipboard"
@@ -416,24 +418,26 @@ msgid ""
"This Rich Text editor has a number of limitations and it is recommended to "
"be aware of them before using it."
msgstr ""
"Editor Rich Text ini punya beberapa keterbatasan dan Anda dianjurkan untuk "
"tahu keterbatasannya sebelum menggunakan editor ini."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:340
msgid "Read more about it"
msgstr ""
msgstr "Baca lebih lanjut"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:345
msgid "Dismiss"
msgstr ""
msgstr "Abaikan"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:381
msgid "The following attachments are being watched for changes:"
msgstr ""
msgstr "Lampiran ini sedang diawasi untuk perubahan:"
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:384
msgid ""
"The attachments will no longer be watched when you switch to a different "
"note."
msgstr ""
msgstr "Lampiran tidak akan diawasi ketika Anda berganti ke catatan lain."
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:389
#, javascript-format
@@ -481,34 +485,32 @@ msgid "Horizontal Rule"
msgstr "Garis Horisontal"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:88
#, fuzzy
msgid "Delete line"
msgstr "Hapus catatan?"
msgstr "Hapus catatan"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
msgid "Undo"
msgstr ""
msgstr "Urungkan"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
msgid "Redo"
msgstr ""
msgstr "Ulangi"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:100
msgid "Indent less"
msgstr ""
msgstr "Kurangi indentasi"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:104
msgid "Indent more"
msgstr ""
msgstr "Tambah indentasi"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:108
#, fuzzy
msgid "Toggle comment"
msgstr "Alihkan daftar catatan"
msgstr "Lihat/sembunyikan komentar"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:112
msgid "Sort selected lines"
msgstr ""
msgstr "Sortir baris terpilih"
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:116
msgid "Swap line up"
@@ -580,9 +582,9 @@ msgstr "Properti catatan"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:63
#: packages/lib/services/KeymapService.js:176
#, fuzzy, javascript-format
#, javascript-format
msgid "Error: %s"
msgstr "Kesalahan"
msgstr "Terjadi kesalahan: %s"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:110
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:443
@@ -599,12 +601,11 @@ msgstr "Impor"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:126
msgid "Command"
msgstr ""
msgstr "Perintah"
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:127
#, fuzzy
msgid "Keyboard Shortcut"
msgstr "Mode Papan Ketik"
msgstr "Pintasan Papan Ketik"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:14
#: packages/app-desktop/gui/MenuBar.js:156 packages/app-desktop/app.js:338
@@ -627,9 +628,8 @@ msgid "Website and documentation"
msgstr "Situs web dan dokumentasi"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:24
#, fuzzy
msgid "Hide Joplin"
msgstr "Tentang Joplin"
msgstr "Sembunyikan Joplin"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:26
#: packages/app-desktop/gui/MenuBar.js:408
@@ -637,9 +637,8 @@ msgid "Close Window"
msgstr "Tutup jendela"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:30
#, fuzzy
msgid "Preferences"
msgstr "Preferensi..."
msgstr "Preferensi"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:30
#: packages/app-desktop/gui/MenuBar.js:284 packages/app-desktop/gui/Root.js:100
@@ -647,19 +646,20 @@ msgid "Options"
msgstr "Pilihan"
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:37
#, fuzzy
msgid "Invalid"
msgstr "Jawaban tidak valid: %s"
msgstr "Jawaban tidak valid"
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:49
msgid "Press the shortcut"
msgstr ""
msgstr "Klik pintasan"
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:49
msgid ""
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
"shortcut."
msgstr ""
"Klik pintasan kemudian tekan ENTER atau tekan BACKSPACE untuk menghapus "
"pintasan."
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:50
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:96
@@ -673,11 +673,13 @@ msgid ""
"may take a few minutes to complete and the app needs to be restarted. To "
"proceed please click on the link."
msgstr ""
"Target sinkronisasi harus diperbarui sebelum Joplin dapat sinkronisasi. "
"Operasinya akan berjalan beberapa menit dan aplikasi perlu dimulai ulang. "
"Untuk melanjutkan, silakan klik linknya."
#: packages/app-desktop/gui/MainScreen/MainScreen.js:405
#, fuzzy
msgid "Restart and upgrade"
msgstr "Kunci master yang perlu upgrade"
msgstr "Mulai ulang dan upgrade"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:409
msgid "Some items cannot be synchronised."
@@ -719,6 +721,8 @@ msgstr "Info lain"
#: packages/app-desktop/gui/MainScreen/MainScreen.js:528
msgid "Use the arrows to move the layout items. Press \"Escape\" to exit."
msgstr ""
"Gunakan tanda panah untuk memindahkan item tata letak. Tekan \"Escape\" "
"untuk keluar."
#: packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js:18
msgid "Statistics..."
@@ -782,13 +786,12 @@ msgid "Toggle editor layout"
msgstr "Alihkan tata letak editor"
#: packages/app-desktop/gui/MainScreen/commands/toggleEditors.js:18
#, fuzzy
msgid "Toggle editors"
msgstr "Alihkan tata letak editor"
msgstr "Alihkan editor"
#: packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js:16
msgid "Change application layout"
msgstr ""
msgstr "Ubah tata letak aplikasi"
#: packages/app-desktop/gui/MainScreen/commands/renameTag.js:30
msgid "Rename tag:"
@@ -805,7 +808,7 @@ msgstr "Judul buku catatan:"
#: packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js:19
#: packages/lib/services/spellChecker/SpellCheckerService.js:180
msgid "Spell checker"
msgstr ""
msgstr "Pemeriksa ejaan"
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js:16
msgid "Share note..."
@@ -999,9 +1002,8 @@ msgstr "Templat"
#: packages/app-desktop/gui/MenuBar.js:377
#: packages/app-desktop/gui/MenuBar.js:423
#, fuzzy
msgid "Export all"
msgstr "Ekspor"
msgstr "Ekspor semuanya"
#: packages/app-desktop/gui/MenuBar.js:389
#, javascript-format
@@ -1236,27 +1238,23 @@ msgstr ""
"Penting: Anda hanya perlu menjalankan ini SEKALI pada satu perangkat."
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:198
#, fuzzy
msgid "Re-encryption"
msgstr "Enkripsi"
msgstr "Enkripsi ulang"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:213
msgid "Ignore"
msgstr ""
msgstr "Abaikan"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:244
#: packages/app-mobile/components/screens/encryption-config.js:215
#, fuzzy
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Mengaktifkan enkripsi berarti *semua* catatan dan lampiran Anda akan "
"disinkronkan ulang dan dikirim dalam keadaan terenkripsi ke target "
"sinkronisasi. Jangan sampai kehilangan kata sandinya karena, untuk alasan "
"keamanan, ini akan menjadi *hanya* satu-satunya cara untuk mendekripsi data! "
"Untuk mengaktifkan enkripsi, silakan masukkan kata sandi Anda di bawah ini."
"Menonaktifkan enkripsi berarti *semua* catatan dan lampiran Anda akan "
"disinkronkan ulang dan dikirim dalam keadaan tidak terenkripsi ke target "
"sinkronisasi. Apa Anda mau lanjut?"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:246
#: packages/app-mobile/components/screens/encryption-config.js:150
@@ -1269,8 +1267,8 @@ msgstr ""
"Mengaktifkan enkripsi berarti *semua* catatan dan lampiran Anda akan "
"disinkronkan ulang dan dikirim dalam keadaan terenkripsi ke target "
"sinkronisasi. Jangan sampai kehilangan kata sandinya karena, untuk alasan "
"keamanan, ini akan menjadi *hanya* satu-satunya cara untuk mendekripsi data! "
"Untuk mengaktifkan enkripsi, silakan masukkan kata sandi Anda di bawah ini."
"keamanan, ini *hanya* satu-satunya cara untuk mendekripsi data! Untuk "
"mengaktifkan enkripsi, silakan masukkan kata sandi Anda di bawah ini."
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:275
#: packages/app-mobile/components/screens/encryption-config.js:258
@@ -1283,28 +1281,24 @@ msgid "Enable encryption"
msgstr "Aktifkan enkripsi"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:290
#, fuzzy
msgid "Master Keys"
msgstr "Kunci Master %s"
msgstr "Kunci Master"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:304
#, fuzzy
msgid "Active"
msgstr "Aksi"
msgstr "Aktif"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:314
msgid "Source"
msgstr ""
msgstr "Sumber"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:329
#, fuzzy
msgid "Password"
msgstr "Kata sandi:"
msgstr "Kata sandi"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:334
#, fuzzy
msgid "Password OK"
msgstr "Kata sandi:"
msgstr "Kata sandi OK"
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:343
msgid ""
@@ -1312,6 +1306,10 @@ msgid ""
"as \"active\"). Any of the keys might be used for decryption, depending on "
"how the notes or notebooks were originally encrypted."
msgstr ""
"Peringatan: Hanya satu kunci master yang akan digunakan untuk enkripsi "
"(kunci yang ditandai \"aktif\"). Kunci yang lain mungkin digunakan untuk "
"dekripsi, tergantung bagaimana buku catatan atau catatannya awalnya "
"dienkripsi."
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:371
#: packages/app-mobile/components/screens/encryption-config.js:248
@@ -1422,7 +1420,7 @@ msgstr "Telusuri..."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:456
msgid "The application must be restarted for these changes to take effect."
msgstr ""
msgstr "Aplikasi harus dimulai ulang agar perubahan ini diterapkan."
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:468
#: packages/app-desktop/gui/NoteList/NoteList.js:163
@@ -1431,25 +1429,24 @@ msgstr "Lakukan sekarang"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:468
msgid "Later"
msgstr ""
msgstr "Nanti"
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:521
#, fuzzy
msgid "Restart now"
msgstr "Dapatkan sekarang:"
msgstr "Mulai ulang sekarang"
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.js:27
msgid "Apply"
msgstr "Terapkan"
#: packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js:137
#, fuzzy, javascript-format
#, javascript-format
msgid "Delete plugin \"%s\"?"
msgstr "Hapus catatan \"%s\"?"
msgstr "Hapus plugin \"%s\"?"
#: packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js:181
msgid "Install plugin"
msgstr ""
msgstr "Pasang plugin"
#: packages/app-desktop/gui/PromptDialog.min.js:249
msgid "Clear"
@@ -1486,12 +1483,11 @@ msgstr "Viewer"
#: packages/app-desktop/gui/NoteContentPropertiesDialog.js:105
#, javascript-format
msgid "Read time: %s min"
msgstr ""
msgstr "Waktu baca: %s menit"
#: packages/app-desktop/gui/NoteContentPropertiesDialog.js:108
#, fuzzy
msgid "Statistics"
msgstr "Statistik..."
msgstr "Statistik"
#: packages/app-desktop/gui/NoteContentPropertiesDialog.js:113
#: packages/app-desktop/gui/ShareNoteDialog.js:175
@@ -1619,18 +1615,16 @@ msgid_plural "Copy Shareable Links"
msgstr[0] "Salin Tautan yang Dapat Dibagikan"
#: packages/app-desktop/commands/toggleExternalEditing.js:18
#, fuzzy
msgid "Toggle external editing"
msgstr "Hentikan pengeditan eksternal"
msgstr "Ubah pengeditan eksternal"
#: packages/app-desktop/commands/toggleExternalEditing.js:37
msgid "Stop"
msgstr ""
#: packages/app-desktop/commands/copyDevCommand.js:18
#, fuzzy
msgid "Copy dev mode command to clipboard"
msgstr "Salin jalur ke papan klip"
msgstr "Salin perintah dev mode ke papan klip"
#: packages/app-desktop/commands/stopExternalEditing.js:18
msgid "Stop external editing"
@@ -1647,9 +1641,8 @@ msgid "Error opening note in editor: %s"
msgstr "Terjadi kesalahan saat membuka catatan di editor: %s"
#: packages/app-desktop/commands/openProfileDirectory.js:18
#, fuzzy
msgid "Open profile directory"
msgstr "Buka direktori templat"
msgstr "Buka direktori profil"
#: packages/app-desktop/app.js:336
#, javascript-format
@@ -1677,9 +1670,8 @@ msgid "Save alarm"
msgstr "Simpan alarm"
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:28
#, fuzzy
msgid "Open"
msgstr "Buka..."
msgstr "Buka"
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:29
#: packages/app-mobile/components/screens/Note.js:796
@@ -1750,8 +1742,9 @@ msgid "Refresh"
msgstr "Segarkan"
#: packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js:42
#, fuzzy
msgid "Sync Target Upgrade"
msgstr ""
msgstr "Sinkronisasi Target Peningkatan"
#: packages/app-mobile/components/screens/notes.js:154
#: packages/app-cli/app/command-rmbook.js:26
@@ -1967,11 +1960,14 @@ msgid ""
"\n"
"You may turn off this option at any time in the Configuration screen."
msgstr ""
"Untuk menghubungkan geolokasi dengan catatan, aplikasi perlu izin Anda untuk "
"mengakses lokasi Anda.\n"
"\n"
"Anda dapat mematikan fungsi ini kapan saja di layar Konfigurasi."
#: packages/app-mobile/components/screens/Note.js:343
#, fuzzy
msgid "Permission needed"
msgstr "Izin penggunaan kamera"
msgstr "Izin diperlukan"
#: packages/app-mobile/components/screens/Note.js:449
#: packages/app-cli/app/command-rmnote.js:27
@@ -2052,11 +2048,12 @@ msgid ""
"The default admin password is insecure and has not been changed! [Change it "
"now](%s)"
msgstr ""
"Kata sandi default admin tidak aman dan belum diubah. [Ubah sekarang](%s)"
#: packages/lib/onedrive-api-node-utils.js:46
#, javascript-format
msgid "All potential ports are in use - please report the issue at %s"
msgstr ""
msgstr "Semua port potensial sedang digunakan - silakan laporkan isu di %s"
#: packages/lib/onedrive-api-node-utils.js:86
msgid ""
@@ -2092,9 +2089,8 @@ msgid "Dropbox"
msgstr "Dropbox"
#: packages/lib/SyncTargetJoplinServer.js:30
#, fuzzy
msgid "Joplin Server"
msgstr "Situs web Joplin"
msgstr "Server Joplin"
#: packages/lib/shim-init-node.js:201
#, javascript-format
@@ -2239,21 +2235,19 @@ msgstr ""
#: packages/lib/models/Setting.js:259
msgid "Joplin Server URL"
msgstr ""
msgstr "URL Server Joplin"
#: packages/lib/models/Setting.js:273
#, fuzzy
msgid "Joplin Server Directory"
msgstr "Direktori Ekspor Joplin"
#: packages/lib/models/Setting.js:283
msgid "Joplin Server username"
msgstr ""
msgstr "Nama pengguna Server Joplin"
#: packages/lib/models/Setting.js:293
#, fuzzy
msgid "Joplin Server password"
msgstr "Masukkan kata sandi master:"
msgstr "Kata sandi Server Joplin"
#: packages/lib/models/Setting.js:305
msgid "Attachment download behaviour"
@@ -2386,9 +2380,8 @@ msgid "Enable typographer support"
msgstr "Aktifkan dukungan typographer"
#: packages/lib/models/Setting.js:562
#, fuzzy
msgid "Enable Linkify"
msgstr "Aktifkan riwayat catatan"
msgstr "Aktifkan Linkify"
#: packages/lib/models/Setting.js:563
msgid "Enable math expressions"
@@ -2403,17 +2396,16 @@ msgid "Enable Mermaid diagrams support"
msgstr "Aktifkan dukungan diagram Mermaid"
#: packages/lib/models/Setting.js:566
#, fuzzy
msgid "Enable audio player"
msgstr "Aktifkan emoji markdown"
msgstr "Aktifkan pemutar audio"
#: packages/lib/models/Setting.js:567
msgid "Enable video player"
msgstr ""
msgstr "Aktifkan pemutar video"
#: packages/lib/models/Setting.js:568
msgid "Enable PDF viewer"
msgstr ""
msgstr "Aktifkan penampil PDF"
#: packages/lib/models/Setting.js:569
msgid "Enable ==mark== syntax"
@@ -2490,20 +2482,18 @@ msgid "Editor font family"
msgstr "Keluarga font pada editor"
#: packages/lib/models/Setting.js:641
#, fuzzy
msgid ""
"This should be a *monospace* font or some elements will render incorrectly. "
"If the font is incorrect or empty, it will default to a generic monospace "
"font."
msgstr ""
"Ini harus font *monospace* atau ia tidak akan berfungsi sebagaimana "
"mestinya. Jika font tidak benar atau kosong, font akan disetel secara "
"otomatis ke font monospace generik."
"Ini harus font *monospace* atau beberapa elemen tidak akan berfungsi "
"sebagaimana mestinya. Jika font tidak benar atau kosong, font akan disetel "
"secara otomatis ke font monospace generik."
#: packages/lib/models/Setting.js:662
#, fuzzy
msgid "Custom stylesheet for rendered Markdown"
msgstr "Lembar gaya khusus untuk gaya keseluruhan aplikasi Joplin"
msgstr "Lembar gaya untuk rendering Markdown"
#: packages/lib/models/Setting.js:678
msgid "Custom stylesheet for Joplin-wide app styles"
@@ -2734,9 +2724,8 @@ msgid "Web Clipper"
msgstr "Web Clipper"
#: packages/lib/models/Setting.js:1389
#, fuzzy
msgid "Keyboard Shortcuts"
msgstr "Mode Papan Ketik"
msgstr "Pintasan Papan Ketik"
#: packages/lib/models/Setting.js:1396
msgid ""
@@ -2774,17 +2763,20 @@ msgid "Downloaded"
msgstr "Terunduh"
#: packages/lib/models/Resource.js:382
#, fuzzy, javascript-format
#, javascript-format
msgid "Attachment conflict: \"%s\""
msgstr "Lampiran"
msgstr "Konflik lampiran: \"%s\""
#: packages/lib/models/Resource.js:383
#, fuzzy, javascript-format
#, javascript-format
msgid ""
"There was a [conflict](%s) on the attachment below.\n"
"\n"
"%s"
msgstr "Terjadi kesalahan saat mengunduh lampiran ini:"
msgstr ""
"Terjadi konflik (%s) saat mengunduh lampiran ini:\n"
"\n"
"%s"
#: packages/lib/models/Tag.js:202
#, javascript-format
@@ -2978,6 +2970,7 @@ msgstr "Sedang berlangsung"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
"Tipe item tidak dikenal diunduh - silakan perbarui Joplin ke versi terbaru"
#: packages/lib/JoplinServerApi.js:71
#, javascript-format
@@ -3007,7 +3000,7 @@ msgstr "Sistem berkas"
#: packages/lib/commands/historyForward.js:17
msgid "Forward"
msgstr ""
msgstr "Maju"
#: packages/lib/versionInfo.js:10
#, javascript-format
@@ -3037,7 +3030,7 @@ msgstr "Versi Profil: %s"
#: packages/lib/versionInfo.js:25
#, javascript-format
msgid "Keychain Supported: %s"
msgstr ""
msgstr "Keychain Didukung: %s"
#: packages/lib/services/RevisionService.js:221
msgid "Restored Notes"
@@ -3086,14 +3079,14 @@ msgid "HTML Directory"
msgstr "Direktori HTML"
#: packages/lib/services/interop/InteropService.js:127
#, fuzzy, javascript-format
#, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\""
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\""
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\" dan output \"%s\""
#: packages/lib/services/interop/InteropService.js:150
#, fuzzy, javascript-format
#, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\" and target \"%s\""
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\""
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\" dan target \"%s\""
#: packages/lib/services/interop/InteropService.js:194
#: packages/lib/services/interop/InteropService.js:206
@@ -3131,23 +3124,23 @@ msgstr "Silakan tentukan format impor untuk %s"
#: packages/lib/services/KeymapService.js:273
msgid "command"
msgstr ""
msgstr "perintah"
#: packages/lib/services/KeymapService.js:273
#: packages/lib/services/KeymapService.js:278
#, javascript-format
msgid "\"%s\" is missing the required \"%s\" property."
msgstr ""
msgstr "\"%s\" tidak menemukan properti \"%s\"."
#: packages/lib/services/KeymapService.js:278
#: packages/lib/services/KeymapService.js:285
msgid "accelerator"
msgstr ""
msgstr "akselerator"
#: packages/lib/services/KeymapService.js:285
#, fuzzy, javascript-format
#, javascript-format
msgid "Invalid %s: %s."
msgstr "Jawaban tidak valid: %s"
msgstr "%s tidak valid: %s."
#: packages/lib/services/KeymapService.js:303
#, javascript-format
@@ -3155,11 +3148,13 @@ msgid ""
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
"unexpected behaviour."
msgstr ""
"Akselerator \"%s\" digunakan untuk perintah \"%s\" dan \"%s\". Ini bisa "
"memberikan dampak tidak terduga."
#: packages/lib/services/KeymapService.js:328
#, javascript-format
msgid "Accelerator \"%s\" is not valid."
msgstr ""
msgstr "Akselerator \"%s\" tidak valid."
#: packages/lib/services/report.js:121
msgid "Items that cannot be synchronised"
@@ -3223,9 +3218,8 @@ msgid "Downloaded and encrypted"
msgstr "Terunduh dan terenkripsi"
#: packages/lib/services/report.js:193
#, fuzzy
msgid "Created locally"
msgstr "Item lokal yang dibuat: %d."
msgstr "Item lokal yang dibuat"
#: packages/lib/services/report.js:206
msgid "Attachments that could not be downloaded"
@@ -3280,20 +3274,19 @@ msgstr "Di %s: %s"
#: packages/lib/services/spellChecker/SpellCheckerService.js:107
msgid "No suggestions"
msgstr ""
msgstr "Tidak ada saran"
#: packages/lib/services/spellChecker/SpellCheckerService.js:114
msgid "Add to dictionary"
msgstr ""
msgstr "Tambahkan ke kamus"
#: packages/lib/services/spellChecker/SpellCheckerService.js:153
msgid "Use spell checker"
msgstr ""
msgstr "Gunakan pemeriksa ejaan"
#: packages/lib/services/spellChecker/SpellCheckerService.js:173
#, fuzzy
msgid "Change language"
msgstr "Bahasa"
msgstr "Ubah bahasa"
#: packages/app-cli/app/command-cp.js:13
msgid ""
@@ -3368,9 +3361,9 @@ msgid "Do not ask for confirmation."
msgstr "Jangan meminta konfirmasi."
#: packages/app-cli/app/command-import.js:27
#, fuzzy, javascript-format
#, javascript-format
msgid "Output format: %s"
msgstr "Format sumber: %s"
msgstr "Format output: %s"
#: packages/app-cli/app/command-import.js:65
msgid "Importing notes..."
@@ -3524,7 +3517,7 @@ msgstr ""
#: packages/app-cli/app/command-sync.js:35
msgid "Upgrade the sync target to the latest version."
msgstr ""
msgstr "Perbarui target sinkronisasi ke versi terakhir."
#: packages/app-cli/app/command-sync.js:105
#, javascript-format
@@ -3554,7 +3547,6 @@ msgid "Synchronisation target: %s (%s)"
msgstr "Target sinkronisasi: %s (%s)"
#: packages/app-cli/app/command-sync.js:177
#, fuzzy
msgid "Cannot initialise synchroniser."
msgstr "Tidak dapat menginisiasi penyinkron."
@@ -3569,7 +3561,7 @@ msgstr "Mengunduh sumber daya..."
#: packages/app-cli/app/command-sync.js:242
#, javascript-format
msgid "Sync target must be upgraded! Run `%s` to proceed."
msgstr ""
msgstr "Target sinkronisasi harus diperbarui. Jalankan `%s` untuk melanjutkan."
#: packages/app-cli/app/command-sync.js:260
msgid "Cancelling... Please wait."
@@ -3628,12 +3620,11 @@ msgstr "Catatan tidak ada: \"%s\". Buat?"
#: packages/app-cli/app/command-edit.js:75
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
msgstr "Mulai mengedit catatan. Tutup editor untuk kembali ke prompt."
#: packages/app-cli/app/command-edit.js:97
#, fuzzy
msgid "Note has been saved."
msgstr "Belum ada buku catatan yang ditentukan."
msgstr "Catatan sudah disimpan."
#: packages/app-cli/app/app-gui.js:452
msgid "To delete a tag, untag the associated notes."

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/tools",
"version": "1.0.16",
"version": "1.7.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/tools",
"version": "1.0.16",
"version": "1.7.2",
"description": "Various tools for Joplin",
"main": "index.js",
"author": "Laurent Cozic",
@@ -18,7 +18,7 @@
},
"license": "MIT",
"dependencies": {
"@joplin/lib": "^1.0.16",
"@joplin/lib": "^1.7.2",
"execa": "^4.1.0",
"fs-extra": "^4.0.3",
"gettext-parser": "^1.3.0",

View File

@@ -1,37 +1,40 @@
const { execCommand, githubRelease, rootDir } = require('./tool-utils.js');
const appDir = `${rootDir}/packages/app-desktop`;
async function main() {
const argv = require('yargs').argv;
process.chdir(appDir);
console.info(`Running from: ${process.cwd()}`);
const version = (await execCommand('npm version patch')).trim();
const tagName = version;
console.info(`New version number: ${version}`);
console.info(await execCommand('git add -A'));
console.info(await execCommand(`git commit -m "Desktop release ${version}"`));
console.info(await execCommand(`git tag ${tagName}`));
console.info(await execCommand('git push && git push --tags'));
const releaseOptions = { isDraft: true, isPreRelease: !!argv.beta };
console.info('Release options: ', releaseOptions);
const release = await githubRelease('joplin', tagName, releaseOptions);
console.info(`Created GitHub release: ${release.html_url}`);
console.info('GitHub release page: https://github.com/laurent22/joplin/releases');
console.info(`To create changelog: node packages/tools/git-changelog.js ${version}`);
'use strict';
const __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); }
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, '__esModule', { value: true });
const tool_utils_1 = require('./tool-utils');
const appDir = `${tool_utils_1.rootDir}/packages/app-desktop`;
function main() {
return __awaiter(this, void 0, void 0, function* () {
yield tool_utils_1.gitPullTry(false);
const argv = require('yargs').argv;
process.chdir(appDir);
console.info(`Running from: ${process.cwd()}`);
const version = (yield tool_utils_1.execCommand2('npm version patch')).trim();
const tagName = version;
console.info(`New version number: ${version}`);
console.info(yield tool_utils_1.execCommand2('git add -A'));
console.info(yield tool_utils_1.execCommand2(`git commit -m "Desktop release ${version}"`));
console.info(yield tool_utils_1.execCommand2(`git tag ${tagName}`));
console.info(yield tool_utils_1.execCommand2('git push && git push --tags'));
const releaseOptions = { isDraft: true, isPreRelease: !!argv.beta };
console.info('Release options: ', releaseOptions);
const release = yield tool_utils_1.githubRelease('joplin', tagName, releaseOptions);
console.info(`Created GitHub release: ${release.html_url}`);
console.info('GitHub release page: https://github.com/laurent22/joplin/releases');
console.info(`To create changelog: node packages/tools/git-changelog.js ${version}`);
});
}
main().catch((error) => {
console.error('Fatal error');
console.error(error);
process.exit(1);
});
// # sourceMappingURL=release-electron.js.map

Some files were not shown because too many files have changed in this diff Show More